mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-21 17:02:49 +00:00
Compare commits
204 Commits
infisical-
...
cypress
Author | SHA1 | Date | |
---|---|---|---|
7487b373fe | |||
9a500504a4 | |||
1510c39631 | |||
d0579b383f | |||
b4f4c1064d | |||
d72d3940e6 | |||
7217bcb3d8 | |||
2faa9222d8 | |||
589f0bc134 | |||
bd6dc3d4c0 | |||
9338babda6 | |||
6f9e8644d7 | |||
2fdb10277e | |||
15d2c536ed | |||
a304228961 | |||
c865b78b41 | |||
be80ac999e | |||
076fe58325 | |||
66bfab1994 | |||
b92c50addd | |||
8fbca05052 | |||
d99830067e | |||
cdc8ef95ab | |||
072e97b956 | |||
4f26a7cad3 | |||
7bb6ff3d0c | |||
ecccec8e35 | |||
7fd15a06e5 | |||
5d4a37004e | |||
aa61fd091d | |||
04ac54bcfa | |||
38dbf1e738 | |||
ddf9d7848c | |||
0b40de49ec | |||
b1d16cab39 | |||
fb7c7045e9 | |||
d570828e47 | |||
2a92b6c787 | |||
ee54fdabe1 | |||
136308f299 | |||
ba41244877 | |||
c4dcf334f0 | |||
66bac3ef48 | |||
e5347719c3 | |||
275416a08f | |||
abe1f54aab | |||
13c1e2b349 | |||
f5a9afec61 | |||
d0a85c98b2 | |||
e0669cae7c | |||
2c011b7d53 | |||
1b24a9b6e9 | |||
00c173aead | |||
2e15ad0625 | |||
3f0b6dc6c1 | |||
f766a1eb29 | |||
543c55b5a6 | |||
cdb1d38f99 | |||
0a53b72cce | |||
b921c376b2 | |||
b1ec59eb67 | |||
4e6e12932a | |||
792c382743 | |||
f5c8e537c9 | |||
4bf09a8efc | |||
001265cf2a | |||
a56a135396 | |||
9838c29867 | |||
4f5946b252 | |||
dc23517133 | |||
5e4d4f56e3 | |||
a855a2cee6 | |||
e86258949c | |||
f119c921d0 | |||
b6ef55783e | |||
feade5d029 | |||
8f74d20e74 | |||
0eb7896b59 | |||
9fcecc9c92 | |||
ee6afa8983 | |||
6f4ac02558 | |||
5971480ca9 | |||
d222b09ba5 | |||
a9fd0374bd | |||
ca008c809a | |||
6df7590051 | |||
60bd5e57fc | |||
703a7a316a | |||
f4de7a2c56 | |||
383825672b | |||
c6124d7444 | |||
ee80f4a89b | |||
0b3b014bf5 | |||
7f463cabce | |||
b1962129a3 | |||
28ad403665 | |||
cb893f71ee | |||
80a3ea42ac | |||
aafd7f0884 | |||
faacb75034 | |||
7caac2e64c | |||
df636c91b4 | |||
9dc97f7208 | |||
4fd227c85f | |||
04c7d49477 | |||
63588b4e44 | |||
fc43511f5d | |||
a995627815 | |||
c2f7923c1d | |||
abf6034aca | |||
5fce85ca41 | |||
702d28faca | |||
dbeafe1f5d | |||
46f700023b | |||
25b988ca9a | |||
41af5cea93 | |||
e21daf6771 | |||
c0f81ec84e | |||
c85e2c71ca | |||
9ae6e5ea1c | |||
d3026a98d8 | |||
14b8c2c12a | |||
d9a69441c4 | |||
f46a23dabf | |||
9e2d6daeba | |||
a2bebb5afa | |||
9fc5303a97 | |||
97a5b509b7 | |||
7660119584 | |||
a51d202d51 | |||
34273b30f2 | |||
726f38c15f | |||
390c2cc4d9 | |||
49098b7693 | |||
501d940558 | |||
7234c014c8 | |||
f3908e6b2a | |||
cf4eb629f2 | |||
95af82963f | |||
bd8c17d720 | |||
d3bc95560c | |||
4a838b788f | |||
01c655699c | |||
3456dfbd86 | |||
560bde297c | |||
c2ca4d77fc | |||
3b3f78ee3c | |||
d6a5c50fd9 | |||
839fcc2775 | |||
eb2f433f43 | |||
04c74293ed | |||
cdf4440848 | |||
20b584b7b8 | |||
3779209ed5 | |||
105c2e51ee | |||
66aa218ad9 | |||
fb3a386aa3 | |||
d723d26d2e | |||
8a576196a3 | |||
2cf5fd80ca | |||
74534cfbaa | |||
66787b1f93 | |||
890082acbc | |||
a364b174e0 | |||
2bb2ccc19e | |||
3bbf770027 | |||
2610356d45 | |||
67e164e2bb | |||
84fcb82116 | |||
4502d12e46 | |||
ef6ee6b2e6 | |||
e902a54af0 | |||
50efb8b8bd | |||
5450c1126a | |||
4929022523 | |||
85378e25aa | |||
b54c29fc48 | |||
fcf3f2837e | |||
0ada343b6f | |||
d0b8aba990 | |||
4365be9b75 | |||
b0c398688b | |||
1141408d5b | |||
b24bff5af6 | |||
c67432a56f | |||
edeb6bbc66 | |||
f72e240ce5 | |||
77ec17ccd4 | |||
6e992858aa | |||
9cda85f03e | |||
c65a53f1f7 | |||
aa1e0b0f28 | |||
4683dc7869 | |||
bc489e65ca | |||
45b85ab962 | |||
698a268b5f | |||
0d74752169 | |||
255705501f | |||
f59b3b3305 | |||
53856ff868 | |||
84d094b4d8 | |||
efb14ca267 | |||
1896442168 | |||
1cdd840485 |
.dockerignore.env.exampledocker-compose.ymlecosystem.config.js
.github
.gitignoreDockerfile.standalone-infisicalREADME.mdbackend
.eslintrcpackage-lock.jsonpackage.json
src
config
controllers
v1
index.tsmembershipController.tsorganizationController.tssecretImpsController.tssecretScanningController.tssecretsFolderController.tsworkspaceController.ts
v2
v3
ee
controllers
v1
index.tsroleController.tssecretApprovalPolicyController.tssecretApprovalRequestsController.tsworkspaceController.ts
v3
models
auditLog
gitAppInstallationSession.tsgitAppOrganizationInstallation.tsgitRisks.tsindex.tsrole.tssecretApprovalPolicy.tssecretApprovalRequest.tssecretVersion.tsroutes
services
validation
helpers
index.tsintegrations
interfaces
middleware
models
index.tssecret.tssecretApprovalRequest.tsserviceToken.tsserviceTokenData.tsserviceTokenDataV3.tsserviceTokenDataV3Key.tsuser.ts
queues
integrations
secret-scanning
routes
v1
v2
v3
services
utils
validation
folders.tsindex.tsintegration.tsintegrationAuth.tsorganization.tssecretImports.tssecretScanning.tssecrets.tsserviceTokenDataV3.tsworkspace.ts
variables
cli/packages/cmd
cypress.config.jscypress
e2e
fixtures
support
docs
cli/commands
documentation
getting-started
platform
images
activity-logs.pngauthentication-google-redirect.pngdashboard-add-folder.pngdashboard-folder-overview.pngdashboard-folders.pngdashboard-name-modal-organization.pngdashboard-name-selected.pngdashboard-secrets-overview.pngdashboard.pngexample-secret-referencing.pngintegrations-teamcity-auth.pngintegrations-teamcity-create.pngintegrations-teamcity-dashboard.pngintegrations-teamcity-projects.pngintegrations-teamcity-serverurl.pngintegrations-teamcity-tokens.pngintegrations-teamcity.pngorganization-ic-add.pngorganization-ic.pngorganization-members-add.pngorganization-members.pngorganization-overview.pngorganization-service-accounts.pngorganization.pngpit-commits.pngpit-snapshot.pngpit-snapshots.pngproject-download.pngproject-drag-drop.pngproject-envar-override.pngproject-envar-toggle-moved.pngproject-envar-toggle.pngproject-environment.pngproject-folder-token.pngproject-hide.pngproject-integrations.pngproject-members.pngproject-search-typed.pngproject-search.pngproject-token-add.pngproject-token-added.pngproject-token-name.pngproject-token-old-add.pngproject-token-old-permissions.pngproject-token-permissions.pngproject_settings_page.pngsecret-import-add.pngsecret-import-change-order.pngsecret-versioning.png
integrations
aws
integrations-aws-access-key-1.pngintegrations-aws-access-key-2.pngintegrations-aws-access-key-3.pngintegrations-aws-iam-1.pngintegrations-aws-parameter-store-auth.pngintegrations-aws-parameter-store-create.pngintegrations-aws-parameter-store-iam-2.pngintegrations-aws-parameter-store-iam-3.pngintegrations-aws-parameter-store.pngintegrations-aws-secret-manager-auth.pngintegrations-aws-secret-manager-create.pngintegrations-aws-secret-manager-iam-2.pngintegrations-aws-secret-manager-iam-3.pngintegrations-aws-secret-manager.png
bitbucket
checkly
integrations-checkly-auth.pngintegrations-checkly-create.pngintegrations-checkly-dashboard.pngintegrations-checkly-token.pngintegrations-checkly.png
circleci
integrations-circleci-auth.pngintegrations-circleci-create.pngintegrations-circleci-token.pngintegrations-circleci.png
cloud-66
integrations-cloud-66-access-token.pngintegrations-cloud-66-copy-pat.pngintegrations-cloud-66-create.pngintegrations-cloud-66-dashboard.pngintegrations-cloud-66-done.pngintegrations-cloud-66-infisical-dashboard.pngintegrations-cloud-66-paste-pat.pngintegrations-cloud-66-pat-setup.pngintegrations-cloud-66-pat.png
cloudflare
integrations-cloudflare-auth.pngintegrations-cloudflare-create.pngintegrations-cloudflare-credentials-1.pngintegrations-cloudflare-credentials-2.pngintegrations-cloudflare-credentials-3.pngintegrations-cloudflare-credentials-4.pngintegrations-cloudflare.png
codefresh
integrations-codefresh-auth.pngintegrations-codefresh-create.pngintegrations-codefresh-dashboard.pngintegrations-codefresh-token.pngintegrations-codefresh.png
digital-ocean
integrations-do-dashboard.pngintegrations-do-enter-token.pngintegrations-do-select-projects.pngintegrations-do-success.pngintegrations-do-token-modal.png
flyio
integrations-flyio-auth.pngintegrations-flyio-create.pngintegrations-flyio-dashboard.pngintegrations-flyio-token.pngintegrations-flyio.png
hashicorp-vault
integrations-hashicorp-vault-access-1.pngintegrations-hashicorp-vault-access-2.pngintegrations-hashicorp-vault-access-3.pngintegrations-hashicorp-vault-auth.pngintegrations-hashicorp-vault-cluster-url.pngintegrations-hashicorp-vault-create.pngintegrations-hashicorp-vault-engine-1.pngintegrations-hashicorp-vault-engine-2.pngintegrations-hashicorp-vault-engine-3.pngintegrations-hashicorp-vault-policy-1.pngintegrations-hashicorp-vault-policy-2.pngintegrations-hashicorp-vault-policy-3.pngintegrations-hashicorp-vault-shell.pngintegrations-hashicorp-vault.png
laravel-forge
integrations-laravelforge-api.pngintegrations-laravelforge-auth.pngintegrations-laravelforge-create.pngintegrations-laravelforge-dashboard.pngintegrations-laravelforge-serverid.pngintegrations-laravelforge-servers.pngintegrations-laravelforge.png
northflank
integrations-northflank-auth.pngintegrations-northflank-create.pngintegrations-northflank-dashboard.pngintegrations-northflank-token.pngintegrations-northflank.png
railway
integrations-railway-authorization.pngintegrations-railway-create.pngintegrations-railway-dashboard.pngintegrations-railway-token.pngintegrations-railway.png
render
integrations-render-auth.pngintegrations-render-create.pngintegrations-render-dashboard.pngintegrations-render-token.pngintegrations-render.png
supabase
integrations-supabase-authorization.pngintegrations-supabase-create.pngintegrations-supabase-dashboard.pngintegrations-supabase-token.pngintegrations-supabase.png
terraform
integrations-terraformcloud-auth.pngintegrations-terraformcloud-create.pngintegrations-terraformcloud-dashboard.pngintegrations-terraformcloud-tokens.pngintegrations-terraformcloud-workspaceid.pngintegrations-terraformcloud-workspaces.pngintegrations-terraformcloud.png
travis-ci
integrations-travisci-auth.pngintegrations-travisci-create.pngintegrations-travisci-token.pngintegrations-travisci.png
windmill
platform
audit-logs
folder
ip-allowlisting
organization
organization-members-roles-add-perm.pngorganization-members-roles.pngorganization-projects.pngorganization-settings-auth.pngorganization-settings-general.pngorganization-usage-billing.png
pit-recovery
project
project-secrets-add.pngproject-secrets-comment.pngproject-secrets-delete-batch.pngproject-secrets-delete.pngproject-secrets-download-env.pngproject-secrets-drawer.pngproject-secrets-drop-env.pngproject-secrets-ellipses.pngproject-secrets-filter.pngproject-secrets-override.pngproject-secrets-overview-open.pngproject-secrets-overview.pngproject-secrets-search.pngproject-secrets-tag.pngproject-secrets-unhide.png
secret-references-imports
self-hosting/configuration/email
email-aws-ses-console.pngemail-aws-ses-user.pngemail-gmail-app-access.pngemail-mailhog-credentials.pngemail-resend-create-domain.pngemail-resend-create-key.pngemail-resend-smtp-settings.pngemail-sendgrid-create-key.pngemail-sendgrid-restrictions.pngemail-socketlabs-credentials.pngemail-socketlabs-dashboard.pngemail-socketlabs-domains.png
service-token-permissions.pngsso
integrations
build-tools
cicd
cloud
internals
mint.jsonsecurity
self-hosting
authentication
configuration
deployment-options
frontend
.eslintrc.jsDockerfilecypress.config.jsnext.config.jspackage-lock.jsonpackage.jsontailwind.config.js
cypress
e2e
fixtures
support
public
images
locales/en
lotties
scripts
initialize-standalone-build.shreplace-standalone-build-variable.shreplace-variable.shset-standalone-build-telemetry.shset-telemetry.sh
src
components
analytics
navigation
signup
tags/CreateTagModal
utilities
v2
context/ProjectPermissionContext
helpers
hooks/api
auditLogs
index.tsxorganization
roles
secretApproval
secretApprovalRequest
secretFolders
secretImports
secretSnapshots
secrets
serviceTokens
subscriptions
types.tsusers
workspace
layouts/AppLayout
pages
api/secret-scanning
integrations/github
login
org
project/[id]
signup
views
DashboardPage
DashboardPage.tsxDashboardPage.utils.tsindex.tsx
components
CompareSecret
CreateTagModal
FolderSection
SecretDetailDrawer
SecretDropzone
SecretImportForm
SecretImportSection
SecretInputRow
SecretTableHeader
IntegrationsPage/components/IntegrationsSection
Login
Org/NonePage
Project
AuditLogsPage/components
MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection
SecretApprovalPage
SecretMainPage
SecretMainPage.store.tsxSecretMainPage.tsxSecretMainPage.types.tsindex.tsx
components
ActionBar
CreateSecretForm
FolderListView
PitDrawer
SecretDropzone
SecretImportListView
SecretListView
GenRandomNumber.tsxSecretDetaiSidebar.tsxSecretItem.tsxSecretListView.tsxSecretListView.utils.tsindex.tsx
SnapshotView
SecretOverviewPage
SecretScanning/components
Settings
BillingSettingsPage/components/BillingCloudTab
OrgSettingsPage/components
OrgAuthTab
OrgDeleteSection
OrgGeneralTab
OrgIncidentContactsSection
OrgNameChangeSection
PersonalSettingsPage
APIKeySection
AuthMethodSection
DeleteAccountSection
PersonalGeneralTab
ProjectSettingsPage
ProjectSettingsPage.tsx
components
DeleteProjectSection
ProjectServiceTokensTab
SecretTagsSection
ServiceTokenSection
ServiceTokenV3Section
helm-charts/infisical
img
nginx
standalone-entrypoint.sh@ -1,2 +1,10 @@
|
||||
backend/node_modules
|
||||
frontend/node_modules
|
||||
frontend/node_modules
|
||||
backend/frontend-build
|
||||
**/node_modules
|
||||
**/.next
|
||||
.dockerignore
|
||||
.git
|
||||
README.md
|
||||
.dockerignore
|
||||
**/Dockerfile
|
||||
|
@ -9,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
|
||||
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
|
||||
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
|
||||
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
|
||||
JWT_SERVICE_TOKEN_SECRET=f32f716d70a42c5703f4656015e76200
|
||||
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
|
||||
|
||||
# JWT lifetime
|
||||
|
2
.github/resources/docker-compose.be-test.yml
vendored
2
.github/resources/docker-compose.be-test.yml
vendored
@ -6,7 +6,7 @@ services:
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mongo
|
||||
image: infisical/backend:test
|
||||
image: infisical/infisical:test
|
||||
command: npm run start
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
|
41
.github/values.yaml
vendored
41
.github/values.yaml
vendored
@ -1,29 +1,3 @@
|
||||
# secretScanningGitApp:
|
||||
# enabled: false
|
||||
# deploymentAnnotations:
|
||||
# secrets.infisical.com/auto-reload: "true"
|
||||
# image:
|
||||
# repository: infisical/staging_deployment_secret-scanning-git-app
|
||||
|
||||
frontend:
|
||||
enabled: true
|
||||
name: frontend
|
||||
podAnnotations: {}
|
||||
deploymentAnnotations:
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/staging_deployment_frontend
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
kubeSecretRef: managed-secret-frontend
|
||||
service:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
|
||||
frontendEnvironmentVariables: null
|
||||
|
||||
backend:
|
||||
enabled: true
|
||||
name: backend
|
||||
@ -32,7 +6,7 @@ backend:
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/staging_deployment_backend
|
||||
repository: infisical/staging_infisical
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
kubeSecretRef: managed-backend-secret
|
||||
@ -40,12 +14,15 @@ backend:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
resources:
|
||||
limits:
|
||||
memory: 300Mi
|
||||
|
||||
backendEnvironmentVariables: null
|
||||
|
||||
## Mongo DB persistence
|
||||
mongodb:
|
||||
enabled: true
|
||||
enabled: false
|
||||
persistence:
|
||||
enabled: false
|
||||
|
||||
@ -60,14 +37,8 @@ ingress:
|
||||
enabled: true
|
||||
# annotations:
|
||||
# kubernetes.io/ingress.class: "nginx"
|
||||
# cert-manager.io/issuer: letsencrypt-nginx
|
||||
# cert-manager.io/issuer: letsencrypt-nginx
|
||||
hostName: gamma.infisical.com ## <- Replace with your own domain
|
||||
frontend:
|
||||
path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
path: /api
|
||||
pathType: Prefix
|
||||
tls:
|
||||
[]
|
||||
# - secretName: letsencrypt-nginx
|
||||
|
@ -39,7 +39,7 @@ jobs:
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/backend:test
|
||||
tags: infisical/infisical:test
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
@ -93,6 +93,7 @@ jobs:
|
||||
tags: infisical/frontend:test
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
- name: ⏻ Spawn frontend container
|
||||
run: |
|
||||
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
||||
@ -116,3 +117,4 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
|
69
.github/workflows/build-staging-img.yml
vendored
69
.github/workflows/build-staging-img.yml
vendored
@ -2,7 +2,7 @@ name: Build, Publish and Deploy to Gamma
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
backend-image:
|
||||
infisical-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -32,8 +32,9 @@ jobs:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/backend:test
|
||||
context: .
|
||||
file: Dockerfile.standalone-infisical
|
||||
tags: infisical/infisical:test
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
@ -49,66 +50,20 @@ jobs:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: backend
|
||||
context: .
|
||||
file: Dockerfile.standalone-infisical
|
||||
tags: |
|
||||
infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
|
||||
infisical/staging_deployment_backend:latest
|
||||
infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||
infisical/staging_infisical:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
|
||||
frontend-image:
|
||||
name: Build frontend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 📦 Build frontend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
load: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
project: 64mmf0n610
|
||||
context: frontend
|
||||
tags: infisical/staging_deployment_frontend:test
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
- name: ⏻ Spawn frontend container
|
||||
run: |
|
||||
docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
|
||||
- name: 🧪 Test frontend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||
- name: ⏻ Shut down frontend container
|
||||
run: |
|
||||
docker stop infisical-frontend-test
|
||||
- name: 🏗️ Build frontend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: frontend
|
||||
tags: |
|
||||
infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
|
||||
infisical/staging_deployment_frontend:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
gamma-deployment:
|
||||
name: Deploy to gamma
|
||||
runs-on: ubuntu-latest
|
||||
needs: [frontend-image, backend-image]
|
||||
needs: [infisical-image]
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
|
@ -73,3 +73,6 @@ jobs:
|
||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: Dockerfile.standalone-infisical
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -33,7 +33,7 @@ reports
|
||||
junit.xml
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
@ -59,4 +59,6 @@ yarn-error.log*
|
||||
.infisical.json
|
||||
|
||||
# Editor specific
|
||||
.vscode/*
|
||||
.vscode/*
|
||||
|
||||
frontend-build
|
@ -1,7 +1,13 @@
|
||||
ARG POSTHOG_HOST=https://app.posthog.com
|
||||
ARG POSTHOG_API_KEY=posthog-api-key
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
|
||||
FROM node:16-alpine AS frontend-dependencies
|
||||
FROM node:16-alpine AS base
|
||||
|
||||
FROM base AS frontend-dependencies
|
||||
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@ -11,7 +17,7 @@ COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
||||
RUN npm ci --only-production --ignore-scripts
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS frontend-builder
|
||||
FROM base AS frontend-builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependencies
|
||||
@ -27,41 +33,38 @@ ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
|
||||
# Production image
|
||||
FROM node:16-alpine AS frontend-runner
|
||||
FROM base AS frontend-runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
RUN adduser --system --uid 1001 non-root-user
|
||||
|
||||
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
|
||||
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
|
||||
VOLUME /app/.next/cache/images
|
||||
|
||||
ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||
|
||||
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
|
||||
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
||||
COPY --from=frontend-builder /app/public ./public
|
||||
RUN chown nextjs:nodejs ./public/data
|
||||
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
RUN chown non-root-user:nodejs ./public/data
|
||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
USER non-root-user
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
##
|
||||
## BACKEND
|
||||
##
|
||||
FROM node:16-alpine AS backend-build
|
||||
FROM base AS backend-build
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 non-root-user
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@ -69,10 +72,11 @@ COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
COPY /backend .
|
||||
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:16-alpine AS backend-runner
|
||||
FROM base AS backend-runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@ -81,27 +85,44 @@ RUN npm ci --only-production
|
||||
|
||||
COPY --from=backend-build /app .
|
||||
|
||||
RUN mkdir frontend-build
|
||||
|
||||
# Production stage
|
||||
FROM node:16-alpine AS production
|
||||
FROM base AS production
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 non-root-user
|
||||
|
||||
## set pre baked keys
|
||||
ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Install PM2
|
||||
RUN npm install -g pm2
|
||||
# Copy ecosystem.config.js
|
||||
COPY ecosystem.config.js .
|
||||
|
||||
RUN apk add --no-cache nginx
|
||||
|
||||
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
|
||||
|
||||
COPY --from=backend-runner /app /backend
|
||||
|
||||
COPY --from=frontend-runner /app/ /app/
|
||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||
|
||||
EXPOSE 80
|
||||
ENV PORT 8080
|
||||
ENV HTTPS_ENABLED false
|
||||
ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
ENV TELEMETRY_ENABLED true
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
USER non-root-user
|
||||
|
||||
CMD ["./standalone-entrypoint.sh"]
|
||||
|
||||
CMD ["pm2-runtime", "start", "ecosystem.config.js"]
|
||||
|
||||
|
||||
|
10
README.md
10
README.md
@ -1,9 +1,8 @@
|
||||
<h1 align="center">
|
||||
<img width="300" src="/img/logoname-black.svg#gh-light-mode-only" alt="infisical">
|
||||
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
|
||||
</h1>
|
||||
<p align="center">
|
||||
<p align="center"><b>Open-source, end-to-end encrypted secret management platform</b>: distribute secrets/configs across your team/infrastructure and prevent secret leaks.</p>
|
||||
<p align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
|
||||
</p>
|
||||
|
||||
<h4 align="center">
|
||||
@ -34,7 +33,7 @@
|
||||
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||
</a>
|
||||
<a href="https://cloudsmith.io/~infisical/repos/">
|
||||
<img src="https://img.shields.io/badge/Downloads-1.38M-orange" alt="Cloudsmith downloads" />
|
||||
<img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
|
||||
</a>
|
||||
<a href="https://infisical.com/slack">
|
||||
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||
@ -44,11 +43,11 @@
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
|
||||
<img src="/img/infisical_github_repo2.png" width="100%" alt="Dashboard" />
|
||||
|
||||
## Introduction
|
||||
|
||||
**[Infisical](https://infisical.com)** is an open source, end-to-end encrypted secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
|
||||
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
|
||||
|
||||
We're on a mission to make secret management more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
|
||||
|
||||
@ -134,7 +133,6 @@ Whether it's big or small, we love contributions. Check out our guide to see how
|
||||
|
||||
Not sure where to get started? You can:
|
||||
|
||||
- [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)!
|
||||
- Join our <a href="https://infisical.com/slack">Slack</a>, and ask us any questions there.
|
||||
- Join our [community calls](https://us06web.zoom.us/j/82623506356) every Wednesday at 11am EST to ask any questions, provide feedback, hangout and more.
|
||||
|
||||
|
@ -26,6 +26,7 @@
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
|
34
backend/package-lock.json
generated
34
backend/package-lock.json
generated
@ -50,6 +50,7 @@
|
||||
"nodemailer": "^6.8.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"posthog-node": "^2.6.0",
|
||||
"probot": "^12.3.1",
|
||||
@ -63,7 +64,7 @@
|
||||
"utility-types": "^3.10.0",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loki": "^6.0.6",
|
||||
"zod": "^3.21.4"
|
||||
"zod": "^3.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.3.1",
|
||||
@ -13727,6 +13728,17 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-gitlab2": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
|
||||
"integrity": "sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg==",
|
||||
"dependencies": {
|
||||
"passport-oauth2": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-google-oauth20": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
|
||||
@ -16684,9 +16696,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
|
||||
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@ -27163,6 +27175,14 @@
|
||||
"passport-oauth2": "1.x.x"
|
||||
}
|
||||
},
|
||||
"passport-gitlab2": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
|
||||
"integrity": "sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg==",
|
||||
"requires": {
|
||||
"passport-oauth2": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"passport-google-oauth20": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
|
||||
@ -29384,9 +29404,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"zod": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
|
||||
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@
|
||||
"nodemailer": "^6.8.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"posthog-node": "^2.6.0",
|
||||
"probot": "^12.3.1",
|
||||
@ -54,7 +55,7 @@
|
||||
"utility-types": "^3.10.0",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loki": "^6.0.6",
|
||||
"zod": "^3.21.4"
|
||||
"zod": "^3.22.3"
|
||||
},
|
||||
"name": "infisical-api",
|
||||
"version": "1.0.0",
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { GITLAB_URL } from "../variables";
|
||||
|
||||
import InfisicalClient from "infisical-node";
|
||||
|
||||
export const client = new InfisicalClient({
|
||||
@ -26,6 +28,7 @@ export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIG
|
||||
export const getJwtProviderAuthSecret = async () => (await client.getSecret("JWT_PROVIDER_AUTH_SECRET")).secretValue;
|
||||
export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
|
||||
export const getJwtSignupSecret = async () => (await client.getSecret("JWT_SIGNUP_SECRET")).secretValue;
|
||||
export const getJwtServiceTokenSecret = async () => (await client.getSecret("JWT_SERVICE_TOKEN_SECRET")).secretValue;
|
||||
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
|
||||
export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
|
||||
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
|
||||
@ -52,6 +55,9 @@ export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIEN
|
||||
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
|
||||
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
|
||||
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
|
||||
export const getClientIdGitLabLogin = async () => (await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
|
||||
export const getClientSecretGitLabLogin = async () => (await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
|
||||
export const getUrlGitLabLogin = async () => (await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
|
||||
|
||||
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
|
||||
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
|
||||
|
@ -16,7 +16,6 @@ import * as workspaceController from "./workspaceController";
|
||||
import * as secretScanningController from "./secretScanningController";
|
||||
import * as webhookController from "./webhookController";
|
||||
import * as secretImpsController from "./secretImpsController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
botController,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { EventType, Role } from "../../ee/models";
|
||||
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
|
||||
@ -15,7 +15,6 @@ import {
|
||||
getUserProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import Role from "../../ee/models/role";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
|
||||
|
||||
|
@ -6,13 +6,11 @@ import {
|
||||
Organization,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { createOrganization as create } from "../../helpers/organization";
|
||||
import { addMembershipsOrg } from "../../helpers/membershipOrg";
|
||||
import { ACCEPTED, ADMIN } from "../../variables";
|
||||
import { getLicenseServerUrl, getSiteURL } from "../../config";
|
||||
import { licenseServerKeyRequest } from "../../config/request";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/organization";
|
||||
import { ACCEPTED } from "../../variables";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@ -34,36 +32,6 @@ export const getOrganizations = async (req: Request, res: Response) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new organization named [organizationName]
|
||||
* and add user as owner
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { organizationName }
|
||||
} = await validateRequest(reqValidator.CreateOrgv1, req);
|
||||
|
||||
// create organization and add user as member
|
||||
const organization = await create({
|
||||
email: req.user.email,
|
||||
name: organizationName
|
||||
});
|
||||
|
||||
await addMembershipsOrg({
|
||||
userIds: [req.user._id.toString()],
|
||||
organizationId: organization._id.toString(),
|
||||
roles: [ADMIN],
|
||||
statuses: [ACCEPTED]
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization with id [organizationId]
|
||||
* @param req
|
||||
|
@ -2,7 +2,7 @@ import { Request, Response } from "express";
|
||||
import { isValidScope } from "../../helpers";
|
||||
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
import { getFolderWithPathFromId } from "../../services/FolderService";
|
||||
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
|
||||
import {
|
||||
BadRequestError,
|
||||
ResourceNotFoundError,
|
||||
@ -95,37 +95,12 @@ export const createSecretImp = async (req: Request, res: Response) => {
|
||||
*/
|
||||
|
||||
const {
|
||||
body: { workspaceId, environment, folderId, secretImport }
|
||||
body: { workspaceId, environment, directory, secretImport }
|
||||
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
if (!folders && folderId !== "root") {
|
||||
throw ResourceNotFoundError({
|
||||
message: "Failed to find folder"
|
||||
});
|
||||
}
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, folderId);
|
||||
secretPath = folderPath;
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// root check
|
||||
let isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
secretImport.environment,
|
||||
secretImport.secretPath
|
||||
);
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
@ -133,27 +108,31 @@ export const createSecretImp = async (req: Request, res: Response) => {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: secretImport.environment,
|
||||
secretPath: secretImport.secretPath
|
||||
})
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
if (!folders && directory !== "/")
|
||||
throw ResourceNotFoundError({ message: "Failed to find folder" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
const importToSecretPath = folders
|
||||
? getFolderWithPathFromId(folders.nodes, folderId).folderPath
|
||||
: "/";
|
||||
|
||||
if (!importSecDoc) {
|
||||
const doc = new SecretImport({
|
||||
workspace: workspaceId,
|
||||
@ -173,7 +152,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
|
||||
importFromEnvironment: secretImport.environment,
|
||||
importFromSecretPath: secretImport.secretPath,
|
||||
importToEnvironment: environment,
|
||||
importToSecretPath
|
||||
importToSecretPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -206,7 +185,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
|
||||
importFromEnvironment: secretImport.environment,
|
||||
importFromSecretPath: secretImport.secretPath,
|
||||
importToEnvironment: environment,
|
||||
importToSecretPath
|
||||
importToSecretPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -563,8 +542,38 @@ export const getSecretImports = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
query: { workspaceId, environment, folderId }
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath: directory
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
@ -575,41 +584,6 @@ export const getSecretImports = async (req: Request, res: Response) => {
|
||||
return res.status(200).json({ secretImport: {} });
|
||||
}
|
||||
|
||||
// check for service token validity
|
||||
const folders = await Folder.findOne({
|
||||
workspace: importSecDoc.workspace,
|
||||
environment: importSecDoc.environment
|
||||
}).lean();
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
||||
secretPath = folderPath;
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
importSecDoc.environment,
|
||||
secretPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
importSecDoc.workspace.toString()
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: importSecDoc.environment,
|
||||
secretPath
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).json({ secretImport: importSecDoc });
|
||||
};
|
||||
|
||||
@ -621,9 +595,39 @@ export const getSecretImports = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId, environment, folderId }
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// check for service token validity
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath: directory
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
@ -634,11 +638,6 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
|
||||
return res.status(200).json({ secrets: [] });
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: importSecDoc.workspace,
|
||||
environment: importSecDoc.environment
|
||||
}).lean();
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { Request, Response } from "express";
|
||||
import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession";
|
||||
import {
|
||||
GitAppInstallationSession,
|
||||
GitAppOrganizationInstallation,
|
||||
GitRisks
|
||||
} from "../../ee/models";
|
||||
import crypto from "crypto";
|
||||
import { Types } from "mongoose";
|
||||
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation";
|
||||
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
|
||||
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
|
||||
import GitRisks, {
|
||||
import {
|
||||
STATUS_RESOLVED_FALSE_POSITIVE,
|
||||
STATUS_RESOLVED_NOT_REVOKED,
|
||||
STATUS_RESOLVED_REVOKED
|
||||
|
@ -9,12 +9,9 @@ import { Secret, ServiceTokenData } from "../../models";
|
||||
import { Folder } from "../../models/folder";
|
||||
import {
|
||||
appendFolder,
|
||||
deleteFolderById,
|
||||
generateFolderId,
|
||||
getAllFolderIds,
|
||||
getFolderByPath,
|
||||
getFolderWithPathFromId,
|
||||
getParentFromFolderId,
|
||||
validateFolderName
|
||||
} from "../../services/FolderService";
|
||||
import {
|
||||
@ -25,13 +22,9 @@ import {
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/folders";
|
||||
|
||||
/**
|
||||
* Create folder with name [folderName] for workspace with id [workspaceId]
|
||||
* and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
|
||||
|
||||
// verify workspace id/environment
|
||||
export const createFolder = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create a folder'
|
||||
@ -107,7 +100,7 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: { workspaceId, environment, folderName, parentFolderId }
|
||||
body: { workspaceId, environment, folderName, directory }
|
||||
} = await validateRequest(reqValidator.CreateFolderV1, req);
|
||||
|
||||
if (!validateFolderName(folderName)) {
|
||||
@ -116,35 +109,28 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// token check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// user check
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
if (req.user) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
const secretPath =
|
||||
folders && parentFolderId
|
||||
? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath
|
||||
: "/";
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
// space has no folders initialized
|
||||
|
||||
if (!folders) {
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// root check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
|
||||
const id = generateFolderId();
|
||||
const folder = new Folder({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
@ -152,14 +138,15 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
id: "root",
|
||||
name: "root",
|
||||
version: 1,
|
||||
children: [{ id, name: folderName, children: [], version: 1 }]
|
||||
children: []
|
||||
}
|
||||
});
|
||||
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
|
||||
await folder.save();
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: folder.nodes
|
||||
nodes: parent
|
||||
});
|
||||
await folderVersion.save();
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
@ -173,9 +160,9 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
type: EventType.CREATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: id,
|
||||
folderId: child.id,
|
||||
folderName,
|
||||
folderPath: `root/${folderName}`
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -183,56 +170,37 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({ folder: { id, name: folderName } });
|
||||
return res.json({ folder: { id: child.id, name: folderName } });
|
||||
}
|
||||
|
||||
const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
|
||||
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
|
||||
const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId(
|
||||
folders.nodes,
|
||||
parentFolderId || "root"
|
||||
);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// root check
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
environment,
|
||||
parentFolderPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
if (!hasCreated) return res.json({ folder: child });
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parentFolder
|
||||
nodes: parent
|
||||
});
|
||||
await folderVersion.save();
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: parentFolderId
|
||||
folderId: child.id
|
||||
});
|
||||
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: folder.id,
|
||||
folderId: child.id,
|
||||
folderName,
|
||||
folderPath
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -240,7 +208,7 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({ folder });
|
||||
return res.json({ folder: child });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -332,8 +300,8 @@ export const updateFolderById = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: { workspaceId, environment, name },
|
||||
params: { folderId }
|
||||
body: { workspaceId, environment, name, directory },
|
||||
params: { folderName }
|
||||
} = await validateRequest(reqValidator.UpdateFolderV1, req);
|
||||
|
||||
if (!validateFolderName(name)) {
|
||||
@ -342,38 +310,31 @@ export const updateFolderById = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
if (req.user) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
const secretPath = getFolderWithPathFromId(folders.nodes, parentFolder.id).folderPath;
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
const folder = parentFolder.children.find(({ id }) => id === folderId);
|
||||
if (!folder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
|
||||
// root check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folder = parentFolder.children.find(({ name }) => name === folderName);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const oldFolderName = folder.name;
|
||||
parentFolder.version += 1;
|
||||
@ -505,24 +466,12 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { folderId },
|
||||
body: { environment, workspaceId }
|
||||
params: { folderName },
|
||||
body: { environment, workspaceId, directory }
|
||||
} = await validateRequest(reqValidator.DeleteFolderV1, req);
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
const delOp = deleteFolderById(folders.nodes, folderId);
|
||||
if (!delOp) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
const { deletedNode: delFolder, parent: parentFolder } = delOp;
|
||||
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
@ -531,12 +480,23 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
|
||||
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const deletedFolder = parentFolder.children.splice(index, 1)[0];
|
||||
|
||||
parentFolder.version += 1;
|
||||
const delFolderIds = getAllFolderIds(delFolder);
|
||||
const delFolderIds = getAllFolderIds(deletedFolder);
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
const folderVersion = new FolderVersion({
|
||||
@ -565,9 +525,9 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
||||
type: EventType.DELETE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId,
|
||||
folderName: delFolder.name,
|
||||
folderPath: secretPath
|
||||
folderId: deletedFolder.id,
|
||||
folderName: deletedFolder.name,
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -575,7 +535,7 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
|
||||
res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -677,69 +637,27 @@ export const getFolders = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
query: { workspaceId, environment, parentFolderId, parentFolderPath }
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetFoldersV1, req);
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
|
||||
if (req.user) await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
|
||||
if (!folders) {
|
||||
res.send({ folders: [], dir: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
// if instead of parentFolderId given a path like /folder1/folder2
|
||||
if (parentFolderPath) {
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
environment,
|
||||
parentFolderPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folder = getFolderByPath(folders.nodes, parentFolderPath);
|
||||
|
||||
if (!folder) {
|
||||
res.send({ folders: [], dir: [] });
|
||||
return;
|
||||
}
|
||||
// dir is not needed at present as this is only used in overview section of secrets
|
||||
res.send({
|
||||
folders: folder.children.map(({ id, name }) => ({ id, name })),
|
||||
dir: [{ name: folder.name, id: folder.id }]
|
||||
});
|
||||
}
|
||||
|
||||
if (!parentFolderId) {
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
|
||||
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
|
||||
id,
|
||||
name
|
||||
}));
|
||||
res.send({ folders: rootFolders });
|
||||
return;
|
||||
}
|
||||
|
||||
const { folder, folderPath, dir } = getFolderWithPathFromId(folders.nodes, parentFolderId);
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, folderPath);
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// check that user is a member of the workspace
|
||||
await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
}
|
||||
|
||||
res.send({
|
||||
folders: folder.children.map(({ id, name }) => ({ id, name })),
|
||||
dir
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
return res.send({ folders: [], dir: [] });
|
||||
}
|
||||
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
|
||||
return res.send({
|
||||
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
|
||||
});
|
||||
};
|
||||
|
@ -202,12 +202,12 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||
);
|
||||
|
||||
// delete workspace
|
||||
await deleteWork({
|
||||
id: workspaceId
|
||||
const workspace = await deleteWork({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully deleted workspace"
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Folder,
|
||||
Integration,
|
||||
Membership,
|
||||
Secret,
|
||||
@ -21,19 +22,22 @@ import {
|
||||
getUserProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { SecretImport } from "../../models";
|
||||
import { ServiceAccountWorkspacePermission } from "../../models";
|
||||
import { Webhook } from "../../models";
|
||||
|
||||
/**
|
||||
* Create new workspace environment named [environmentName]
|
||||
* Create new workspace environment named [environmentName]
|
||||
* with slug [environmentSlug] under workspace with id
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Create environment'
|
||||
#swagger.description = 'Create environment'
|
||||
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
@ -42,12 +46,12 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Create environment'
|
||||
#swagger.description = 'Create environment'
|
||||
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
@ -56,7 +60,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
@ -84,7 +88,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
@ -111,7 +115,7 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
},
|
||||
"description": "Response after creating a new environment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -242,7 +246,7 @@ export const reorderWorkspaceEnvironments = async (req: Request, res: Response)
|
||||
* @returns
|
||||
*/
|
||||
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Rename workspace environment'
|
||||
#swagger.description = 'Rename a specific environment within a workspace'
|
||||
|
||||
@ -313,7 +317,7 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
@ -369,13 +373,43 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceTokenData.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
{
|
||||
workspace: workspaceId,
|
||||
"scopes.environment": oldEnvironmentSlug
|
||||
},
|
||||
{ $set: { "scopes.$[element].environment": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
|
||||
);
|
||||
await Integration.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Folder.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await SecretImport.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await SecretImport.updateMany(
|
||||
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
|
||||
{ $set: { "imports.$[element].environment": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
|
||||
);
|
||||
|
||||
await ServiceAccountWorkspacePermission.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Webhook.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Membership.updateMany(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
@ -418,10 +452,10 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Delete workspace environment'
|
||||
#swagger.description = 'Delete a specific environment from a workspace'
|
||||
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
@ -454,7 +488,7 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
@ -472,9 +506,9 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
},
|
||||
"description": "Response after deleting an environment from a workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
@ -561,10 +595,10 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
|
||||
// TODO(akhilmhdh) after rbac this can be completely removed
|
||||
export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Get all accessible environments of a workspace'
|
||||
#swagger.description = 'Fetch all environments that the user has access to in a specified workspace'
|
||||
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
@ -611,7 +645,7 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId }
|
||||
|
@ -1,11 +1,25 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models";
|
||||
import {
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
ServiceAccount,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { Role } from "../../ee/models";
|
||||
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
|
||||
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
||||
import Role from "../../ee/models/role";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { CUSTOM } from "../../variables";
|
||||
import {
|
||||
createOrganization as create,
|
||||
deleteOrganization,
|
||||
updateSubscriptionOrgQuantity
|
||||
} from "../../helpers/organization";
|
||||
import { addMembershipsOrg } from "../../helpers/membershipOrg";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import {
|
||||
ACCEPTED,
|
||||
ADMIN,
|
||||
CUSTOM
|
||||
} from "../../variables";
|
||||
import * as reqValidator from "../../validation/organization";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import {
|
||||
@ -332,3 +346,60 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response
|
||||
serviceAccounts
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new organization named [organizationName]
|
||||
* and add user as owner
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { name }
|
||||
} = await validateRequest(reqValidator.CreateOrgv2, req);
|
||||
|
||||
// create organization and add user as member
|
||||
const organization = await create({
|
||||
email: req.user.email,
|
||||
name
|
||||
});
|
||||
|
||||
await addMembershipsOrg({
|
||||
userIds: [req.user._id.toString()],
|
||||
organizationId: organization._id.toString(),
|
||||
roles: [ADMIN],
|
||||
statuses: [ACCEPTED]
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteOrganizationById = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.DeleteOrgv2, req);
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
role: ADMIN
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw UnauthorizedRequestError();
|
||||
|
||||
const organization = await deleteOrganization({
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
}
|
@ -5,49 +5,9 @@ import bcrypt from "bcrypt";
|
||||
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { deleteUser } from "../../helpers/user";
|
||||
import * as reqValidator from "../../validation";
|
||||
|
||||
/**
|
||||
* Return the current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMe = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = "Retrieve the current user on the request"
|
||||
#swagger.description = "Retrieve the current user on the request"
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/CurrentUser",
|
||||
"description": "Current user on request"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const user = await User.findById(req.user._id).select(
|
||||
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the current user's MFA-enabled status [isMfaEnabled].
|
||||
* Note: Infisical currently only supports email-based 2FA only; this will expand to
|
||||
@ -296,3 +256,59 @@ export const deleteMySessions = async (req: Request, res: Response) => {
|
||||
message: "Successfully revoked all sessions"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMe = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = "Retrieve the current user on the request"
|
||||
#swagger.description = "Retrieve the current user on the request"
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/CurrentUser",
|
||||
"description": "Current user on request"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const user = await User.findById(req.user._id).select(
|
||||
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the current user.
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteMe = async (req: Request, res: Response) => {
|
||||
const user = await deleteUser({
|
||||
userId: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
}
|
@ -7,5 +7,5 @@ export {
|
||||
authController,
|
||||
secretsController,
|
||||
signupController,
|
||||
workspacesController,
|
||||
workspacesController
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ import { Types } from "mongoose";
|
||||
import { EventService, SecretService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { BotService } from "../../services";
|
||||
import { containsGlobPatterns, isValidScope, repackageSecretToRaw } from "../../helpers/secrets";
|
||||
import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
import { Folder, IServiceTokenData } from "../../models";
|
||||
import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models";
|
||||
import { Permission } from "../../models/serviceTokenDataV3";
|
||||
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
@ -17,8 +18,115 @@ import {
|
||||
getUserProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { validateServiceTokenDataClientForWorkspace } from "../../validation";
|
||||
import {
|
||||
validateServiceTokenDataClientForWorkspace,
|
||||
validateServiceTokenDataV3ClientForWorkspace
|
||||
} from "../../validation";
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
|
||||
import { ActorType } from "../../ee/models";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { AuthData } from "../../interfaces/middleware";
|
||||
import {
|
||||
generateSecretApprovalRequest,
|
||||
getSecretPolicyOfBoard
|
||||
} from "../../ee/services/SecretApprovalService";
|
||||
import { CommitType } from "../../ee/models/secretApprovalRequest";
|
||||
import { IRole } from "../../ee/models/role";
|
||||
|
||||
const checkSecretsPermission = async ({
|
||||
authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction
|
||||
}: {
|
||||
authData: AuthData;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretAction: ProjectPermissionActions; // CRUD
|
||||
}): Promise<{
|
||||
authVerifier: (env: string, secPath: string) => boolean;
|
||||
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
|
||||
}> => {
|
||||
let STV2RequiredPermissions = [];
|
||||
let STV3RequiredPermissions: Permission[] = [];
|
||||
|
||||
switch (secretAction) {
|
||||
case ProjectPermissionActions.Create:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
case ProjectPermissionActions.Read:
|
||||
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.READ];
|
||||
break;
|
||||
case ProjectPermissionActions.Edit:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
case ProjectPermissionActions.Delete:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
}
|
||||
|
||||
switch (authData.actor.type) {
|
||||
case ActorType.USER: {
|
||||
const { permission, membership } = await getUserProjectPermissions(
|
||||
authData.actor.metadata.userId,
|
||||
workspaceId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
secretAction,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
return {
|
||||
authVerifier: (env: string, secPath: string) =>
|
||||
permission.can(
|
||||
secretAction,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env,
|
||||
secretPath: secPath
|
||||
})
|
||||
),
|
||||
membership
|
||||
};
|
||||
}
|
||||
case ActorType.SERVICE: {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: STV2RequiredPermissions
|
||||
});
|
||||
return { authVerifier: () => true };
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData,
|
||||
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: STV3RequiredPermissions
|
||||
});
|
||||
return {
|
||||
authVerifier: (env: string, secPath: string) =>
|
||||
isValidScopeV3({
|
||||
authPayload: authData.authPayload as IServiceTokenDataV3,
|
||||
environment: env,
|
||||
secretPath: secPath,
|
||||
requiredPermissions: STV3RequiredPermissions
|
||||
})
|
||||
};
|
||||
}
|
||||
default: {
|
||||
throw UnauthorizedRequestError();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return secrets for workspace with id [workspaceId] and environment
|
||||
@ -58,32 +166,13 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
if (!environment || !workspaceId)
|
||||
throw BadRequestError({ message: "Missing environment or workspace id" });
|
||||
|
||||
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
permissionCheckFn = (env: string, secPath: string) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env,
|
||||
secretPath: secPath
|
||||
})
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
});
|
||||
permissionCheckFn = (env: string, secPath: string) =>
|
||||
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
|
||||
}
|
||||
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Read
|
||||
});
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
@ -150,21 +239,13 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
|
||||
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Read
|
||||
});
|
||||
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
@ -196,24 +277,24 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { secretName },
|
||||
body: { secretPath, environment, workspaceId, type, secretValue, secretComment }
|
||||
body: {
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId,
|
||||
type,
|
||||
secretValue,
|
||||
secretComment,
|
||||
skipMultilineEncoding
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateSecretRawV3, req);
|
||||
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Create
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
@ -249,7 +330,8 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
secretPath,
|
||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||
secretCommentIV: secretCommentEncrypted.iv,
|
||||
secretCommentTag: secretCommentEncrypted.tag
|
||||
secretCommentTag: secretCommentEncrypted.tag,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
@ -279,24 +361,16 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { secretName },
|
||||
body: { secretValue, environment, secretPath, type, workspaceId }
|
||||
body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
|
||||
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
|
||||
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Edit
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
@ -316,7 +390,8 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
secretPath
|
||||
secretPath,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
@ -346,21 +421,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
body: { environment, secretPath, type, workspaceId }
|
||||
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
|
||||
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Delete
|
||||
});
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
@ -409,37 +476,18 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
if (folderId && folderId !== "root") {
|
||||
const folder = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
if (!folder) return res.send({ secrets: [] });
|
||||
|
||||
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
|
||||
}
|
||||
|
||||
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
permissionCheckFn = (env: string, secPath: string) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: env,
|
||||
secretPath: secPath
|
||||
})
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
});
|
||||
permissionCheckFn = (env: string, secPath: string) =>
|
||||
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
|
||||
}
|
||||
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Read
|
||||
});
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
@ -488,21 +536,13 @@ export const getSecretByName = async (req: Request, res: Response) => {
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.GetSecretByNameV3, req);
|
||||
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
});
|
||||
}
|
||||
await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Read
|
||||
});
|
||||
|
||||
const secret = await SecretService.getSecret({
|
||||
secretName,
|
||||
@ -540,25 +580,50 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding
|
||||
},
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.CreateSecretV3, req);
|
||||
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
const { membership } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Create
|
||||
});
|
||||
|
||||
if (membership && type !== "personal") {
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
if (secretApprovalPolicy) {
|
||||
const secretApprovalRequest = await generateSecretApprovalRequest({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
policy: secretApprovalPolicy,
|
||||
commiterMembershipId: membership._id.toString(),
|
||||
authData: req.authData,
|
||||
data: {
|
||||
[CommitType.CREATE]: [
|
||||
{
|
||||
secretName,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
return res.send({ approval: secretApprovalRequest });
|
||||
}
|
||||
}
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
@ -577,7 +642,8 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
metadata
|
||||
metadata,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
@ -607,28 +673,68 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext,
|
||||
secretValueTag,
|
||||
secretValueIV,
|
||||
secretId,
|
||||
type,
|
||||
environment,
|
||||
secretPath,
|
||||
workspaceId
|
||||
workspaceId,
|
||||
tags,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
secretName: newSecretName,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
skipMultilineEncoding
|
||||
},
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
|
||||
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext)) {
|
||||
throw BadRequestError({ message: "Missing encrypted key" });
|
||||
}
|
||||
|
||||
const { membership } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Edit
|
||||
});
|
||||
|
||||
if (membership && type !== "personal") {
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
if (secretApprovalPolicy) {
|
||||
const secretApprovalRequest = await generateSecretApprovalRequest({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
policy: secretApprovalPolicy,
|
||||
commiterMembershipId: membership._id.toString(),
|
||||
authData: req.authData,
|
||||
data: {
|
||||
[CommitType.UPDATE]: [
|
||||
{
|
||||
secretName,
|
||||
newSecretName,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
tags,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
return res.send({ approval: secretApprovalRequest });
|
||||
}
|
||||
}
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
@ -636,11 +742,21 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
secretId,
|
||||
authData: req.authData,
|
||||
newSecretName,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath
|
||||
secretPath,
|
||||
tags,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
@ -663,28 +779,43 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { type, environment, secretPath, workspaceId },
|
||||
body: { type, environment, secretPath, workspaceId, secretId },
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
|
||||
|
||||
if (req.user?._id) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
const { membership } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Delete
|
||||
});
|
||||
|
||||
if (membership && type !== "personal") {
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
if (secretApprovalPolicy) {
|
||||
const secretApprovalRequest = await generateSecretApprovalRequest({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
policy: secretApprovalPolicy,
|
||||
commiterMembershipId: membership._id.toString(),
|
||||
data: {
|
||||
[CommitType.DELETE]: [
|
||||
{
|
||||
secretName
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
return res.send({ approval: secretApprovalRequest });
|
||||
}
|
||||
}
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
secretId,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
@ -704,3 +835,135 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
||||
export const createSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
|
||||
|
||||
const { membership } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Create
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
if (secretApprovalPolicy) {
|
||||
const secretApprovalRequest = await generateSecretApprovalRequest({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
policy: secretApprovalPolicy,
|
||||
commiterMembershipId: membership._id.toString(),
|
||||
data: {
|
||||
[CommitType.CREATE]: secrets.filter(({ type }) => type === "shared")
|
||||
}
|
||||
});
|
||||
return res.send({ approval: secretApprovalRequest });
|
||||
}
|
||||
}
|
||||
|
||||
const createdSecrets = await SecretService.createSecretBatch({
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secrets,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: createdSecrets
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req);
|
||||
|
||||
const { membership } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Edit
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
if (secretApprovalPolicy) {
|
||||
const secretApprovalRequest = await generateSecretApprovalRequest({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
policy: secretApprovalPolicy,
|
||||
commiterMembershipId: membership._id.toString(),
|
||||
data: {
|
||||
[CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared")
|
||||
},
|
||||
authData: req.authData
|
||||
});
|
||||
return res.send({ approval: secretApprovalRequest });
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSecrets = await SecretService.updateSecretBatch({
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secrets,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: updatedSecrets
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req);
|
||||
|
||||
const { membership } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretAction: ProjectPermissionActions.Delete
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
if (secretApprovalPolicy) {
|
||||
const secretApprovalRequest = await generateSecretApprovalRequest({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
policy: secretApprovalPolicy,
|
||||
commiterMembershipId: membership._id.toString(),
|
||||
data: {
|
||||
[CommitType.DELETE]: secrets.filter(({ type }) => type === "shared")
|
||||
},
|
||||
authData: req.authData
|
||||
});
|
||||
return res.send({ approval: secretApprovalRequest });
|
||||
}
|
||||
}
|
||||
|
||||
const deletedSecrets = await SecretService.deleteSecretBatch({
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secrets,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: deletedSecrets
|
||||
});
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { Secret } from "../../models";
|
||||
import { Secret, ServiceTokenDataV3 } from "../../models";
|
||||
import { SecretService } from "../../services";
|
||||
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
@ -101,3 +101,17 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
message: "Successfully named workspace secrets"
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceServiceTokenDataV3, req);
|
||||
|
||||
const serviceTokenData = await ServiceTokenDataV3.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
@ -8,6 +8,8 @@ import * as actionController from "./actionController";
|
||||
import * as membershipController from "./membershipController";
|
||||
import * as cloudProductsController from "./cloudProductsController";
|
||||
import * as roleController from "./roleController";
|
||||
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
|
||||
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
|
||||
|
||||
export {
|
||||
secretController,
|
||||
@ -19,5 +21,7 @@ export {
|
||||
actionController,
|
||||
membershipController,
|
||||
cloudProductsController,
|
||||
roleController
|
||||
roleController,
|
||||
secretApprovalPolicyController,
|
||||
secretApprovalRequestController
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
memberPermissions
|
||||
} from "../../services/RoleService";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import Role from "../../models/role";
|
||||
import { Role } from "../../models";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
|
||||
@ -212,6 +212,7 @@ export const getUserPermissions = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { orgId }
|
||||
} = await validateRequest(GetUserPermission, req);
|
||||
|
||||
const { permission } = await getUserOrgPermissions(req.user._id, orgId);
|
||||
|
||||
res.status(200).json({
|
||||
|
128
backend/src/ee/controllers/v1/secretApprovalPolicyController.ts
Normal file
128
backend/src/ee/controllers/v1/secretApprovalPolicyController.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getUserProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
||||
import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import * as reqValidator from "../../validation/secretApproval";
|
||||
|
||||
const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" });
|
||||
|
||||
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { approvals, secretPath, approvers, environment, workspaceId, name }
|
||||
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const secretApproval = new SecretApprovalPolicy({
|
||||
workspace: workspaceId,
|
||||
name: name ?? `${environment}-${nanoid(3)}`,
|
||||
secretPath,
|
||||
environment,
|
||||
approvals,
|
||||
approvers
|
||||
});
|
||||
await secretApproval.save();
|
||||
|
||||
return res.send({
|
||||
approval: secretApproval
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { approvals, approvers, secretPath, name },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
|
||||
|
||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
||||
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
secretApproval.workspace.toString()
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
|
||||
approvals,
|
||||
approvers,
|
||||
name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`,
|
||||
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
|
||||
});
|
||||
|
||||
return res.send({
|
||||
approval: updatedDoc
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.DeleteSecretApprovalRule, req);
|
||||
|
||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
||||
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
secretApproval.workspace.toString()
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const deletedDoc = await SecretApprovalPolicy.findByIdAndDelete(id);
|
||||
|
||||
return res.send({
|
||||
approval: deletedDoc
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetSecretApprovalRuleList, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const doc = await SecretApprovalPolicy.find({ workspace: workspaceId });
|
||||
|
||||
return res.send({
|
||||
approvals: doc
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId, environment, secretPath }
|
||||
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
|
||||
);
|
||||
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
return res.send({ policy: secretApprovalPolicy });
|
||||
};
|
@ -0,0 +1,333 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getUserProjectPermissions } from "../../services/ProjectRoleService";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { Folder } from "../../../models";
|
||||
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
|
||||
import * as reqValidator from "../../validation/secretApprovalRequest";
|
||||
import { getFolderWithPathFromId } from "../../../services/FolderService";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
||||
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
|
||||
import { Types } from "mongoose";
|
||||
import { EEAuditLogService } from "../../services";
|
||||
import { EventType } from "../../models";
|
||||
|
||||
export const getSecretApprovalRequestCount = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
|
||||
|
||||
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
const approvalRequestCount = await SecretApprovalRequest.aggregate([
|
||||
{
|
||||
$match: {
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: SecretApprovalPolicy.collection.name,
|
||||
localField: "policy",
|
||||
foreignField: "_id",
|
||||
as: "policy"
|
||||
}
|
||||
},
|
||||
{ $unwind: "$policy" },
|
||||
...(membership.role !== "admin"
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ committer: new Types.ObjectId(membership.id) },
|
||||
{ "policy.approvers": new Types.ObjectId(membership.id) }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
$group: {
|
||||
_id: "$status",
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
]);
|
||||
const openRequests = approvalRequestCount.find(({ _id }) => _id === "open");
|
||||
const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close");
|
||||
|
||||
return res.send({
|
||||
approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 }
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { status, committer, workspaceId, environment, limit, offset }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
|
||||
|
||||
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
|
||||
const query = {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
committer: committer ? new Types.ObjectId(committer) : undefined,
|
||||
status
|
||||
};
|
||||
// to strip of undefined in query we use es6 spread to ignore those fields
|
||||
Object.entries(query).forEach(
|
||||
([key, value]) => value === undefined && delete query[key as keyof typeof query]
|
||||
);
|
||||
const approvalRequests = await SecretApprovalRequest.aggregate([
|
||||
{
|
||||
$match: query
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{
|
||||
$lookup: {
|
||||
from: SecretApprovalPolicy.collection.name,
|
||||
localField: "policy",
|
||||
foreignField: "_id",
|
||||
as: "policy"
|
||||
}
|
||||
},
|
||||
{ $unwind: "$policy" },
|
||||
...(membership.role !== "admin"
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ committer: new Types.ObjectId(membership.id) },
|
||||
{ "policy.approvers": new Types.ObjectId(membership.id) }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{ $skip: offset },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
if (!approvalRequests.length) return res.send({ approvals: [] });
|
||||
|
||||
const unqiueEnvs = environment ?? {
|
||||
$in: [...new Set(approvalRequests.map(({ environment }) => environment))]
|
||||
};
|
||||
const approvalRootFolders = await Folder.find({
|
||||
workspace: workspaceId,
|
||||
environment: unqiueEnvs
|
||||
}).lean();
|
||||
|
||||
const formatedApprovals = approvalRequests.map((el) => {
|
||||
let secretPath = "/";
|
||||
const folders = approvalRootFolders.find(({ environment }) => environment === el.environment);
|
||||
if (folders) {
|
||||
secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/";
|
||||
}
|
||||
return { ...el, secretPath };
|
||||
});
|
||||
|
||||
return res.send({
|
||||
approvals: formatedApprovals
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
|
||||
.populate<{ policy: ISecretApprovalPolicy }>("policy")
|
||||
.populate({
|
||||
path: "commits.secretVersion",
|
||||
populate: {
|
||||
path: "tags"
|
||||
}
|
||||
})
|
||||
.populate("commits.secret", "version")
|
||||
.populate("commits.newVersion.tags")
|
||||
.lean();
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { membership } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
secretApprovalRequest.workspace.toString()
|
||||
);
|
||||
// allow to fetch only if its admin or is the committer or approver
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find(
|
||||
(approverId) => approverId.toString() === membership._id.toString()
|
||||
)
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
let secretPath = "/";
|
||||
const approvalRootFolders = await Folder.findOne({
|
||||
workspace: secretApprovalRequest.workspace,
|
||||
environment: secretApprovalRequest.environment
|
||||
}).lean();
|
||||
if (approvalRootFolders) {
|
||||
secretPath =
|
||||
getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId)
|
||||
?.folderPath || "/";
|
||||
}
|
||||
|
||||
return res.send({
|
||||
approval: { ...secretApprovalRequest, secretPath }
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { status },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req);
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { membership } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
secretApprovalRequest.workspace.toString()
|
||||
);
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
const reviewerPos = secretApprovalRequest.reviewers.findIndex(
|
||||
({ member }) => member.toString() === membership._id.toString()
|
||||
);
|
||||
if (reviewerPos !== -1) {
|
||||
secretApprovalRequest.reviewers[reviewerPos].status = status;
|
||||
} else {
|
||||
secretApprovalRequest.reviewers.push({ member: membership._id, status });
|
||||
}
|
||||
await secretApprovalRequest.save();
|
||||
|
||||
return res.send({ status });
|
||||
};
|
||||
|
||||
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
|
||||
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { membership } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
secretApprovalRequest.workspace.toString()
|
||||
);
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
||||
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
|
||||
{}
|
||||
);
|
||||
const hasMinApproval =
|
||||
secretApprovalRequest.policy.approvals <=
|
||||
secretApprovalRequest.policy.approvers.filter(
|
||||
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||
).length;
|
||||
|
||||
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
|
||||
const approval = await performSecretApprovalRequestMerge(
|
||||
id,
|
||||
req.authData,
|
||||
membership._id.toString()
|
||||
);
|
||||
return res.send({ approval });
|
||||
};
|
||||
|
||||
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { status },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req);
|
||||
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { membership } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
secretApprovalRequest.workspace.toString()
|
||||
);
|
||||
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
if (secretApprovalRequest.hasMerged)
|
||||
throw BadRequestError({ message: "Approval request has been merged" });
|
||||
if (secretApprovalRequest.status === "close" && status === "close")
|
||||
throw BadRequestError({ message: "Approval request is already closed" });
|
||||
if (secretApprovalRequest.status === "open" && status === "open")
|
||||
throw BadRequestError({ message: "Approval request is already open" });
|
||||
|
||||
const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate(
|
||||
id,
|
||||
{ status, statusChangeBy: membership._id },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (status === "close") {
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.SECRET_APPROVAL_CLOSED,
|
||||
metadata: {
|
||||
closedBy: membership._id.toString(),
|
||||
secretApprovalRequestId: id,
|
||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: secretApprovalRequest.workspace
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.SECRET_APPROVAL_REOPENED,
|
||||
metadata: {
|
||||
reopenedBy: membership._id.toString(),
|
||||
secretApprovalRequestId: id,
|
||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: secretApprovalRequest.workspace
|
||||
}
|
||||
);
|
||||
}
|
||||
return res.send({ approval: updatedRequest });
|
||||
};
|
@ -5,6 +5,7 @@ import {
|
||||
Membership,
|
||||
Secret,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
TFolderSchema,
|
||||
User,
|
||||
Workspace
|
||||
@ -20,6 +21,7 @@ import {
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
ServiceActor,
|
||||
ServiceActorV3,
|
||||
TFolderRootVersionSchema,
|
||||
TrustedIP,
|
||||
UserActor
|
||||
@ -27,7 +29,7 @@ import {
|
||||
import { EESecretService } from "../../services";
|
||||
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
|
||||
// import Folder, { TFolderSchema } from "../../../models/folder";
|
||||
import { searchByFolderId } from "../../../services/FolderService";
|
||||
import { getFolderByPath, searchByFolderId } from "../../../services/FolderService";
|
||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
@ -104,7 +106,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
query: { environment, folderId, offset, limit }
|
||||
query: { environment, directory, offset, limit }
|
||||
} = await validateRequest(GetWorkspaceSecretSnapshotsV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
@ -113,10 +115,20 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders?.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const secretSnapshots = await SecretSnapshot.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId: folderId || "root"
|
||||
folderId
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
@ -135,7 +147,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
|
||||
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
query: { environment, folderId }
|
||||
query: { environment, directory }
|
||||
} = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
@ -144,10 +156,20 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders?.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const count = await SecretSnapshot.countDocuments({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId: folderId || "root"
|
||||
folderId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
@ -215,7 +237,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
||||
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { folderId, environment, version }
|
||||
body: { directory, environment, version }
|
||||
} = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
@ -224,6 +246,16 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders?.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// validate secret snapshot
|
||||
const secretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId,
|
||||
@ -653,7 +685,7 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.AuditLogs
|
||||
);
|
||||
|
||||
|
||||
const query = {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
...(eventType
|
||||
@ -668,13 +700,13 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
|
||||
: {}),
|
||||
...(actor
|
||||
? {
|
||||
"actor.type": actor.split("-", 2)[0],
|
||||
"actor.type": actor.substring(0, actor.lastIndexOf("-")),
|
||||
...(actor.split("-", 2)[0] === ActorType.USER
|
||||
? {
|
||||
"actor.metadata.userId": actor.split("-", 2)[1]
|
||||
"actor.metadata.userId": actor.substring(actor.lastIndexOf("-") + 1)
|
||||
}
|
||||
: {
|
||||
"actor.metadata.serviceId": actor.split("-", 2)[1]
|
||||
"actor.metadata.serviceId": actor.substring(actor.lastIndexOf("-") + 1)
|
||||
})
|
||||
}
|
||||
: {}),
|
||||
@ -742,9 +774,27 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
|
||||
name: serviceTokenData.name
|
||||
}
|
||||
}));
|
||||
|
||||
const serviceV3Actors: ServiceActorV3[] = (
|
||||
await ServiceTokenDataV3.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
})
|
||||
).map((serviceTokenData) => ({
|
||||
type: ActorType.SERVICE_V3,
|
||||
metadata: {
|
||||
serviceId: serviceTokenData._id.toString(),
|
||||
name: serviceTokenData.name
|
||||
}
|
||||
}));
|
||||
|
||||
const actors = [
|
||||
...userActors,
|
||||
...serviceActors,
|
||||
...serviceV3Actors
|
||||
];
|
||||
|
||||
return res.status(200).send({
|
||||
actors: [...userActors, ...serviceActors]
|
||||
actors
|
||||
});
|
||||
};
|
||||
|
||||
|
5
backend/src/ee/controllers/v3/index.ts
Normal file
5
backend/src/ee/controllers/v3/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import * as serviceTokenDataController from "./serviceTokenDataController";
|
||||
|
||||
export {
|
||||
serviceTokenDataController
|
||||
}
|
325
backend/src/ee/controllers/v3/serviceTokenDataController.ts
Normal file
325
backend/src/ee/controllers/v3/serviceTokenDataController.ts
Normal file
@ -0,0 +1,325 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IServiceTokenDataV3,
|
||||
IUser,
|
||||
ServiceTokenDataV3,
|
||||
ServiceTokenDataV3Key,
|
||||
Workspace
|
||||
} from "../../../models";
|
||||
import {
|
||||
IServiceTokenV3Scope,
|
||||
IServiceTokenV3TrustedIp
|
||||
} from "../../../models/serviceTokenDataV3";
|
||||
import {
|
||||
ActorType,
|
||||
EventType
|
||||
} from "../../models";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/serviceTokenDataV3";
|
||||
import { createToken } from "../../../helpers/auth";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getUserProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ResourceNotFoundError } from "../../../utils/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||
import { getJwtServiceTokenSecret } from "../../../config";
|
||||
|
||||
/**
|
||||
* Return project key for service token
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getServiceTokenDataKey = async (req: Request, res: Response) => {
|
||||
const key = await ServiceTokenDataV3Key.findOne({
|
||||
serviceTokenData: (req.authData.authPayload as IServiceTokenDataV3)._id
|
||||
}).populate<{ sender: IUser }>("sender", "publicKey");
|
||||
|
||||
if (!key) throw ResourceNotFoundError({
|
||||
message: "Failed to find project key for service token"
|
||||
});
|
||||
|
||||
const { _id, workspace, encryptedKey, nonce, sender: { publicKey } } = key;
|
||||
|
||||
return res.status(200).send({
|
||||
key: {
|
||||
_id,
|
||||
workspace,
|
||||
encryptedKey,
|
||||
publicKey,
|
||||
nonce
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create service token data
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
name,
|
||||
workspaceId,
|
||||
publicKey,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
encryptedKey, // for ServiceTokenDataV3Key
|
||||
nonce // for ServiceTokenDataV3Key
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
|
||||
// validate trusted ips
|
||||
const reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
||||
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
|
||||
message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
|
||||
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
|
||||
|
||||
if (!isValidIPOrCidr) return res.status(400).send({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
|
||||
return extractIPDetails(trustedIp.ipAddress);
|
||||
});
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
let user;
|
||||
if (req.authData.actor.type === ActorType.USER) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
const isActive = true;
|
||||
const serviceTokenData = await new ServiceTokenDataV3({
|
||||
name,
|
||||
user,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
publicKey,
|
||||
usageCount: 0,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
scopes,
|
||||
isActive,
|
||||
expiresAt
|
||||
}).save();
|
||||
|
||||
await new ServiceTokenDataV3Key({
|
||||
encryptedKey,
|
||||
nonce,
|
||||
sender: req.user._id,
|
||||
serviceTokenData: serviceTokenData._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).save();
|
||||
|
||||
const token = createToken({
|
||||
payload: {
|
||||
_id: serviceTokenData._id.toString()
|
||||
},
|
||||
expiresIn,
|
||||
secret: await getJwtServiceTokenSecret()
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3,
|
||||
metadata: {
|
||||
name,
|
||||
isActive,
|
||||
scopes: scopes as Array<IServiceTokenV3Scope>,
|
||||
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData,
|
||||
serviceToken: `stv3.${token}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update service token data with id [serviceTokenDataId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { serviceTokenDataId },
|
||||
body: {
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn
|
||||
}
|
||||
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
|
||||
|
||||
let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId);
|
||||
if (!serviceTokenData) throw ResourceNotFoundError({
|
||||
message: "Service token not found"
|
||||
});
|
||||
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
serviceTokenData.workspace.toString()
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findById(serviceTokenData.workspace);
|
||||
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
|
||||
// validate trusted ips
|
||||
let reformattedTrustedIps;
|
||||
if (trustedIps) {
|
||||
reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
||||
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
|
||||
message: "Failed to update IP access range to service token due to plan restriction. Upgrade plan to update IP access range."
|
||||
});
|
||||
|
||||
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
|
||||
|
||||
if (!isValidIPOrCidr) return res.status(400).send({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
|
||||
return extractIPDetails(trustedIp.ipAddress);
|
||||
});
|
||||
}
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
|
||||
serviceTokenDataId,
|
||||
{
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
expiresAt
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!serviceTokenData) throw BadRequestError({
|
||||
message: "Failed to update service token"
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_SERVICE_TOKEN_V3,
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
isActive,
|
||||
scopes: scopes as Array<IServiceTokenV3Scope>,
|
||||
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete service token data with id [serviceTokenDataId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { serviceTokenDataId }
|
||||
} = await validateRequest(reqValidator.DeleteServiceTokenV3, req);
|
||||
|
||||
let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId);
|
||||
if (!serviceTokenData) throw ResourceNotFoundError({
|
||||
message: "Service token not found"
|
||||
});
|
||||
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
serviceTokenData.workspace.toString()
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId);
|
||||
|
||||
if (!serviceTokenData) throw BadRequestError({
|
||||
message: "Failed to delete service token"
|
||||
});
|
||||
|
||||
await ServiceTokenDataV3Key.findOneAndDelete({
|
||||
serviceTokenData: serviceTokenData._id
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_SERVICE_TOKEN_V3,
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
isActive: serviceTokenData.isActive,
|
||||
scopes: serviceTokenData.scopes as Array<IServiceTokenV3Scope>,
|
||||
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt: serviceTokenData.expiresAt
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
@ -1,47 +1,58 @@
|
||||
export enum ActorType {
|
||||
USER = "user",
|
||||
SERVICE = "service"
|
||||
SERVICE = "service",
|
||||
SERVICE_V3 = "service-v3"
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
WEB = "web",
|
||||
CLI = "cli",
|
||||
K8_OPERATOR = "k8-operator",
|
||||
OTHER = "other"
|
||||
WEB = "web",
|
||||
CLI = "cli",
|
||||
K8_OPERATOR = "k8-operator",
|
||||
OTHER = "other"
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
GET_SECRETS = "get-secrets",
|
||||
GET_SECRET = "get-secret",
|
||||
REVEAL_SECRET = "reveal-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||
CREATE_INTEGRATION = "create-integration",
|
||||
DELETE_INTEGRATION = "delete-integration",
|
||||
ADD_TRUSTED_IP = "add-trusted-ip",
|
||||
UPDATE_TRUSTED_IP = "update-trusted-ip",
|
||||
DELETE_TRUSTED_IP = "delete-trusted-ip",
|
||||
CREATE_SERVICE_TOKEN = "create-service-token",
|
||||
DELETE_SERVICE_TOKEN = "delete-service-token",
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
CREATE_FOLDER = "create-folder",
|
||||
UPDATE_FOLDER = "update-folder",
|
||||
DELETE_FOLDER = "delete-folder",
|
||||
CREATE_WEBHOOK = "create-webhook",
|
||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||
DELETE_WEBHOOK = "delete-webhook",
|
||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
|
||||
}
|
||||
GET_SECRETS = "get-secrets",
|
||||
GET_SECRET = "get-secret",
|
||||
REVEAL_SECRET = "reveal-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
CREATE_SECRETS = "create-secrets",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
UPDATE_SECRETS = "update-secrets",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||
CREATE_INTEGRATION = "create-integration",
|
||||
DELETE_INTEGRATION = "delete-integration",
|
||||
ADD_TRUSTED_IP = "add-trusted-ip",
|
||||
UPDATE_TRUSTED_IP = "update-trusted-ip",
|
||||
DELETE_TRUSTED_IP = "delete-trusted-ip",
|
||||
CREATE_SERVICE_TOKEN = "create-service-token", // v2
|
||||
DELETE_SERVICE_TOKEN = "delete-service-token", // v2
|
||||
CREATE_SERVICE_TOKEN_V3 = "create-service-token-v3", // v3
|
||||
UPDATE_SERVICE_TOKEN_V3 = "update-service-token-v3", // v3
|
||||
DELETE_SERVICE_TOKEN_V3 = "delete-service-token-v3", // v3
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
CREATE_FOLDER = "create-folder",
|
||||
UPDATE_FOLDER = "update-folder",
|
||||
DELETE_FOLDER = "delete-folder",
|
||||
CREATE_WEBHOOK = "create-webhook",
|
||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||
DELETE_WEBHOOK = "delete-webhook",
|
||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
|
||||
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
|
||||
}
|
||||
|
@ -2,358 +2,428 @@ import {
|
||||
ActorType,
|
||||
EventType
|
||||
} from "./enums";
|
||||
import {
|
||||
IServiceTokenV3Scope,
|
||||
IServiceTokenV3TrustedIp
|
||||
} from "../../../models/serviceTokenDataV3";
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
email: string;
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ServiceActorMetadata {
|
||||
serviceId: string;
|
||||
name: string;
|
||||
serviceId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
}
|
||||
|
||||
export interface ServiceActor {
|
||||
type: ActorType.SERVICE;
|
||||
type: ActorType.SERVICE;
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export interface ServiceActorV3 {
|
||||
type: ActorType.SERVICE_V3;
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor =
|
||||
| UserActor
|
||||
| ServiceActor;
|
||||
| ServiceActor
|
||||
| ServiceActorV3;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
numberOfSecrets: number;
|
||||
};
|
||||
type: EventType.GET_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
numberOfSecrets: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretEvent {
|
||||
type: EventType.GET_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
type: EventType.GET_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretEvent {
|
||||
type: EventType.CREATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
type: EventType.CREATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretBatchEvent {
|
||||
type: EventType.CREATE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretEvent {
|
||||
type: EventType.UPDATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
type: EventType.UPDATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretBatchEvent {
|
||||
type: EventType.UPDATE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretEvent {
|
||||
type: EventType.DELETE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
type: EventType.DELETE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretBatchEvent {
|
||||
type: EventType.DELETE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetWorkspaceKeyEvent {
|
||||
type: EventType.GET_WORKSPACE_KEY,
|
||||
metadata: {
|
||||
keyId: string;
|
||||
}
|
||||
type: EventType.GET_WORKSPACE_KEY;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AuthorizeIntegrationEvent {
|
||||
type: EventType.AUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
}
|
||||
type: EventType.AUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UnauthorizeIntegrationEvent {
|
||||
type: EventType.UNAUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
}
|
||||
type: EventType.UNAUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateIntegrationEvent {
|
||||
type: EventType.CREATE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
}
|
||||
type: EventType.CREATE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIntegrationEvent {
|
||||
type: EventType.DELETE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
}
|
||||
type: EventType.DELETE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddTrustedIPEvent {
|
||||
type: EventType.ADD_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
type: EventType.ADD_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateTrustedIPEvent {
|
||||
type: EventType.UPDATE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
type: EventType.UPDATE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteTrustedIPEvent {
|
||||
type: EventType.DELETE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
type: EventType.DELETE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateServiceTokenEvent {
|
||||
type: EventType.CREATE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
}
|
||||
type: EventType.CREATE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteServiceTokenEvent {
|
||||
type: EventType.DELETE_SERVICE_TOKEN;
|
||||
type: EventType.DELETE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateServiceTokenV3Event {
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateServiceTokenV3Event {
|
||||
type: EventType.UPDATE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
scopes?: Array<IServiceTokenV3Scope>;
|
||||
trustedIps?: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteServiceTokenV3Event {
|
||||
type: EventType.DELETE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
expiresAt?: Date;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateEnvironmentEvent {
|
||||
type: EventType.UPDATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
oldName: string;
|
||||
newName: string;
|
||||
oldSlug: string;
|
||||
newSlug: string;
|
||||
}
|
||||
type: EventType.UPDATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
oldName: string;
|
||||
newName: string;
|
||||
oldSlug: string;
|
||||
newSlug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteEnvironmentEvent {
|
||||
type: EventType.DELETE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
type: EventType.DELETE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddWorkspaceMemberEvent {
|
||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RemoveWorkspaceMemberEvent {
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateFolderEvent {
|
||||
type: EventType.CREATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
type: EventType.CREATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateFolderEvent {
|
||||
type: EventType.UPDATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
oldFolderName: string;
|
||||
newFolderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
type: EventType.UPDATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
oldFolderName: string;
|
||||
newFolderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteFolderEvent {
|
||||
type: EventType.DELETE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
type: EventType.DELETE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateWebhookEvent {
|
||||
type: EventType.CREATE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
type: EventType.CREATE_WEBHOOK;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateWebhookStatusEvent {
|
||||
type: EventType.UPDATE_WEBHOOK_STATUS,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
type: EventType.UPDATE_WEBHOOK_STATUS;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteWebhookEvent {
|
||||
type: EventType.DELETE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
type: EventType.DELETE_WEBHOOK;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretImportsEvent {
|
||||
type: EventType.GET_SECRET_IMPORTS,
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
numberOfImports: number;
|
||||
}
|
||||
type: EventType.GET_SECRET_IMPORTS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
numberOfImports: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretImportEvent {
|
||||
type: EventType.CREATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
}
|
||||
type: EventType.CREATE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretImportEvent {
|
||||
type: EventType.UPDATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
orderBefore: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[],
|
||||
orderAfter: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[]
|
||||
}
|
||||
type: EventType.UPDATE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
orderBefore: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[];
|
||||
orderAfter: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretImportEvent {
|
||||
type: EventType.DELETE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
}
|
||||
type: EventType.DELETE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateUserRole {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
oldRole: string;
|
||||
newRole: string;
|
||||
}
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
oldRole: string;
|
||||
newRole: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateUserDeniedPermissions {
|
||||
@ -367,37 +437,82 @@ interface UpdateUserDeniedPermissions {
|
||||
}[]
|
||||
}
|
||||
}
|
||||
interface SecretApprovalMerge {
|
||||
type: EventType.SECRET_APPROVAL_MERGED;
|
||||
metadata: {
|
||||
mergedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
| UpdateSecretEvent
|
||||
| DeleteSecretEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
| AuthorizeIntegrationEvent
|
||||
| UnauthorizeIntegrationEvent
|
||||
| CreateIntegrationEvent
|
||||
| DeleteIntegrationEvent
|
||||
| AddTrustedIPEvent
|
||||
| UpdateTrustedIPEvent
|
||||
| DeleteTrustedIPEvent
|
||||
| CreateServiceTokenEvent
|
||||
| DeleteServiceTokenEvent
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
| RemoveWorkspaceMemberEvent
|
||||
| CreateFolderEvent
|
||||
| UpdateFolderEvent
|
||||
| DeleteFolderEvent
|
||||
| CreateWebhookEvent
|
||||
| UpdateWebhookStatusEvent
|
||||
| DeleteWebhookEvent
|
||||
| GetSecretImportsEvent
|
||||
| CreateSecretImportEvent
|
||||
| UpdateSecretImportEvent
|
||||
| DeleteSecretImportEvent
|
||||
| UpdateUserRole
|
||||
| UpdateUserDeniedPermissions;
|
||||
interface SecretApprovalClosed {
|
||||
type: EventType.SECRET_APPROVAL_CLOSED;
|
||||
metadata: {
|
||||
closedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretApprovalReopened {
|
||||
type: EventType.SECRET_APPROVAL_REOPENED;
|
||||
metadata: {
|
||||
reopenedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretApprovalRequest {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST;
|
||||
metadata: {
|
||||
committedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
| CreateSecretBatchEvent
|
||||
| UpdateSecretEvent
|
||||
| UpdateSecretBatchEvent
|
||||
| DeleteSecretEvent
|
||||
| DeleteSecretBatchEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
| AuthorizeIntegrationEvent
|
||||
| UnauthorizeIntegrationEvent
|
||||
| CreateIntegrationEvent
|
||||
| DeleteIntegrationEvent
|
||||
| AddTrustedIPEvent
|
||||
| UpdateTrustedIPEvent
|
||||
| DeleteTrustedIPEvent
|
||||
| CreateServiceTokenEvent
|
||||
| DeleteServiceTokenEvent
|
||||
| CreateServiceTokenV3Event
|
||||
| UpdateServiceTokenV3Event
|
||||
| DeleteServiceTokenV3Event
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
| RemoveWorkspaceMemberEvent
|
||||
| CreateFolderEvent
|
||||
| UpdateFolderEvent
|
||||
| DeleteFolderEvent
|
||||
| CreateWebhookEvent
|
||||
| UpdateWebhookStatusEvent
|
||||
| DeleteWebhookEvent
|
||||
| GetSecretImportsEvent
|
||||
| CreateSecretImportEvent
|
||||
| UpdateSecretImportEvent
|
||||
| DeleteSecretImportEvent
|
||||
| UpdateUserRole
|
||||
| UpdateUserDeniedPermissions
|
||||
| SecretApprovalMerge
|
||||
| SecretApprovalClosed
|
||||
| SecretApprovalRequest
|
||||
| SecretApprovalReopened;
|
||||
|
@ -29,6 +29,4 @@ const gitAppInstallationSession = new Schema<GitAppInstallationSession>({
|
||||
});
|
||||
|
||||
|
||||
const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
|
||||
|
||||
export default GitAppInstallationSession;
|
||||
export const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
|
@ -26,6 +26,4 @@ const gitAppOrganizationInstallation = new Schema<Installation>({
|
||||
});
|
||||
|
||||
|
||||
const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
|
||||
|
||||
export default GitAppOrganizationInstallation;
|
||||
export const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
|
@ -5,7 +5,7 @@ export const STATUS_RESOLVED_REVOKED = "RESOLVED_REVOKED";
|
||||
export const STATUS_RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED";
|
||||
export const STATUS_UNRESOLVED = "UNRESOLVED";
|
||||
|
||||
export type GitRisks = {
|
||||
export type IGitRisks = {
|
||||
id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
@ -42,7 +42,7 @@ export type GitRisks = {
|
||||
organization: Schema.Types.ObjectId,
|
||||
}
|
||||
|
||||
const gitRisks = new Schema<GitRisks>({
|
||||
const gitRisks = new Schema<IGitRisks>({
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
@ -147,6 +147,4 @@ const gitRisks = new Schema<GitRisks>({
|
||||
}
|
||||
}, { timestamps: true });
|
||||
|
||||
const GitRisks = model<GitRisks>("GitRisks", gitRisks);
|
||||
|
||||
export default GitRisks;
|
||||
export const GitRisks = model<IGitRisks>("GitRisks", gitRisks);
|
@ -2,6 +2,7 @@ export * from "./secretSnapshot";
|
||||
export * from "./secretVersion";
|
||||
export * from "./folderVersion";
|
||||
export * from "./log";
|
||||
export * from "./role";
|
||||
export * from "./action";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./trustedIp";
|
||||
@ -9,3 +10,5 @@ export * from "./auditLog";
|
||||
export * from "./gitRisks";
|
||||
export * from "./gitAppOrganizationInstallation";
|
||||
export * from "./gitAppInstallationSession";
|
||||
export * from "./secretApprovalPolicy";
|
||||
export * from "./secretApprovalRequest";
|
||||
|
@ -50,6 +50,4 @@ const roleSchema = new Schema<IRole>(
|
||||
|
||||
roleSchema.index({ organization: 1, workspace: 1 });
|
||||
|
||||
const Role = model<IRole>("Role", roleSchema);
|
||||
|
||||
export default Role;
|
||||
export const Role = model<IRole>("Role", roleSchema);
|
51
backend/src/ee/models/secretApprovalPolicy.ts
Normal file
51
backend/src/ee/models/secretApprovalPolicy.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface ISecretApprovalPolicy {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
name: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
approvers: Types.ObjectId[];
|
||||
approvals: number;
|
||||
}
|
||||
|
||||
const secretApprovalPolicySchema = new Schema<ISecretApprovalPolicy>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
approvers: [
|
||||
{
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Membership"
|
||||
}
|
||||
],
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
approvals: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretApprovalPolicy = model<ISecretApprovalPolicy>(
|
||||
"SecretApprovalPolicy",
|
||||
secretApprovalPolicySchema
|
||||
);
|
203
backend/src/ee/models/secretApprovalRequest.ts
Normal file
203
backend/src/ee/models/secretApprovalRequest.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from "../../variables";
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
export enum CommitType {
|
||||
DELETE = "delete",
|
||||
UPDATE = "update",
|
||||
CREATE = "create"
|
||||
}
|
||||
|
||||
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
const nanoId = customAlphabet(SLUG_ALPHABETS, 10);
|
||||
|
||||
export interface ISecretApprovalSecChange {
|
||||
_id: Types.ObjectId;
|
||||
version: number;
|
||||
secretBlindIndex?: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentCiphertext?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
algorithm?: "aes-256-gcm";
|
||||
keyEncoding?: "utf8" | "base64";
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export type ISecretCommits<T = Types.ObjectId, J = Types.ObjectId> = Array<
|
||||
| {
|
||||
newVersion: ISecretApprovalSecChange;
|
||||
op: CommitType.CREATE;
|
||||
}
|
||||
| {
|
||||
// secret is recorded to get the latest version, we can keep ref to secret for pulling change as it will also get changed
|
||||
// on merge
|
||||
secretVersion: J;
|
||||
secret: T;
|
||||
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
|
||||
op: CommitType.UPDATE;
|
||||
}
|
||||
| {
|
||||
secret: T;
|
||||
secretVersion: J;
|
||||
op: CommitType.DELETE;
|
||||
}
|
||||
>;
|
||||
export interface ISecretApprovalRequest {
|
||||
_id: Types.ObjectId;
|
||||
committer: Types.ObjectId;
|
||||
slug: string;
|
||||
statusChangeBy: Types.ObjectId;
|
||||
reviewers: {
|
||||
member: Types.ObjectId;
|
||||
status: ApprovalStatus;
|
||||
}[];
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
hasMerged: boolean;
|
||||
status: "open" | "close";
|
||||
policy: Types.ObjectId;
|
||||
commits: ISecretCommits;
|
||||
conflicts: Array<{ secretId: string; op: CommitType }>;
|
||||
}
|
||||
|
||||
const secretApprovalSecretChangeSchema = new Schema<ISecretApprovalSecChange>({
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
secretBlindIndex: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
skipMultilineEncoding: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
enum: [ALGORITHM_AES_256_GCM],
|
||||
required: true,
|
||||
default: ALGORITHM_AES_256_GCM
|
||||
},
|
||||
keyEncoding: {
|
||||
type: String,
|
||||
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||
required: true,
|
||||
default: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
tags: {
|
||||
ref: "Tag",
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: []
|
||||
}
|
||||
});
|
||||
|
||||
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
folderId: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "root"
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
default: () => nanoId()
|
||||
},
|
||||
reviewers: {
|
||||
type: [
|
||||
{
|
||||
member: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Membership"
|
||||
},
|
||||
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
|
||||
}
|
||||
],
|
||||
default: []
|
||||
},
|
||||
policy: { type: Schema.Types.ObjectId, ref: "SecretApprovalPolicy" },
|
||||
hasMerged: { type: Boolean, default: false },
|
||||
status: { type: String, enum: ["close", "open"], default: "open" },
|
||||
committer: { type: Schema.Types.ObjectId, ref: "Membership" },
|
||||
statusChangeBy: { type: Schema.Types.ObjectId, ref: "Membership" },
|
||||
commits: [
|
||||
{
|
||||
secret: { type: Types.ObjectId, ref: "Secret" },
|
||||
newVersion: secretApprovalSecretChangeSchema,
|
||||
secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
|
||||
op: { type: String, enum: [CommitType], required: true }
|
||||
}
|
||||
],
|
||||
conflicts: {
|
||||
type: [
|
||||
{
|
||||
secretId: { type: String, required: true },
|
||||
op: { type: String, enum: [CommitType], required: true }
|
||||
}
|
||||
],
|
||||
default: []
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretApprovalRequest = model<ISecretApprovalRequest>(
|
||||
"SecretApprovalRequest",
|
||||
secretApprovalRequestSchema
|
||||
);
|
@ -4,7 +4,7 @@ import {
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../../variables";
|
||||
|
||||
export interface ISecretVersion {
|
||||
@ -23,6 +23,7 @@ export interface ISecretVersion {
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
algorithm: "aes-256-gcm";
|
||||
keyEncoding: "utf8" | "base64";
|
||||
createdAt: string;
|
||||
@ -36,95 +37,96 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
// could be deleted
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Secret",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
ref: "User"
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
isDeleted: {
|
||||
// consider removing field
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretBlindIndex: {
|
||||
type: String,
|
||||
select: false,
|
||||
select: false
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
skipMultilineEncoding: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
enum: [ALGORITHM_AES_256_GCM],
|
||||
required: true,
|
||||
default: ALGORITHM_AES_256_GCM,
|
||||
default: ALGORITHM_AES_256_GCM
|
||||
},
|
||||
keyEncoding: {
|
||||
type: String,
|
||||
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||
required: true,
|
||||
default: ENCODING_SCHEME_UTF8,
|
||||
default: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
tags: {
|
||||
ref: "Tag",
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: [],
|
||||
default: []
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretVersion = model<ISecretVersion>(
|
||||
"SecretVersion",
|
||||
secretVersionSchema
|
||||
);
|
||||
export const SecretVersion = model<ISecretVersion>("SecretVersion", secretVersionSchema);
|
||||
|
@ -8,6 +8,8 @@ import action from "./action";
|
||||
import cloudProducts from "./cloudProducts";
|
||||
import secretScanning from "./secretScanning";
|
||||
import roles from "./role";
|
||||
import secretApprovalPolicy from "./secretApprovalPolicy";
|
||||
import secretApprovalRequest from "./secretApprovalRequest";
|
||||
|
||||
export {
|
||||
secret,
|
||||
@ -19,5 +21,7 @@ export {
|
||||
action,
|
||||
cloudProducts,
|
||||
secretScanning,
|
||||
roles
|
||||
roles,
|
||||
secretApprovalPolicy,
|
||||
secretApprovalRequest
|
||||
};
|
||||
|
47
backend/src/ee/routes/v1/secretApprovalPolicy.ts
Normal file
47
backend/src/ee/routes/v1/secretApprovalPolicy.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { requireAuth } from "../../../middleware";
|
||||
import { secretApprovalPolicyController } from "../../controllers/v1";
|
||||
import { AuthMode } from "../../../variables";
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.getSecretApprovalPolicy
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/board",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.getSecretApprovalPolicyOfBoard
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.createSecretApprovalPolicy
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.updateSecretApprovalPolicy
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.deleteSecretApprovalPolicy
|
||||
);
|
||||
|
||||
export default router;
|
55
backend/src/ee/routes/v1/secretApprovalRequest.ts
Normal file
55
backend/src/ee/routes/v1/secretApprovalRequest.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { requireAuth } from "../../../middleware";
|
||||
import { secretApprovalRequestController } from "../../controllers/v1";
|
||||
import { AuthMode } from "../../../variables";
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalRequestController.getSecretApprovalRequests
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/count",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalRequestController.getSecretApprovalRequestCount
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:id",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalRequestController.getSecretApprovalRequestDetails
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:id/merge",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalRequestController.mergeSecretApprovalRequest
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:id/review",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalRequestController.updateSecretApprovalReviewStatus
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:id/status",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalRequestController.updateSecretApprovalRequestStatus
|
||||
);
|
||||
|
||||
export default router;
|
@ -6,58 +6,23 @@ import { ssoController } from "../../controllers/v1";
|
||||
import { authLimiter } from "../../../helpers/rateLimiter";
|
||||
import { AuthMode } from "../../../variables";
|
||||
|
||||
router.get("/redirect/google", authLimiter, (req, res, next) => {
|
||||
passport.authenticate("google", {
|
||||
scope: ["profile", "email"],
|
||||
session: false,
|
||||
...(req.query.callback_port
|
||||
? {
|
||||
state: req.query.callback_port as string
|
||||
}
|
||||
: {})
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
router.get(
|
||||
"/google",
|
||||
passport.authenticate("google", {
|
||||
failureRedirect: "/login/provider/error",
|
||||
session: false
|
||||
}),
|
||||
ssoController.redirectSSO
|
||||
);
|
||||
|
||||
router.get("/redirect/github", authLimiter, (req, res, next) => {
|
||||
passport.authenticate("github", {
|
||||
session: false,
|
||||
...(req.query.callback_port
|
||||
? {
|
||||
state: req.query.callback_port as string
|
||||
}
|
||||
: {})
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
router.get(
|
||||
"/github",
|
||||
"/redirect/saml2/:ssoIdentifier",
|
||||
authLimiter,
|
||||
passport.authenticate("github", {
|
||||
failureRedirect: "/login/provider/error",
|
||||
session: false
|
||||
}),
|
||||
ssoController.redirectSSO
|
||||
(req, res, next) => {
|
||||
const options = {
|
||||
failureRedirect: "/",
|
||||
additionalParams: {
|
||||
RelayState: JSON.stringify({
|
||||
spInitiated: true,
|
||||
callbackPort: req.query.callback_port ?? ""
|
||||
})
|
||||
},
|
||||
};
|
||||
passport.authenticate("saml", options)(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => {
|
||||
const options = {
|
||||
failureRedirect: "/",
|
||||
additionalParams: {
|
||||
RelayState: req.query.callback_port ?? ""
|
||||
}
|
||||
};
|
||||
passport.authenticate("saml", options)(req, res, next);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/saml2/:ssoIdentifier",
|
||||
passport.authenticate("saml", {
|
||||
|
5
backend/src/ee/routes/v3/index.ts
Normal file
5
backend/src/ee/routes/v3/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import serviceTokenData from "./serviceTokenData";
|
||||
|
||||
export {
|
||||
serviceTokenData
|
||||
}
|
39
backend/src/ee/routes/v3/serviceTokenData.ts
Normal file
39
backend/src/ee/routes/v3/serviceTokenData.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { requireAuth } from "../../../middleware";
|
||||
import { AuthMode } from "../../../variables";
|
||||
import { serviceTokenDataController } from "../../controllers/v3";
|
||||
|
||||
router.get(
|
||||
"/me/key",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
serviceTokenDataController.getServiceTokenDataKey
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
serviceTokenDataController.createServiceTokenData
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:serviceTokenDataId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
serviceTokenDataController.updateServiceTokenData
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:serviceTokenDataId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
serviceTokenDataController.deleteServiceTokenData
|
||||
);
|
||||
|
||||
export default router;
|
@ -1,12 +1,12 @@
|
||||
import { Types } from "mongoose";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import NodeCache from "node-cache";
|
||||
import {
|
||||
import {
|
||||
getLicenseKey,
|
||||
getLicenseServerKey,
|
||||
getLicenseServerUrl,
|
||||
} from "../../config";
|
||||
import {
|
||||
import {
|
||||
licenseKeyRequest,
|
||||
licenseServerKeyRequest,
|
||||
refreshLicenseKeyToken,
|
||||
@ -37,6 +37,7 @@ interface FeatureSet {
|
||||
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
|
||||
trial_end: number | null;
|
||||
has_used_trial: boolean;
|
||||
secretApproval: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,7 +47,7 @@ interface FeatureSet {
|
||||
* - Self-hosted enterprise: Fetch and update global feature set
|
||||
*/
|
||||
class EELicenseService {
|
||||
|
||||
|
||||
private readonly _isLicenseValid: boolean; // TODO: deprecate
|
||||
|
||||
public instanceType: "self-hosted" | "enterprise-self-hosted" | "cloud" = "self-hosted";
|
||||
@ -64,7 +65,7 @@ class EELicenseService {
|
||||
secretVersioning: true,
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: false,
|
||||
rbac: true,
|
||||
rbac: false,
|
||||
customRateLimits: false,
|
||||
customAlerts: false,
|
||||
auditLogs: false,
|
||||
@ -72,18 +73,19 @@ class EELicenseService {
|
||||
samlSSO: false,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true
|
||||
has_used_trial: true,
|
||||
secretApproval: false
|
||||
}
|
||||
|
||||
public localFeatureSet: NodeCache;
|
||||
|
||||
|
||||
constructor() {
|
||||
this._isLicenseValid = true;
|
||||
this.localFeatureSet = new NodeCache({
|
||||
stdTTL: 60,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public async getPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId): Promise<FeatureSet> {
|
||||
try {
|
||||
if (this.instanceType === "cloud") {
|
||||
@ -96,7 +98,7 @@ class EELicenseService {
|
||||
if (!organization) throw OrganizationNotFoundError();
|
||||
|
||||
let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`;
|
||||
|
||||
|
||||
if (workspaceId) {
|
||||
url += `?workspaceId=${workspaceId}`;
|
||||
}
|
||||
@ -114,14 +116,14 @@ class EELicenseService {
|
||||
|
||||
return this.globalFeatureSet;
|
||||
}
|
||||
|
||||
|
||||
public async refreshPlan(organizationId: Types.ObjectId, workspaceId?: Types.ObjectId) {
|
||||
if (this.instanceType === "cloud") {
|
||||
this.localFeatureSet.del(`${organizationId.toString()}-${workspaceId?.toString() ?? ""}`);
|
||||
await this.getPlan(organizationId, workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async delPlan(organizationId: Types.ObjectId) {
|
||||
if (this.instanceType === "cloud") {
|
||||
this.localFeatureSet.del(`${organizationId.toString()}-`);
|
||||
@ -136,23 +138,23 @@ class EELicenseService {
|
||||
if (licenseServerKey) {
|
||||
// license server key is present -> validate it
|
||||
const token = await refreshLicenseServerKeyToken()
|
||||
|
||||
|
||||
if (token) {
|
||||
this.instanceType = "cloud";
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (licenseKey) {
|
||||
// license key is present -> validate it
|
||||
const token = await refreshLicenseKeyToken();
|
||||
|
||||
|
||||
if (token) {
|
||||
const { data: { currentPlan } } = await licenseKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license/v1/plan`
|
||||
);
|
||||
|
||||
|
||||
this.globalFeatureSet = currentPlan;
|
||||
this.instanceType = "enterprise-self-hosted";
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Probot } from "probot";
|
||||
import GitRisks from "../../models/gitRisks";
|
||||
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
|
||||
import {
|
||||
GitAppOrganizationInstallation,
|
||||
GitRisks
|
||||
} from "../../models";
|
||||
import { scanGithubPushEventForSecretLeaks } from "../../../queues/secret-scanning/githubScanPushEvent";
|
||||
export default async (app: Probot) => {
|
||||
app.on("installation.deleted", async (context) => {
|
||||
|
@ -49,7 +49,8 @@ export enum ProjectPermissionSub {
|
||||
IpAllowList = "ip-allowlist",
|
||||
Workspace = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretRollback = "secret-rollback"
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@ -72,6 +73,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
@ -85,6 +87,11 @@ const buildAdminPermission = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
@ -154,6 +161,8 @@ const buildMemberPermission = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
@ -203,6 +212,7 @@ const buildViewerPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
|
656
backend/src/ee/services/SecretApprovalService.ts
Normal file
656
backend/src/ee/services/SecretApprovalService.ts
Normal file
@ -0,0 +1,656 @@
|
||||
import picomatch from "picomatch";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
containsGlobPatterns,
|
||||
generateSecretBlindIndexWithSaltHelper,
|
||||
getSecretBlindIndexSaltHelper
|
||||
} from "../../helpers/secrets";
|
||||
import { Folder, ISecret, Secret } from "../../models";
|
||||
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../models/secretApprovalPolicy";
|
||||
import {
|
||||
CommitType,
|
||||
ISecretApprovalRequest,
|
||||
ISecretApprovalSecChange,
|
||||
ISecretCommits,
|
||||
SecretApprovalRequest
|
||||
} from "../models/secretApprovalRequest";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../variables";
|
||||
import TelemetryService from "../../services/TelemetryService";
|
||||
import { EEAuditLogService, EESecretService } from "../services";
|
||||
import { EventType, SecretVersion } from "../models";
|
||||
import { AuthData } from "../../interfaces/middleware";
|
||||
|
||||
// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2
|
||||
const getPolicyScore = (policy: ISecretApprovalPolicy) =>
|
||||
policy.secretPath ? (containsGlobPatterns(policy.secretPath) ? 1 : 2) : 0;
|
||||
|
||||
// this will fetch the policy that gets priority for an environment and secret path
|
||||
export const getSecretPolicyOfBoard = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
secretPath: string
|
||||
) => {
|
||||
const policies = await SecretApprovalPolicy.find({ workspace: workspaceId, environment });
|
||||
if (!policies) return;
|
||||
// this will filter policies either without scoped to secret path or the one that matches with secret path
|
||||
const policiesFilteredByPath = policies.filter(
|
||||
({ secretPath: policyPath }) =>
|
||||
!policyPath || picomatch.isMatch(secretPath, policyPath, { strictSlashes: false })
|
||||
);
|
||||
// now sort by priority. exact secret path gets first match followed by glob followed by just env scoped
|
||||
// if that is tie get by first createdAt
|
||||
const policiesByPriority = policiesFilteredByPath.sort(
|
||||
(a, b) => getPolicyScore(b) - getPolicyScore(a)
|
||||
);
|
||||
const finalPolicy = policiesByPriority.shift();
|
||||
return finalPolicy;
|
||||
};
|
||||
|
||||
const getLatestSecretVersion = async (secretIds: Types.ObjectId[]) => {
|
||||
const latestSecretVersions = await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
},
|
||||
type: SECRET_SHARED
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 }
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
version: { $max: "$version" },
|
||||
versionId: { $max: "$_id" }, // id of latest secret versionId
|
||||
secret: { $first: "$$ROOT" }
|
||||
}
|
||||
}
|
||||
]).exec();
|
||||
// reduced with secret id and latest version as document
|
||||
return latestSecretVersions.reduce(
|
||||
(prev, curr) => ({ ...prev, [curr._id.toString()]: curr.secret }),
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
type TApprovalCreateSecret = Omit<ISecretApprovalSecChange, "_id" | "version"> & {
|
||||
secretName: string;
|
||||
};
|
||||
type TApprovalUpdateSecret = Partial<Omit<ISecretApprovalSecChange, "_id" | "version">> & {
|
||||
secretName: string;
|
||||
newSecretName?: string;
|
||||
};
|
||||
|
||||
type TGenerateSecretApprovalRequestArg = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
policy: ISecretApprovalPolicy;
|
||||
data: {
|
||||
[CommitType.CREATE]?: TApprovalCreateSecret[];
|
||||
[CommitType.UPDATE]?: TApprovalUpdateSecret[];
|
||||
[CommitType.DELETE]?: { secretName: string }[];
|
||||
};
|
||||
commiterMembershipId: string;
|
||||
authData: AuthData;
|
||||
};
|
||||
|
||||
export const generateSecretApprovalRequest = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
policy,
|
||||
data,
|
||||
commiterMembershipId,
|
||||
authData
|
||||
}: TGenerateSecretApprovalRequestArg) => {
|
||||
// calculate folder id from secret path
|
||||
let folderId = "root";
|
||||
const rootFolder = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!rootFolder && secretPath !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
if (rootFolder) {
|
||||
const folder = getFolderByPath(rootFolder.nodes, secretPath);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// generate secret blindIndexes
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
const commits: ISecretApprovalRequest["commits"] = [];
|
||||
|
||||
// -----
|
||||
// for created secret approval change
|
||||
const createdSecret = data[CommitType.CREATE];
|
||||
if (createdSecret && createdSecret?.length) {
|
||||
// validation checks whether secret exists for creation
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
createdSecret.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[createdSecret[i].secretName] = curr;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
// check created secret exists
|
||||
const exists = await Secret.exists({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.or(
|
||||
createdSecret.map(({ secretName }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: SECRET_SHARED
|
||||
}))
|
||||
)
|
||||
.exec();
|
||||
if (exists) throw BadRequestError({ message: "Secrets already exist" });
|
||||
commits.push(
|
||||
...createdSecret.map((el) => ({
|
||||
op: CommitType.CREATE as const,
|
||||
newVersion: {
|
||||
...el,
|
||||
version: 0,
|
||||
_id: new Types.ObjectId(),
|
||||
secretBlindIndex: secretBlindIndexes[el.secretName]
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// ----
|
||||
// updated secrets approval change
|
||||
const updatedSecret = data[CommitType.UPDATE];
|
||||
if (updatedSecret && updatedSecret?.length) {
|
||||
// validation checks whether secret doesn't exists for update
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
updatedSecret.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[updatedSecret[i].secretName] = curr;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
// check update secret exists
|
||||
const oldSecrets = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment,
|
||||
type: SECRET_SHARED,
|
||||
secretBlindIndex: {
|
||||
$in: updatedSecret.map(({ secretName }) => secretBlindIndexes[secretName])
|
||||
}
|
||||
})
|
||||
.select("+secretBlindIndex")
|
||||
.lean()
|
||||
.exec();
|
||||
if (oldSecrets.length !== updatedSecret.length)
|
||||
throw BadRequestError({ message: "Secrets already exist" });
|
||||
|
||||
// finally check updating blindindex exist
|
||||
const nameUpdatedSecrets = updatedSecret.filter(({ newSecretName }) => Boolean(newSecretName));
|
||||
const newSecretBlindIndexes = await Promise.all(
|
||||
nameUpdatedSecrets.map(({ newSecretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName: newSecretName as string,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[nameUpdatedSecrets[i].secretName] = curr;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
const doesAnySecretExistWithNewIndex = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment,
|
||||
secretBlindIndex: { $in: Object.values(newSecretBlindIndexes) }
|
||||
});
|
||||
if (doesAnySecretExistWithNewIndex.length)
|
||||
throw BadRequestError({ message: "Secret with new name already exist" });
|
||||
|
||||
const oldSecretsGroupById = oldSecrets.reduce<Record<string, ISecret>>(
|
||||
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
|
||||
{}
|
||||
);
|
||||
const latestSecretVersions = await getLatestSecretVersion(
|
||||
updatedSecret.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
|
||||
);
|
||||
|
||||
commits.push(
|
||||
...updatedSecret.map((el) => {
|
||||
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
|
||||
return {
|
||||
op: CommitType.UPDATE as const,
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId.toString()]._id,
|
||||
newVersion: {
|
||||
...el,
|
||||
secretBlindIndex: newSecretBlindIndexes?.[el.secretName],
|
||||
_id: new Types.ObjectId(),
|
||||
version: oldSecretsGroupById[secretBlindIndexes[el.secretName]].version || 1
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// -----
|
||||
// deleted secrets
|
||||
const deletedSecrets = data[CommitType.DELETE];
|
||||
if (deletedSecrets && deletedSecrets.length) {
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
deletedSecrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[deletedSecrets[i].secretName] = curr;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const secretsToDelete = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment,
|
||||
type: SECRET_SHARED,
|
||||
secretBlindIndex: {
|
||||
$in: deletedSecrets.map(({ secretName }) => secretBlindIndexes[secretName])
|
||||
}
|
||||
})
|
||||
.select({ secretBlindIndex: 1, _id: 1 })
|
||||
.lean()
|
||||
.exec();
|
||||
if (secretsToDelete.length !== deletedSecrets.length)
|
||||
throw BadRequestError({ message: "Deleted secrets not found" });
|
||||
|
||||
const oldSecretsGroupById = secretsToDelete.reduce<Record<string, ISecret>>(
|
||||
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
|
||||
{}
|
||||
);
|
||||
const latestSecretVersions = await getLatestSecretVersion(
|
||||
deletedSecrets.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
|
||||
);
|
||||
|
||||
commits.push(
|
||||
...deletedSecrets.map((el) => {
|
||||
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
|
||||
return {
|
||||
op: CommitType.DELETE as const,
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId.toString()]
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const secretApprovalRequest = new SecretApprovalRequest({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
policy,
|
||||
commits,
|
||||
committer: commiterMembershipId
|
||||
});
|
||||
await secretApprovalRequest.save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
authData,
|
||||
{
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: commiterMembershipId,
|
||||
secretApprovalRequestId: secretApprovalRequest._id.toString(),
|
||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: secretApprovalRequest.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
|
||||
// validation for a merge conditions happen in another function in controller
|
||||
export const performSecretApprovalRequestMerge = async (
|
||||
id: string,
|
||||
authData: AuthData,
|
||||
userMembershipId: string
|
||||
) => {
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
|
||||
.populate<{ commits: ISecretCommits<ISecret> }>({
|
||||
path: "commits.secret",
|
||||
select: "+secretBlindIndex",
|
||||
populate: {
|
||||
path: "tags"
|
||||
}
|
||||
})
|
||||
.select("+commits.newVersion.secretBlindIndex");
|
||||
if (!secretApprovalRequest) throw BadRequestError({ message: "Approval request not found" });
|
||||
|
||||
const workspaceId = secretApprovalRequest.workspace;
|
||||
const environment = secretApprovalRequest.environment;
|
||||
const folderId = secretApprovalRequest.folderId;
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const conflicts: Array<{ secretId: string; op: CommitType }> = [];
|
||||
|
||||
const secretCreationCommits = secretApprovalRequest.commits.filter(
|
||||
({ op }) => op === CommitType.CREATE
|
||||
) as Array<{ op: CommitType.CREATE; newVersion: ISecretApprovalSecChange }>;
|
||||
if (secretCreationCommits.length) {
|
||||
// the created secrets already exist thus creation conflict ones
|
||||
const conflictedSecrets = await Secret.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folder: folderId,
|
||||
secretBlindIndex: {
|
||||
$in: secretCreationCommits.map(({ newVersion }) => newVersion.secretBlindIndex)
|
||||
}
|
||||
})
|
||||
.select("+secretBlindIndex")
|
||||
.lean();
|
||||
const conflictGroupByBlindIndex = conflictedSecrets.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.secretBlindIndex || ""]: true }),
|
||||
{}
|
||||
);
|
||||
const nonConflictSecrets = secretCreationCommits.filter(
|
||||
({ newVersion }) => !conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""]
|
||||
);
|
||||
secretCreationCommits
|
||||
.filter(({ newVersion }) => conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""])
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: CommitType.CREATE, secretId: el.newVersion._id.toString() });
|
||||
});
|
||||
|
||||
// create secret
|
||||
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
|
||||
nonConflictSecrets.map(
|
||||
({
|
||||
newVersion: {
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex,
|
||||
algorithm,
|
||||
keyEncoding,
|
||||
tags
|
||||
}
|
||||
}) => ({
|
||||
version: 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type: SECRET_SHARED,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
folder: folderId,
|
||||
algorithm: algorithm || ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: keyEncoding || ENCODING_SCHEME_UTF8,
|
||||
tags,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: newlyCreatedSecrets.map(
|
||||
(secret) =>
|
||||
new SecretVersion({
|
||||
secret: secret._id,
|
||||
version: secret.version,
|
||||
workspace: secret.workspace,
|
||||
type: secret.type,
|
||||
folder: folderId,
|
||||
tags: secret.tags,
|
||||
skipMultilineEncoding: secret?.skipMultilineEncoding,
|
||||
environment: secret.environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex: secret.secretBlindIndex,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
const secretUpdationCommits = secretApprovalRequest.commits.filter(
|
||||
({ op }) => op === CommitType.UPDATE
|
||||
) as Array<{
|
||||
op: CommitType.UPDATE;
|
||||
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
|
||||
secret: ISecret;
|
||||
}>;
|
||||
if (secretUpdationCommits.length) {
|
||||
const conflictedByNewBlindIndex = await Secret.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folder: folderId,
|
||||
secretBlindIndex: {
|
||||
$in: secretUpdationCommits
|
||||
.map(({ newVersion }) => newVersion?.secretBlindIndex)
|
||||
.filter(Boolean)
|
||||
}
|
||||
})
|
||||
.select("+secretBlindIndex")
|
||||
.lean();
|
||||
const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => (curr?.secretBlindIndex ? { ...prev, [curr.secretBlindIndex]: true } : prev),
|
||||
{}
|
||||
);
|
||||
secretUpdationCommits
|
||||
.filter(
|
||||
({ newVersion, secret }) =>
|
||||
(newVersion.secretBlindIndex && conflictGroupByBlindIndex[newVersion.secretBlindIndex]) ||
|
||||
!secret
|
||||
)
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: CommitType.UPDATE, secretId: el.newVersion._id.toString() });
|
||||
});
|
||||
|
||||
const nonConflictSecrets = secretUpdationCommits.filter(
|
||||
({ newVersion, secret }) =>
|
||||
Boolean(secret) &&
|
||||
(newVersion?.secretBlindIndex
|
||||
? !conflictGroupByBlindIndex[newVersion.secretBlindIndex]
|
||||
: true)
|
||||
);
|
||||
await Secret.bulkWrite(
|
||||
// id and version are stripped off
|
||||
nonConflictSecrets.map(
|
||||
({
|
||||
newVersion: {
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex,
|
||||
tags
|
||||
},
|
||||
secret
|
||||
}) => ({
|
||||
updateOne: {
|
||||
filter: {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
secretBlindIndex: secret.secretBlindIndex,
|
||||
type: SECRET_SHARED
|
||||
},
|
||||
update: {
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex,
|
||||
tags,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: nonConflictSecrets.map(({ newVersion, secret }) => {
|
||||
return new SecretVersion({
|
||||
secret: secret._id,
|
||||
version: secret.version + 1,
|
||||
workspace: workspaceId,
|
||||
type: SECRET_SHARED,
|
||||
folder: folderId,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex: newVersion?.secretBlindIndex ?? secret.secretBlindIndex,
|
||||
secretKeyCiphertext: newVersion?.secretKeyCiphertext ?? secret.secretKeyCiphertext,
|
||||
secretKeyIV: newVersion?.secretKeyIV ?? secret.secretKeyCiphertext,
|
||||
secretKeyTag: newVersion?.secretKeyTag ?? secret.secretKeyTag,
|
||||
secretValueCiphertext: newVersion?.secretValueCiphertext ?? secret.secretValueCiphertext,
|
||||
secretValueIV: newVersion?.secretValueIV ?? secret.secretValueIV,
|
||||
secretValueTag: newVersion?.secretValueTag ?? secret.secretValueTag,
|
||||
tags: newVersion?.tags ?? secret.tags,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
skipMultilineEncoding: newVersion?.skipMultilineEncoding ?? secret.skipMultilineEncoding
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const secretDeletionCommits = secretApprovalRequest.commits.filter(
|
||||
({ op }) => op === CommitType.DELETE
|
||||
) as Array<{
|
||||
op: CommitType.DELETE;
|
||||
secret: ISecret;
|
||||
}>;
|
||||
if (secretDeletionCommits.length) {
|
||||
await Secret.deleteMany({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.or(
|
||||
secretDeletionCommits.map(({ secret: { secretBlindIndex } }) => ({
|
||||
secretBlindIndex,
|
||||
type: { $in: ["shared", "personal"] }
|
||||
}))
|
||||
)
|
||||
.exec();
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: secretDeletionCommits.map(({ secret }) => secret._id)
|
||||
});
|
||||
}
|
||||
|
||||
const updatedSecretApproval = await SecretApprovalRequest.findByIdAndUpdate(
|
||||
id,
|
||||
{
|
||||
conflicts,
|
||||
hasMerged: true,
|
||||
status: "close",
|
||||
statusChangeBy: userMembershipId
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (postHogClient) {
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets merged",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secretApprovalRequest.commits.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
// question to team where to keep secretKey
|
||||
await EEAuditLogService.createAuditLog(
|
||||
authData,
|
||||
{
|
||||
type: EventType.SECRET_APPROVAL_MERGED,
|
||||
metadata: {
|
||||
mergedBy: userMembershipId,
|
||||
secretApprovalRequestId: id,
|
||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId
|
||||
}
|
||||
);
|
||||
|
||||
return updatedSecretApproval;
|
||||
};
|
54
backend/src/ee/validation/secretApproval.ts
Normal file
54
backend/src/ee/validation/secretApproval.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const GetSecretApprovalRuleList = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const GetSecretApprovalPolicyOfABoard = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const CreateSecretApprovalRule = z.object({
|
||||
body: z
|
||||
.object({
|
||||
workspaceId: z.string(),
|
||||
name: z.string().optional(),
|
||||
environment: z.string(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
})
|
||||
});
|
||||
|
||||
export const UpdateSecretApprovalRule = z.object({
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z.string().optional().nullable()
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteSecretApprovalRule = z.object({
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
})
|
||||
});
|
49
backend/src/ee/validation/secretApprovalRequest.ts
Normal file
49
backend/src/ee/validation/secretApprovalRequest.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { z } from "zod";
|
||||
import { ApprovalStatus } from "../models/secretApprovalRequest";
|
||||
|
||||
export const getSecretApprovalRequests = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim().optional(),
|
||||
committer: z.string().trim().optional(),
|
||||
status: z.enum(["open", "close"]).optional(),
|
||||
limit: z.coerce.number().default(20),
|
||||
offset: z.coerce.number().default(0)
|
||||
})
|
||||
});
|
||||
|
||||
export const getSecretApprovalRequestCount = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const getSecretApprovalRequestDetails = z.object({
|
||||
params: z.object({
|
||||
id: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const updateSecretApprovalReviewStatus = z.object({
|
||||
body: z.object({
|
||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
||||
}),
|
||||
params: z.object({
|
||||
id: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const mergeSecretApprovalRequest = z.object({
|
||||
params: z.object({
|
||||
id: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const updateSecretApprovalRequestStatus = z.object({
|
||||
params: z.object({
|
||||
id: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.enum(["open", "close"])
|
||||
})
|
||||
});
|
@ -7,6 +7,7 @@ import {
|
||||
ITokenVersion,
|
||||
IUser,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
TokenVersion,
|
||||
User,
|
||||
} from "../models";
|
||||
@ -23,12 +24,14 @@ import {
|
||||
getJwtProviderAuthSecret,
|
||||
getJwtRefreshLifetime,
|
||||
getJwtRefreshSecret,
|
||||
getJwtServiceTokenSecret
|
||||
} from "../config";
|
||||
import {
|
||||
AuthMode
|
||||
} from "../variables";
|
||||
import {
|
||||
ServiceTokenAuthData,
|
||||
ServiceTokenV3AuthData,
|
||||
UserAuthData
|
||||
} from "../interfaces/middleware";
|
||||
|
||||
@ -47,6 +50,9 @@ export const validateAuthMode = ({
|
||||
headers: { [key: string]: string | string[] | undefined },
|
||||
acceptedAuthModes: AuthMode[]
|
||||
}) => {
|
||||
|
||||
// TODO: update this to accept service token v3
|
||||
|
||||
const apiKey = headers["x-api-key"];
|
||||
const authHeader = headers["authorization"];
|
||||
|
||||
@ -65,6 +71,7 @@ export const validateAuthMode = ({
|
||||
if (typeof authHeader === "string") {
|
||||
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
|
||||
const [tokenType, tokenValue] = <[string, string]>authHeader.split(" ", 2) ?? [null, null]
|
||||
|
||||
if (tokenType === null)
|
||||
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
|
||||
if (tokenType.toLowerCase() !== "bearer")
|
||||
@ -72,15 +79,21 @@ export const validateAuthMode = ({
|
||||
if (tokenValue === null)
|
||||
throw BadRequestError({ message: "Missing Authorization Body in the request header." });
|
||||
|
||||
switch (tokenValue.split(".", 1)[0]) {
|
||||
const parts = tokenValue.split(".");
|
||||
|
||||
switch (parts[0]) {
|
||||
case "st":
|
||||
authMode = AuthMode.SERVICE_TOKEN;
|
||||
authTokenValue = tokenValue;
|
||||
break;
|
||||
case "stv3":
|
||||
authMode = AuthMode.SERVICE_TOKEN_V3;
|
||||
authTokenValue = parts.slice(1).join(".");
|
||||
break;
|
||||
default:
|
||||
authMode = AuthMode.JWT;
|
||||
authTokenValue = tokenValue;
|
||||
}
|
||||
|
||||
authTokenValue = tokenValue;
|
||||
}
|
||||
|
||||
if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." });
|
||||
@ -211,8 +224,73 @@ export const getAuthSTDPayload = async ({
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
userAgentType: getUserAgentType(req.headers["user-agent"])
|
||||
}
|
||||
}
|
||||
|
||||
// return serviceTokenDataToReturn;
|
||||
/**
|
||||
* Return service token data V3 payload corresponding to service token [authTokenValue]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.authTokenValue - service token value
|
||||
* @returns {ServiceTokenData} serviceTokenData - service token data
|
||||
*/
|
||||
export const getAuthSTDV3Payload = async ({
|
||||
req,
|
||||
authTokenValue,
|
||||
}: {
|
||||
req: Request,
|
||||
authTokenValue: string;
|
||||
}): Promise<ServiceTokenV3AuthData> => {
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, await getJwtServiceTokenSecret())
|
||||
);
|
||||
|
||||
const serviceTokenData = await ServiceTokenDataV3.findOneAndUpdate(
|
||||
{
|
||||
_id: new Types.ObjectId(decodedToken._id),
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
lastUsed: new Date(),
|
||||
$inc: { usageCount: 1 }
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!serviceTokenData) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate"
|
||||
});
|
||||
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
||||
// case: service token expired
|
||||
await ServiceTokenDataV3.findByIdAndUpdate(
|
||||
serviceTokenData._id,
|
||||
{
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
actor: {
|
||||
type: ActorType.SERVICE_V3,
|
||||
metadata: {
|
||||
serviceId: serviceTokenData._id.toString(),
|
||||
name: serviceTokenData.name
|
||||
}
|
||||
},
|
||||
authPayload: serviceTokenData,
|
||||
ipAddress: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
userAgentType: getUserAgentType(req.headers["user-agent"])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -382,11 +460,15 @@ export const createToken = ({
|
||||
secret,
|
||||
}: {
|
||||
payload: any;
|
||||
expiresIn: string | number;
|
||||
expiresIn?: string | number;
|
||||
secret: string;
|
||||
}) => {
|
||||
return jwt.sign(payload, secret, {
|
||||
expiresIn,
|
||||
...(
|
||||
(expiresIn !== undefined && expiresIn !== null)
|
||||
? { expiresIn }
|
||||
: {}
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -103,7 +103,10 @@ export const getSecretsBotHelper = async ({
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}) => {
|
||||
const content: Record<string, { value: string; comment?: string }> = {};
|
||||
const content: Record<
|
||||
string,
|
||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean }
|
||||
> = {};
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
|
||||
let folderId = "root";
|
||||
@ -165,6 +168,8 @@ export const getSecretsBotHelper = async ({
|
||||
});
|
||||
content[secretKey].comment = commentValue;
|
||||
}
|
||||
|
||||
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
|
||||
});
|
||||
});
|
||||
|
||||
@ -194,6 +199,8 @@ export const getSecretsBotHelper = async ({
|
||||
});
|
||||
content[secretKey].comment = commentValue;
|
||||
}
|
||||
|
||||
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
|
||||
});
|
||||
|
||||
await expandSecrets(workspaceId.toString(), key, content);
|
||||
|
@ -14,7 +14,7 @@ export const initDatabaseHelper = async ({
|
||||
}) => {
|
||||
try {
|
||||
await mongoose.connect(mongoURL);
|
||||
|
||||
|
||||
// allow empty strings to pass the required validator
|
||||
mongoose.Schema.Types.String.checkRequired(v => typeof v === "string");
|
||||
|
||||
@ -31,14 +31,10 @@ export const initDatabaseHelper = async ({
|
||||
* Close database conection
|
||||
*/
|
||||
export const closeDatabaseHelper = async () => {
|
||||
return Promise.all([
|
||||
new Promise((resolve) => {
|
||||
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
||||
mongoose.connection.close()
|
||||
.then(() => resolve("Database connection closed"));
|
||||
} else {
|
||||
resolve("Database connection already closed");
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
if (mongoose.connection && mongoose.connection.readyState === 1) {
|
||||
await mongoose.connection.close();
|
||||
return "Database connection closed";
|
||||
} else {
|
||||
return "Database connection already closed";
|
||||
}
|
||||
};
|
@ -1,5 +1,43 @@
|
||||
import { Types } from "mongoose";
|
||||
import { MembershipOrg, Organization } from "../models";
|
||||
import mongoose, { Types, mongo } from "mongoose";
|
||||
import {
|
||||
Bot,
|
||||
BotKey,
|
||||
BotOrg,
|
||||
Folder,
|
||||
IncidentContactOrg,
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Key,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
Organization,
|
||||
Secret,
|
||||
SecretBlindIndexData,
|
||||
SecretImport,
|
||||
ServiceToken,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
ServiceTokenDataV3Key,
|
||||
Tag,
|
||||
Webhook,
|
||||
Workspace
|
||||
} from "../models";
|
||||
import {
|
||||
Action,
|
||||
AuditLog,
|
||||
FolderVersion,
|
||||
GitAppInstallationSession,
|
||||
GitAppOrganizationInstallation,
|
||||
GitRisks,
|
||||
Log,
|
||||
Role,
|
||||
SSOConfig,
|
||||
SecretApprovalPolicy,
|
||||
SecretApprovalRequest,
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
TrustedIP
|
||||
} from "../ee/models";
|
||||
import {
|
||||
ACCEPTED,
|
||||
} from "../variables";
|
||||
@ -17,6 +55,7 @@ import {
|
||||
import {
|
||||
createBotOrg
|
||||
} from "./botOrg";
|
||||
import { InternalServerError, ResourceNotFoundError } from "../utils/errors";
|
||||
|
||||
/**
|
||||
* Create an organization with name [name]
|
||||
@ -65,6 +104,320 @@ export const createOrganization = async ({
|
||||
return organization;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete organization with id [organizationId]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.organizationId - id of organization to delete
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganization = async ({
|
||||
organizationId,
|
||||
existingSession
|
||||
}: {
|
||||
organizationId: Types.ObjectId;
|
||||
existingSession?: mongo.ClientSession;
|
||||
}) => {
|
||||
|
||||
let session;
|
||||
|
||||
if (existingSession) {
|
||||
session = existingSession;
|
||||
} else {
|
||||
session = await mongoose.startSession();
|
||||
session.startTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
const organization = await Organization.findByIdAndDelete(
|
||||
organizationId,
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
|
||||
if (!organization) throw ResourceNotFoundError();
|
||||
|
||||
await MembershipOrg.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await BotOrg.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SSOConfig.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Role.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await IncidentContactOrg.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await GitRisks.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await GitAppInstallationSession.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await GitAppOrganizationInstallation.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
const workspaceIds = await Workspace.distinct("_id", {
|
||||
organization: organization._id
|
||||
});
|
||||
|
||||
await Workspace.deleteMany({
|
||||
organization: organization._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Membership.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Key.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Bot.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await BotKey.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretBlindIndexData.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Secret.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretVersion.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretSnapshot.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretImport.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Folder.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await FolderVersion.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Webhook.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await TrustedIP.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Tag.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await IntegrationAuth.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Integration.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await ServiceToken.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await ServiceTokenData.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await ServiceTokenDataV3.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await ServiceTokenDataV3Key.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await AuditLog.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Log.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Action.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretApprovalPolicy.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretApprovalRequest.deleteMany({
|
||||
workspace: {
|
||||
$in: workspaceIds
|
||||
}
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
if (organization.customerId) {
|
||||
// delete from stripe here
|
||||
await licenseServerKeyRequest.delete(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}`
|
||||
);
|
||||
}
|
||||
|
||||
return organization;
|
||||
} catch (err) {
|
||||
if (!existingSession) {
|
||||
await session.abortTransaction();
|
||||
}
|
||||
throw InternalServerError({
|
||||
message: "Failed to delete organization"
|
||||
});
|
||||
} finally {
|
||||
if (!existingSession) {
|
||||
await session.commitTransaction();
|
||||
session.endSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update organization subscription quantity to reflect number of members in
|
||||
* the organization.
|
||||
|
@ -1,20 +1,25 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
CreateSecretBatchParams,
|
||||
CreateSecretParams,
|
||||
DeleteSecretBatchParams,
|
||||
DeleteSecretParams,
|
||||
GetSecretParams,
|
||||
GetSecretsParams,
|
||||
UpdateSecretBatchParams,
|
||||
UpdateSecretParams
|
||||
} from "../interfaces/services/SecretService";
|
||||
import {
|
||||
Folder,
|
||||
ISecret,
|
||||
IServiceTokenData,
|
||||
IServiceTokenDataV3,
|
||||
Secret,
|
||||
SecretBlindIndexData,
|
||||
ServiceTokenData,
|
||||
TFolderRootSchema
|
||||
} from "../models";
|
||||
import { Permission } from "../models/serviceTokenDataV3";
|
||||
import { EventType, SecretVersion } from "../ee/models";
|
||||
import {
|
||||
BadRequestError,
|
||||
@ -50,6 +55,49 @@ import picomatch from "picomatch";
|
||||
import path from "path";
|
||||
import { getAnImportedSecret } from "../services/SecretImportService";
|
||||
|
||||
/**
|
||||
* Validate scope for service token v3
|
||||
* @param authPayload
|
||||
* @param environment
|
||||
* @param secretPath
|
||||
* @returns
|
||||
*/
|
||||
export const isValidScopeV3 = ({
|
||||
authPayload,
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions
|
||||
}: {
|
||||
authPayload: IServiceTokenDataV3;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
requiredPermissions: Permission[];
|
||||
}) => {
|
||||
const { scopes } = authPayload;
|
||||
|
||||
const validScope = scopes.find(
|
||||
(scope) =>
|
||||
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
|
||||
scope.environment === environment
|
||||
);
|
||||
|
||||
if (
|
||||
validScope &&
|
||||
!requiredPermissions.every((permission) => validScope.permissions.includes(permission))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(validScope);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate scope for service token v2
|
||||
* @param authPayload
|
||||
* @param environment
|
||||
* @param secretPath
|
||||
* @returns
|
||||
*/
|
||||
export const isValidScope = (
|
||||
authPayload: IServiceTokenData,
|
||||
environment: string,
|
||||
@ -71,6 +119,8 @@ export function containsGlobPatterns(secretPath: string) {
|
||||
return globChars.some((char) => normalizedPath.includes(char));
|
||||
}
|
||||
|
||||
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "Folder not found" });
|
||||
|
||||
/**
|
||||
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
||||
*
|
||||
@ -330,7 +380,8 @@ export const createSecretHelper = async ({
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretPath = "/",
|
||||
metadata
|
||||
metadata,
|
||||
skipMultilineEncoding
|
||||
}: CreateSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
@ -394,6 +445,7 @@ export const createSecretHelper = async ({
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
skipMultilineEncoding,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
@ -416,6 +468,7 @@ export const createSecretHelper = async ({
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
skipMultilineEncoding,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
@ -737,27 +790,70 @@ export const getSecretHelper = async ({
|
||||
export const updateSecretHelper = async ({
|
||||
secretName,
|
||||
workspaceId,
|
||||
secretId,
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
newSecretName,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath
|
||||
secretPath,
|
||||
tags,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
skipMultilineEncoding
|
||||
}: UpdateSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
// get secret blind index salt
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
let oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
});
|
||||
|
||||
if (secretId) {
|
||||
const secret = await Secret.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
_id: secretId
|
||||
}).select("secretBlindIndex");
|
||||
if (secret && secret.secretBlindIndex) oldSecretBlindIndex = secret.secretBlindIndex;
|
||||
}
|
||||
|
||||
let secret: ISecret | null = null;
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
|
||||
let newSecretNameBlindIndex = undefined;
|
||||
if (newSecretName) {
|
||||
newSecretNameBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName: newSecretName,
|
||||
salt
|
||||
});
|
||||
const doesSecretAlreadyExist = await Secret.exists({
|
||||
secretBlindIndex: newSecretNameBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type
|
||||
});
|
||||
|
||||
if (doesSecretAlreadyExist) {
|
||||
throw BadRequestError({ message: "Secret with the provided name already exist" });
|
||||
}
|
||||
}
|
||||
|
||||
if (type === SECRET_SHARED) {
|
||||
// case: update shared secret
|
||||
secret = await Secret.findOneAndUpdate(
|
||||
{
|
||||
secretBlindIndex,
|
||||
secretBlindIndex: oldSecretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
@ -767,6 +863,15 @@ export const updateSecretHelper = async ({
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex: newSecretNameBlindIndex,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
tags,
|
||||
$inc: { version: 1 }
|
||||
},
|
||||
{
|
||||
@ -778,7 +883,7 @@ export const updateSecretHelper = async ({
|
||||
|
||||
secret = await Secret.findOneAndUpdate(
|
||||
{
|
||||
secretBlindIndex,
|
||||
secretBlindIndex: oldSecretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
@ -789,6 +894,12 @@ export const updateSecretHelper = async ({
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
tags,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex: newSecretNameBlindIndex,
|
||||
$inc: { version: 1 }
|
||||
},
|
||||
{
|
||||
@ -805,16 +916,18 @@ export const updateSecretHelper = async ({
|
||||
workspace: secret.workspace,
|
||||
folder: folderId,
|
||||
type,
|
||||
tags,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
environment: secret.environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex,
|
||||
secretBlindIndex: newSecretName ? newSecretNameBlindIndex : oldSecretBlindIndex,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
skipMultilineEncoding,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
@ -903,12 +1016,22 @@ export const deleteSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
secretPath = "/"
|
||||
secretPath = "/",
|
||||
// used for update corner case and blindIndex goes wrong way
|
||||
secretId
|
||||
}: DeleteSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
let secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
if (secretId) {
|
||||
const secret = await Secret.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
_id: secretId
|
||||
}).select("secretBlindIndex");
|
||||
if (secret && secret.secretBlindIndex) secretBlindIndex = secret.secretBlindIndex;
|
||||
}
|
||||
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
|
||||
@ -1098,6 +1221,7 @@ const recursivelyExpandSecret = async (
|
||||
|
||||
let interpolatedValue = interpolatedSec[key];
|
||||
if (!interpolatedValue) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Couldn't find referenced value - ${key}`);
|
||||
return "";
|
||||
}
|
||||
@ -1147,7 +1271,7 @@ const formatMultiValueEnv = (val?: string) => {
|
||||
export const expandSecrets = async (
|
||||
workspaceId: string,
|
||||
rootEncKey: string,
|
||||
secrets: Record<string, { value: string; comment?: string }>
|
||||
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
|
||||
) => {
|
||||
const expandedSec: Record<string, string> = {};
|
||||
const interpolatedSec: Record<string, string> = {};
|
||||
@ -1165,7 +1289,10 @@ export const expandSecrets = async (
|
||||
|
||||
for (const key of Object.keys(secrets)) {
|
||||
if (expandedSec?.[key]) {
|
||||
secrets[key].value = formatMultiValueEnv(expandedSec[key]);
|
||||
// should not do multi line encoding if user has set it to skip
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||
? expandedSec[key]
|
||||
: formatMultiValueEnv(expandedSec[key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1180,8 +1307,506 @@ export const expandSecrets = async (
|
||||
key
|
||||
);
|
||||
|
||||
secrets[key].value = formatMultiValueEnv(expandedVal);
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||
? expandedVal
|
||||
: formatMultiValueEnv(expandedVal);
|
||||
}
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
export const createSecretBatchHelper = async ({
|
||||
secrets,
|
||||
workspaceId,
|
||||
authData,
|
||||
secretPath,
|
||||
environment
|
||||
}: CreateSecretBatchParams) => {
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
secrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[secrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = secrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const exists = await Secret.exists({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.or(
|
||||
secrets.map(({ secretName, type }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}))
|
||||
)
|
||||
.exec();
|
||||
|
||||
if (exists)
|
||||
throw BadRequestError({
|
||||
message: "Failed to create secret that already exists"
|
||||
});
|
||||
|
||||
// create secret
|
||||
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
|
||||
secrets.map(
|
||||
({
|
||||
type,
|
||||
secretName,
|
||||
secretKeyIV,
|
||||
metadata,
|
||||
secretKeyTag,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding
|
||||
}) => ({
|
||||
version: 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
metadata,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: newlyCreatedSecrets.map(
|
||||
(secret) =>
|
||||
new SecretVersion({
|
||||
secret: secret._id,
|
||||
version: secret.version,
|
||||
workspace: secret.workspace,
|
||||
type: secret.type,
|
||||
folder: folderId,
|
||||
skipMultilineEncoding: secret?.skipMultilineEncoding,
|
||||
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
environment: secret.environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex: secret.secretBlindIndex,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
authData,
|
||||
{
|
||||
type: EventType.CREATE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
secrets: newlyCreatedSecrets.map(({ secretBlindIndex, version, _id }) => ({
|
||||
secretId: _id.toString(),
|
||||
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
|
||||
secretVersion: version
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId
|
||||
}
|
||||
);
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets added",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newlyCreatedSecrets;
|
||||
};
|
||||
|
||||
export const updateSecretBatchHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
authData,
|
||||
secretPath,
|
||||
secrets
|
||||
}: UpdateSecretBatchParams) => {
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
secrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[secrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = secrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const secretsToBeUpdated = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.select("+secretBlindIndex")
|
||||
.or(
|
||||
secrets.map(({ secretName, type }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}))
|
||||
)
|
||||
.lean();
|
||||
|
||||
if (secretsToBeUpdated.length !== secrets.length)
|
||||
throw BadRequestError({ message: "Some secrets not found" });
|
||||
|
||||
await Secret.bulkWrite(
|
||||
secrets.map(
|
||||
({
|
||||
type,
|
||||
secretName,
|
||||
tags,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding
|
||||
}) => ({
|
||||
updateOne: {
|
||||
filter: {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
},
|
||||
update: {
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
tags,
|
||||
skipMultilineEncoding
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, ISecret>>(
|
||||
(prev, curr) => {
|
||||
if (curr.secretBlindIndex) prev[curr.secretBlindIndex] = curr;
|
||||
return prev;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: secrets.map((secret) => {
|
||||
const {
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
secretBlindIndex,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
skipMultilineEncoding
|
||||
} = secretsGroupedByBlindIndex[secretBlindIndexes[secret.secretName]];
|
||||
|
||||
return new SecretVersion({
|
||||
secret: _id,
|
||||
version: version + 1,
|
||||
workspace: workspace,
|
||||
type,
|
||||
folder: folderId,
|
||||
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex: secretBlindIndex,
|
||||
secretKeyCiphertext: secretKeyCiphertext,
|
||||
secretKeyIV: secretKeyIV,
|
||||
secretKeyTag: secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
authData,
|
||||
{
|
||||
type: EventType.UPDATE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({
|
||||
secretId: _id.toString(),
|
||||
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
|
||||
secretVersion: version + 1
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId
|
||||
}
|
||||
);
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets modified",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export const deleteSecretBatchHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
authData,
|
||||
secretPath = "/",
|
||||
secrets
|
||||
}: DeleteSecretBatchParams) => {
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
secrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[secrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = secrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const deletedSecrets = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.or(
|
||||
secrets.map(({ secretName, type }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}))
|
||||
)
|
||||
.select({ secretBlindIndexes: 1 })
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
await Secret.deleteMany({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.or(
|
||||
secrets.map(({ secretName, type }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}))
|
||||
)
|
||||
.exec();
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: deletedSecrets.map((secret) => secret._id)
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
authData,
|
||||
{
|
||||
type: EventType.DELETE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
secrets: deletedSecrets.map(({ _id, version, secretBlindIndex }) => ({
|
||||
secretId: _id.toString(),
|
||||
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
|
||||
secretVersion: version
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId
|
||||
}
|
||||
);
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets deleted",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
secrets: deletedSecrets
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,27 @@
|
||||
import { IUser, User } from "../models";
|
||||
import mongoose, { Types, mongo } from "mongoose";
|
||||
import {
|
||||
APIKeyData,
|
||||
BackupPrivateKey,
|
||||
IUser,
|
||||
Key,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
TokenVersion,
|
||||
User,
|
||||
UserAction
|
||||
} from "../models";
|
||||
import {
|
||||
Action,
|
||||
Log
|
||||
} from "../ee/models";
|
||||
import { sendMail } from "./nodemailer";
|
||||
import {
|
||||
InternalServerError,
|
||||
ResourceNotFoundError
|
||||
} from "../utils/errors";
|
||||
import { ADMIN } from "../variables";
|
||||
import { deleteOrganization } from "../helpers/organization";
|
||||
import { deleteWorkspace } from "../helpers/workspace";
|
||||
|
||||
/**
|
||||
* Initialize a user under email [email]
|
||||
@ -134,3 +156,207 @@ export const checkUserDevice = async ({
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check that if we delete user with id [userId] then
|
||||
* there won't be any admin-less organizations or projects
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId - id of user to check deletion conditions for
|
||||
*/
|
||||
const checkDeleteUserConditions = async ({
|
||||
userId
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
}) => {
|
||||
const memberships = await Membership.find({
|
||||
user: userId
|
||||
});
|
||||
|
||||
const membershipOrgs = await MembershipOrg.find({
|
||||
user: userId
|
||||
});
|
||||
|
||||
// delete organizations where user is only member
|
||||
for await (const membershipOrg of membershipOrgs) {
|
||||
const orgMemberCount = await MembershipOrg.countDocuments({
|
||||
organization: membershipOrg.organization,
|
||||
});
|
||||
|
||||
const otherOrgAdminCount = await MembershipOrg.countDocuments({
|
||||
organization: membershipOrg.organization,
|
||||
user: { $ne: userId },
|
||||
role: ADMIN
|
||||
});
|
||||
|
||||
if (orgMemberCount > 1 && otherOrgAdminCount === 0) {
|
||||
throw InternalServerError({
|
||||
message: "Failed to delete account because an org would be admin-less"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// delete workspaces where user is only member
|
||||
for await (const membership of memberships) {
|
||||
const workspaceMemberCount = await Membership.countDocuments({
|
||||
workspace: membership.workspace
|
||||
});
|
||||
|
||||
const otherWorkspaceAdminCount = await Membership.countDocuments({
|
||||
workspace: membership.workspace,
|
||||
user: { $ne: userId },
|
||||
role: ADMIN
|
||||
});
|
||||
|
||||
if (workspaceMemberCount > 1 && otherWorkspaceAdminCount === 0) {
|
||||
throw InternalServerError({
|
||||
message: "Failed to delete account because a workspace would be admin-less"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete account with id [userId]
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.userId - id of user to delete
|
||||
* @returns {User} user - deleted user
|
||||
*/
|
||||
export const deleteUser = async ({
|
||||
userId,
|
||||
existingSession
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
existingSession?: mongo.ClientSession;
|
||||
}) => {
|
||||
|
||||
let session;
|
||||
|
||||
if (existingSession) {
|
||||
session = existingSession;
|
||||
} else {
|
||||
session = await mongoose.startSession();
|
||||
session.startTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findByIdAndDelete(userId, {
|
||||
session
|
||||
});
|
||||
|
||||
if (!user) throw ResourceNotFoundError();
|
||||
|
||||
await checkDeleteUserConditions({
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
await UserAction.deleteMany({
|
||||
user: user._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await BackupPrivateKey.deleteMany({
|
||||
user: user._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await APIKeyData.deleteMany({
|
||||
user: user._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Action.deleteMany({
|
||||
user: user._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Log.deleteMany({
|
||||
user: user._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await TokenVersion.deleteMany({
|
||||
user: user._id
|
||||
});
|
||||
|
||||
await Key.deleteMany({
|
||||
receiver: user._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
const membershipOrgs = await MembershipOrg.find({
|
||||
user: userId
|
||||
}, null, {
|
||||
session
|
||||
});
|
||||
|
||||
// delete organizations where user is only member
|
||||
for await (const membershipOrg of membershipOrgs) {
|
||||
const memberCount = await MembershipOrg.countDocuments({
|
||||
organization: membershipOrg.organization
|
||||
});
|
||||
|
||||
if (memberCount === 1) {
|
||||
// organization only has 1 member (the current user)
|
||||
|
||||
await deleteOrganization({
|
||||
organizationId: membershipOrg.organization,
|
||||
existingSession: session
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const memberships = await Membership.find({
|
||||
user: userId
|
||||
}, null, {
|
||||
session
|
||||
});
|
||||
|
||||
// delete workspaces where user is only member
|
||||
for await (const membership of memberships) {
|
||||
const memberCount = await Membership.countDocuments({
|
||||
workspace: membership.workspace
|
||||
});
|
||||
|
||||
if (memberCount === 1) {
|
||||
// workspace only has 1 member (the current user) -> delete workspace
|
||||
|
||||
await deleteWorkspace({
|
||||
workspaceId: membership.workspace,
|
||||
existingSession: session
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await MembershipOrg.deleteMany({
|
||||
user: userId
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Membership.deleteMany({
|
||||
user: userId
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
return user;
|
||||
} catch (err) {
|
||||
if (!existingSession) {
|
||||
await session.abortTransaction();
|
||||
}
|
||||
throw InternalServerError({
|
||||
message: "Failed to delete account"
|
||||
})
|
||||
} finally {
|
||||
if (!existingSession) {
|
||||
await session.commitTransaction();
|
||||
session.endSession();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,42 @@
|
||||
import { Types } from "mongoose";
|
||||
import mongoose, { Types, mongo } from "mongoose";
|
||||
import {
|
||||
Bot,
|
||||
BotKey,
|
||||
Folder,
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Key,
|
||||
Membership,
|
||||
Secret,
|
||||
Workspace,
|
||||
SecretBlindIndexData,
|
||||
SecretImport,
|
||||
ServiceToken,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
ServiceTokenDataV3Key,
|
||||
Tag,
|
||||
Webhook,
|
||||
Workspace
|
||||
} from "../models";
|
||||
import {
|
||||
Action,
|
||||
AuditLog,
|
||||
FolderVersion,
|
||||
IPType,
|
||||
Log,
|
||||
SecretApprovalPolicy,
|
||||
SecretApprovalRequest,
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
TrustedIP
|
||||
} from "../ee/models";
|
||||
import { createBot } from "../helpers/bot";
|
||||
import { EELicenseService } from "../ee/services";
|
||||
import { SecretService } from "../services";
|
||||
import {
|
||||
InternalServerError,
|
||||
ResourceNotFoundError
|
||||
} from "../utils/errors";
|
||||
|
||||
/**
|
||||
* Create a workspace with name [name] in organization with id [organizationId]
|
||||
@ -77,18 +101,190 @@ export const createWorkspace = async ({
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.id - id of workspace to delete
|
||||
*/
|
||||
export const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||
await Workspace.deleteOne({ _id: id });
|
||||
await Bot.deleteOne({
|
||||
workspace: id,
|
||||
});
|
||||
await Membership.deleteMany({
|
||||
workspace: id,
|
||||
});
|
||||
await Secret.deleteMany({
|
||||
workspace: id,
|
||||
});
|
||||
await Key.deleteMany({
|
||||
workspace: id,
|
||||
});
|
||||
export const deleteWorkspace = async ({
|
||||
workspaceId,
|
||||
existingSession
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
existingSession?: mongo.ClientSession;
|
||||
}) => {
|
||||
|
||||
let session;
|
||||
|
||||
if (existingSession) {
|
||||
session = existingSession;
|
||||
} else {
|
||||
session = await mongoose.startSession();
|
||||
session.startTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
const workspace = await Workspace.findByIdAndDelete(workspaceId, { session });
|
||||
|
||||
if (!workspace) throw ResourceNotFoundError();
|
||||
|
||||
await Membership.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Key.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Bot.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await BotKey.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretBlindIndexData.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Secret.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretVersion.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretSnapshot.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretImport.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Folder.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await FolderVersion.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Webhook.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await TrustedIP.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Tag.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await IntegrationAuth.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Integration.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await ServiceToken.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await ServiceTokenData.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await ServiceTokenDataV3.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await ServiceTokenDataV3Key.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await AuditLog.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Log.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await Action.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretApprovalPolicy.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
await SecretApprovalRequest.deleteMany({
|
||||
workspace: workspace._id
|
||||
}, {
|
||||
session
|
||||
});
|
||||
|
||||
return workspace;
|
||||
} catch (err) {
|
||||
if (!existingSession) {
|
||||
await session.abortTransaction();
|
||||
}
|
||||
throw InternalServerError({
|
||||
message: "Failed to delete organization"
|
||||
});
|
||||
} finally {
|
||||
if (!existingSession) {
|
||||
await session.commitTransaction();
|
||||
session.endSession();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -25,8 +25,11 @@ import {
|
||||
users as eeUsersRouter,
|
||||
workspace as eeWorkspaceRouter,
|
||||
roles as v1RoleRouter,
|
||||
secretApprovalPolicy as v1SecretApprovalPolicy,
|
||||
secretApprovalRequest as v1SecretApprovalRequest,
|
||||
secretScanning as v1SecretScanningRouter
|
||||
} from "./ee/routes/v1";
|
||||
import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3";
|
||||
import {
|
||||
auth as v1AuthRouter,
|
||||
bot as v1BotRouter,
|
||||
@ -38,6 +41,7 @@ import {
|
||||
membership as v1MembershipRouter,
|
||||
organization as v1OrganizationRouter,
|
||||
password as v1PasswordRouter,
|
||||
sso as v1SSORouter,
|
||||
secretImps as v1SecretImpsRouter,
|
||||
secret as v1SecretRouter,
|
||||
secretsFolder as v1SecretsFolder,
|
||||
@ -54,7 +58,6 @@ import {
|
||||
organizations as v2OrganizationsRouter,
|
||||
secret as v2SecretRouter, // begin to phase out
|
||||
secrets as v2SecretsRouter,
|
||||
serviceAccounts as v2ServiceAccountsRouter,
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
signup as v2SignupRouter,
|
||||
tags as v2TagsRouter,
|
||||
@ -84,6 +87,9 @@ import { setup } from "./utils/setup";
|
||||
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
|
||||
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
|
||||
const SmeeClient = require("smee-client"); // eslint-disable-line
|
||||
import path from "path";
|
||||
|
||||
let handler: null | any = null;
|
||||
|
||||
const main = async () => {
|
||||
await setup();
|
||||
@ -144,6 +150,27 @@ const main = async () => {
|
||||
next();
|
||||
});
|
||||
|
||||
if ((await getNodeEnv()) === "production" && process.env.STANDALONE_BUILD === "true") {
|
||||
const nextJsBuildPath = path.join(__dirname, "../frontend-build");
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const conf = require("../frontend-build/.next/required-server-files.json").config;
|
||||
const NextServer =
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require("../frontend-build/node_modules/next/dist/server/next-server").default;
|
||||
const nextApp = new NextServer({
|
||||
dev: false,
|
||||
dir: nextJsBuildPath,
|
||||
port: await getPort(),
|
||||
conf,
|
||||
hostname: "local",
|
||||
customServer: false
|
||||
});
|
||||
|
||||
handler = nextApp.getRequestHandler();
|
||||
}
|
||||
|
||||
// (EE) routes
|
||||
app.use("/api/v1/secret", eeSecretRouter);
|
||||
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
|
||||
@ -153,6 +180,7 @@ const main = async () => {
|
||||
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
||||
app.use("/api/v1/sso", eeSSORouter);
|
||||
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
||||
app.use("/api/v3/service-token", v3ServiceTokenDataRouter);
|
||||
|
||||
// v1 routes
|
||||
app.use("/api/v1/signup", v1SignupRouter);
|
||||
@ -176,6 +204,9 @@ const main = async () => {
|
||||
app.use("/api/v1/webhooks", v1WebhooksRouter);
|
||||
app.use("/api/v1/secret-imports", v1SecretImpsRouter);
|
||||
app.use("/api/v1/roles", v1RoleRouter);
|
||||
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
|
||||
app.use("/api/v1/sso", v1SSORouter);
|
||||
app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest);
|
||||
|
||||
// v2 routes (improvements)
|
||||
app.use("/api/v2/signup", v2SignupRouter);
|
||||
@ -188,7 +219,7 @@ const main = async () => {
|
||||
app.use("/api/v2/secret", v2SecretRouter); // deprecate
|
||||
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
|
||||
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
|
||||
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
||||
// app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
||||
|
||||
// v3 routes (experimental)
|
||||
app.use("/api/v3/auth", v3AuthRouter);
|
||||
@ -202,6 +233,12 @@ const main = async () => {
|
||||
// server status
|
||||
app.use("/api", healthCheck);
|
||||
|
||||
if (handler) {
|
||||
app.all("*", (req, res) => {
|
||||
return handler(req, res);
|
||||
});
|
||||
}
|
||||
|
||||
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||
app.use((req, res, next) => {
|
||||
if (res.headersSent) return next();
|
||||
@ -221,10 +258,24 @@ const main = async () => {
|
||||
// await createTestUserForDevelopment();
|
||||
setUpHealthEndpoint(server);
|
||||
|
||||
server.on("close", async () => {
|
||||
const serverCleanup = async () => {
|
||||
await DatabaseService.closeDatabase();
|
||||
syncSecretsToThirdPartyServices.close();
|
||||
githubPushEventSecretScan.close();
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on("SIGINT", function () {
|
||||
server.close(async () => {
|
||||
await serverCleanup();
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGTERM", function () {
|
||||
server.close(async () => {
|
||||
await serverCleanup();
|
||||
});
|
||||
});
|
||||
|
||||
return server;
|
||||
|
@ -65,10 +65,10 @@ import sodium from "libsodium-wrappers";
|
||||
import { standardRequest } from "../config/request";
|
||||
|
||||
const getSecretKeyValuePair = (
|
||||
secrets: Record<string, { value: string; comment?: string } | null>
|
||||
secrets: Record<string, { value: string | null; comment?: string } | null>
|
||||
) =>
|
||||
Object.keys(secrets).reduce<Record<string, string>>((prev, key) => {
|
||||
if (secrets[key]) prev[key] = secrets[key]?.value || "";
|
||||
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
|
||||
prev[key] = secrets?.[key] === null ? null : secrets?.[key]?.value;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
@ -87,13 +87,15 @@ const syncSecrets = async ({
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
appendices
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
integrationAuth: IIntegrationAuth;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
appendices?: { prefix: string, suffix: string };
|
||||
}) => {
|
||||
switch (integration.integration) {
|
||||
case INTEGRATION_GCP_SECRET_MANAGER:
|
||||
@ -153,7 +155,8 @@ const syncSecrets = async ({
|
||||
await syncSecretsGitHub({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
accessToken,
|
||||
appendices
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITLAB:
|
||||
@ -218,7 +221,8 @@ const syncSecrets = async ({
|
||||
await syncSecretsCheckly({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
accessToken,
|
||||
appendices
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_QOVERY:
|
||||
@ -325,40 +329,42 @@ const syncSecretsGCPSecretManager = async ({
|
||||
name: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
|
||||
interface GCPSMListSecretsRes {
|
||||
secrets?: GCPSecret[];
|
||||
totalSize?: number;
|
||||
nextPageToken?: string;
|
||||
}
|
||||
|
||||
|
||||
let gcpSecrets: GCPSecret[] = [];
|
||||
|
||||
|
||||
const pageSize = 100;
|
||||
let pageToken: string | undefined;
|
||||
let hasMorePages = true;
|
||||
|
||||
const filterParam = integration.metadata.secretGCPLabel
|
||||
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
|
||||
const filterParam = integration.metadata.secretGCPLabel
|
||||
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
|
||||
: "";
|
||||
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
pageSize: String(pageSize),
|
||||
...(pageToken ? { pageToken } : {})
|
||||
});
|
||||
|
||||
const res: GCPSMListSecretsRes = (await standardRequest.get(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
const res: GCPSMListSecretsRes = (
|
||||
await standardRequest.get(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
|
||||
)
|
||||
).data;
|
||||
|
||||
if (res.secrets) {
|
||||
const filteredSecrets = res.secrets?.filter((gcpSecret) => {
|
||||
const arr = gcpSecret.name.split("/");
|
||||
@ -366,54 +372,58 @@ const syncSecretsGCPSecretManager = async ({
|
||||
|
||||
let isValid = true;
|
||||
|
||||
if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) {
|
||||
if (
|
||||
integration.metadata.secretPrefix &&
|
||||
!key.startsWith(integration.metadata.secretPrefix)
|
||||
) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
|
||||
return isValid;
|
||||
});
|
||||
|
||||
gcpSecrets = gcpSecrets.concat(filteredSecrets);
|
||||
}
|
||||
|
||||
|
||||
if (!res.nextPageToken) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
|
||||
pageToken = res.nextPageToken;
|
||||
}
|
||||
|
||||
const res: { [key: string]: string; } = {};
|
||||
|
||||
|
||||
const res: { [key: string]: string } = {};
|
||||
|
||||
interface GCPLatestSecretVersionAccess {
|
||||
name: string;
|
||||
payload: {
|
||||
data: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
for await (const gcpSecret of gcpSecrets) {
|
||||
const arr = gcpSecret.name.split("/");
|
||||
const key = arr[arr.length - 1];
|
||||
|
||||
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
const secretLatest: GCPLatestSecretVersionAccess = (
|
||||
await standardRequest.get(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
)
|
||||
).data;
|
||||
|
||||
|
||||
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
||||
}
|
||||
|
||||
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if (!(key in res)) {
|
||||
// case: create secret
|
||||
@ -423,11 +433,14 @@ const syncSecretsGCPSecretManager = async ({
|
||||
replication: {
|
||||
automatic: {}
|
||||
},
|
||||
...(integration.metadata.secretGCPLabel ? {
|
||||
labels: {
|
||||
[integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue
|
||||
}
|
||||
} : {})
|
||||
...(integration.metadata.secretGCPLabel
|
||||
? {
|
||||
labels: {
|
||||
[integration.metadata.secretGCPLabel.labelName]:
|
||||
integration.metadata.secretGCPLabel.labelValue
|
||||
}
|
||||
}
|
||||
: {})
|
||||
},
|
||||
{
|
||||
params: {
|
||||
@ -439,7 +452,7 @@ const syncSecretsGCPSecretManager = async ({
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
||||
{
|
||||
@ -456,7 +469,7 @@ const syncSecretsGCPSecretManager = async ({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for await (const key of Object.keys(res)) {
|
||||
if (!(key in secrets)) {
|
||||
// case: delete secret
|
||||
@ -489,7 +502,7 @@ const syncSecretsGCPSecretManager = async ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
|
||||
@ -729,15 +742,12 @@ const syncSecretsAWSParameterStore = async ({
|
||||
} = {};
|
||||
|
||||
if (parameterList) {
|
||||
awsParameterStoreSecretsObj = parameterList.reduce(
|
||||
(obj: any, secret: any) => {
|
||||
return ({
|
||||
...obj,
|
||||
[secret.Name.substring(integration.path.length)]: secret
|
||||
});
|
||||
},
|
||||
{}
|
||||
);
|
||||
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => {
|
||||
return {
|
||||
...obj,
|
||||
[secret.Name.substring(integration.path.length)]: secret
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Identify secrets to create
|
||||
@ -1336,11 +1346,13 @@ const syncSecretsNetlify = async ({
|
||||
const syncSecretsGitHub = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
accessToken,
|
||||
appendices
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessToken: string;
|
||||
appendices?: { prefix: string, suffix: string };
|
||||
}) => {
|
||||
interface GitHubRepoKey {
|
||||
key_id: string;
|
||||
@ -1370,7 +1382,7 @@ const syncSecretsGitHub = async ({
|
||||
).data;
|
||||
|
||||
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
|
||||
const encryptedSecrets: GitHubSecretRes = (
|
||||
let encryptedSecrets: GitHubSecretRes = (
|
||||
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
|
||||
owner: integration.owner,
|
||||
repo: integration.app
|
||||
@ -1383,6 +1395,15 @@ const syncSecretsGitHub = async ({
|
||||
{}
|
||||
);
|
||||
|
||||
encryptedSecrets = Object.keys(encryptedSecrets).reduce((result: {
|
||||
[key: string]: GitHubSecret;
|
||||
}, key) => {
|
||||
if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) {
|
||||
result[key] = encryptedSecrets[key];
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
Object.keys(encryptedSecrets).map(async (key) => {
|
||||
if (!(key in secrets)) {
|
||||
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
||||
@ -1869,8 +1890,10 @@ const syncSecretsGitLab = async ({
|
||||
value: string;
|
||||
environment_scope: string;
|
||||
}
|
||||
|
||||
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL;
|
||||
|
||||
const gitLabApiUrl = integrationAuth.url
|
||||
? `${integrationAuth.url}/api`
|
||||
: INTEGRATION_GITLAB_API_URL;
|
||||
|
||||
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
|
||||
const headers = {
|
||||
@ -1880,7 +1903,9 @@ const syncSecretsGitLab = async ({
|
||||
};
|
||||
|
||||
let allEnvVariables: GitLabSecret[] = [];
|
||||
let url: string | null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
|
||||
let url:
|
||||
| string
|
||||
| null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
|
||||
|
||||
while (url) {
|
||||
const response: any = await standardRequest.get(url, { headers });
|
||||
@ -1901,23 +1926,27 @@ const syncSecretsGitLab = async ({
|
||||
|
||||
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
|
||||
const getSecretsRes: GitLabSecret[] = allEnvVariables
|
||||
.filter(
|
||||
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
|
||||
)
|
||||
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
|
||||
.filter((gitLabSecret) => {
|
||||
let isValid = true;
|
||||
|
||||
if (integration.metadata.secretPrefix && !gitLabSecret.key.startsWith(integration.metadata.secretPrefix)) {
|
||||
if (
|
||||
integration.metadata.secretPrefix &&
|
||||
!gitLabSecret.key.startsWith(integration.metadata.secretPrefix)
|
||||
) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) {
|
||||
if (
|
||||
integration.metadata.secretSuffix &&
|
||||
!gitLabSecret.key.endsWith(integration.metadata.secretSuffix)
|
||||
) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
|
||||
return isValid;
|
||||
});
|
||||
|
||||
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
|
||||
if (!existingSecret) {
|
||||
@ -2060,13 +2089,15 @@ const syncSecretsSupabase = async ({
|
||||
const syncSecretsCheckly = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
accessToken,
|
||||
appendices
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessToken: string;
|
||||
appendices?: { prefix: string, suffix: string };
|
||||
}) => {
|
||||
const getSecretsRes = (
|
||||
let getSecretsRes = (
|
||||
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
@ -2082,6 +2113,15 @@ const syncSecretsCheckly = async ({
|
||||
{}
|
||||
);
|
||||
|
||||
getSecretsRes = Object.keys(getSecretsRes).reduce((result: {
|
||||
[key: string]: string;
|
||||
}, key) => {
|
||||
if ((appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && (appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)) {
|
||||
result[key] = getSecretsRes[key];
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
// add secrets
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if (!(key in getSecretsRes)) {
|
||||
@ -2371,41 +2411,43 @@ const syncSecretsTeamCity = async ({
|
||||
|
||||
if (integration.targetEnvironment && integration.targetEnvironmentId) {
|
||||
// case: sync to specific build-config in TeamCity project
|
||||
const res = (await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
|
||||
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
))
|
||||
.data
|
||||
.property
|
||||
.filter((parameter) => !parameter.inherited)
|
||||
.reduce((obj: any, secret: TeamCitySecret) => {
|
||||
const secretName = secret.name.replace(/^env\./, "");
|
||||
return {
|
||||
...obj,
|
||||
[secretName]: secret.value
|
||||
};
|
||||
}, {});
|
||||
|
||||
const res = (
|
||||
await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
|
||||
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data.property
|
||||
.filter((parameter) => !parameter.inherited)
|
||||
.reduce((obj: any, secret: TeamCitySecret) => {
|
||||
const secretName = secret.name.replace(/^env\./, "");
|
||||
return {
|
||||
...obj,
|
||||
[secretName]: secret.value
|
||||
};
|
||||
}, {});
|
||||
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
|
||||
// case: secret does not exist in TeamCity or secret value has changed
|
||||
// -> create/update secret
|
||||
await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
name:`env.${key}`,
|
||||
value: secrets[key].value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
await standardRequest.post(
|
||||
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
name: `env.${key}`,
|
||||
value: secrets[key].value
|
||||
},
|
||||
});
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3034,4 +3076,4 @@ const syncSecretsNorthflank = async ({
|
||||
);
|
||||
};
|
||||
|
||||
export { syncSecrets };
|
||||
export { syncSecrets };
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IServiceTokenData,
|
||||
IServiceTokenDataV3,
|
||||
IUser,
|
||||
} from "../../models";
|
||||
import {
|
||||
ServiceActor,
|
||||
ServiceActorV3,
|
||||
UserActor,
|
||||
UserAgentType
|
||||
} from "../../ee/models";
|
||||
@ -21,6 +23,11 @@ export interface UserAuthData extends BaseAuthData {
|
||||
authPayload: IUser;
|
||||
}
|
||||
|
||||
export interface ServiceTokenV3AuthData extends BaseAuthData {
|
||||
actor: ServiceActorV3;
|
||||
authPayload: IServiceTokenDataV3;
|
||||
}
|
||||
|
||||
export interface ServiceTokenAuthData extends BaseAuthData {
|
||||
actor: ServiceActor;
|
||||
authPayload: IServiceTokenData;
|
||||
@ -28,4 +35,5 @@ export interface ServiceTokenAuthData extends BaseAuthData {
|
||||
|
||||
export type AuthData =
|
||||
| UserAuthData
|
||||
| ServiceTokenV3AuthData
|
||||
| ServiceTokenAuthData;
|
@ -16,6 +16,7 @@ export interface CreateSecretParams {
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
secretPath: string;
|
||||
metadata?: {
|
||||
source?: string;
|
||||
@ -42,6 +43,11 @@ export interface GetSecretParams {
|
||||
|
||||
export interface UpdateSecretParams {
|
||||
secretName: string;
|
||||
newSecretName?: string;
|
||||
secretId?: string;
|
||||
secretKeyCiphertext?: string;
|
||||
secretKeyIV?: string;
|
||||
secretKeyTag?: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
type: "shared" | "personal";
|
||||
@ -50,13 +56,73 @@ export interface UpdateSecretParams {
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretPath: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface DeleteSecretParams {
|
||||
secretName: string;
|
||||
secretId?: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
type: "shared" | "personal";
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
export interface CreateSecretBatchParams {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
secrets: Array<{
|
||||
secretName: string;
|
||||
type: "shared" | "personal";
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
metadata?: {
|
||||
source?: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UpdateSecretBatchParams {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
secrets: Array<{
|
||||
secretName: string;
|
||||
type: "shared" | "personal";
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
tags?: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DeleteSecretBatchParams {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
secrets: Array<{
|
||||
secretName: string;
|
||||
type: "shared" | "personal";
|
||||
}>;
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ import { ErrorRequestHandler } from "express";
|
||||
import { TokenExpiredError } from "jsonwebtoken";
|
||||
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
|
||||
import { getLogger } from "../utils/logger";
|
||||
import RequestError, { LogLevel } from "../utils/requestError";
|
||||
import { getNodeEnv } from "../config";
|
||||
import RequestError from "../utils/requestError";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const requestErrorHandler: ErrorRequestHandler = async (
|
||||
error: RequestError | Error,
|
||||
@ -14,41 +14,37 @@ export const requestErrorHandler: ErrorRequestHandler = async (
|
||||
) => {
|
||||
if (res.headersSent) return next();
|
||||
|
||||
if (await getNodeEnv() !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
|
||||
if (error instanceof TokenExpiredError) {
|
||||
error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" });
|
||||
} else if (!(error instanceof RequestError)) {
|
||||
error = InternalServerError({
|
||||
context: { exception: error.message },
|
||||
stack: error.stack,
|
||||
});
|
||||
const logAndCaptureException = async (error: RequestError) => {
|
||||
(await getLogger("backend-main")).log(
|
||||
(<RequestError>error).levelName.toLowerCase(),
|
||||
(<RequestError>error).message
|
||||
`${error.stack}\n${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
//* Set Sentry user identification if req.user is populated
|
||||
if (req.user !== undefined && req.user !== null) {
|
||||
Sentry.setUser({ email: (req.user as any).email });
|
||||
}
|
||||
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
|
||||
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
|
||||
if (
|
||||
[LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes(
|
||||
(<RequestError>error).level
|
||||
)
|
||||
) {
|
||||
//* Set Sentry user identification if req.user is populated
|
||||
if (req.user !== undefined && req.user !== null) {
|
||||
Sentry.setUser({ email: (req.user as any).email });
|
||||
}
|
||||
|
||||
Sentry.captureException(error);
|
||||
};
|
||||
|
||||
if (error instanceof RequestError) {
|
||||
if (error instanceof TokenExpiredError) {
|
||||
error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" });
|
||||
}
|
||||
await logAndCaptureException((<RequestError>error));
|
||||
} else {
|
||||
if (error instanceof ForbiddenError) {
|
||||
error = UnauthorizedRequestError({ context: { exception: error.message }, stack: error.stack })
|
||||
} else {
|
||||
error = InternalServerError({ context: { exception: error.message }, stack: error.stack });
|
||||
}
|
||||
|
||||
await logAndCaptureException((<RequestError>error));
|
||||
}
|
||||
|
||||
res
|
||||
.status((<RequestError>error).statusCode)
|
||||
.json((<RequestError>error).format(req));
|
||||
delete (<any>error).stacktrace // remove stack trace from being sent to client
|
||||
res.status((<RequestError>error).statusCode).json(error);
|
||||
|
||||
next();
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { NextFunction, Request, Response } from "express";
|
||||
import {
|
||||
getAuthAPIKeyPayload,
|
||||
getAuthSTDPayload,
|
||||
getAuthSTDV3Payload,
|
||||
getAuthUserPayload,
|
||||
validateAuthMode,
|
||||
} from "../helpers/auth";
|
||||
@ -49,6 +50,12 @@ const requireAuth = ({
|
||||
});
|
||||
req.serviceTokenData = authData.authPayload;
|
||||
break;
|
||||
case AuthMode.SERVICE_TOKEN_V3:
|
||||
authData = await getAuthSTDV3Payload({
|
||||
req,
|
||||
authTokenValue
|
||||
});
|
||||
break;
|
||||
case AuthMode.API_KEY:
|
||||
authData = await getAuthAPIKeyPayload({
|
||||
req,
|
||||
@ -61,9 +68,7 @@ const requireAuth = ({
|
||||
req,
|
||||
authTokenValue
|
||||
});
|
||||
// authPayload = authUserPayload.user;
|
||||
req.user = authData.authPayload;
|
||||
// req.tokenVersionId = authUserPayload.tokenVersionId; // TODO
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -14,17 +14,19 @@ export * from "./tag";
|
||||
export * from "./folder";
|
||||
export * from "./secretImports";
|
||||
export * from "./secretBlindIndexData";
|
||||
export * from "./serviceToken";
|
||||
export * from "./serviceAccount";
|
||||
export * from "./serviceAccountKey";
|
||||
export * from "./serviceAccountOrganizationPermission";
|
||||
export * from "./serviceAccountWorkspacePermission";
|
||||
export * from "./serviceToken"; // TODO: deprecate
|
||||
export * from "./serviceAccount"; // TODO: deprecate
|
||||
export * from "./serviceAccountKey"; // TODO: deprecate
|
||||
export * from "./serviceAccountOrganizationPermission"; // TODO: deprecate
|
||||
export * from "./serviceAccountWorkspacePermission"; // TODO: deprecate
|
||||
export * from "./tokenData";
|
||||
export * from "./user";
|
||||
export * from "./userAction";
|
||||
export * from "./workspace";
|
||||
export * from "./serviceTokenData";
|
||||
export * from "./serviceTokenData"; // TODO: deprecate
|
||||
export * from "./apiKeyData";
|
||||
export * from "./loginSRPDetail";
|
||||
export * from "./tokenVersion";
|
||||
export * from "./webhooks";
|
||||
export * from "./webhooks";
|
||||
export * from "./serviceTokenDataV3";
|
||||
export * from "./serviceTokenDataV3Key";
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../variables";
|
||||
|
||||
export interface ISecret {
|
||||
@ -12,7 +12,7 @@ export interface ISecret {
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
user?: Types.ObjectId;
|
||||
environment: string;
|
||||
secretBlindIndex?: string;
|
||||
secretKeyCiphertext: string;
|
||||
@ -27,13 +27,14 @@ export interface ISecret {
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
algorithm: "aes-256-gcm";
|
||||
keyEncoding: "utf8" | "base64";
|
||||
tags?: string[];
|
||||
folder?: string;
|
||||
metadata?: {
|
||||
[key: string]: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
@ -41,108 +42,112 @@ const secretSchema = new Schema<ISecret>(
|
||||
version: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 1,
|
||||
default: 1
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
ref: "User"
|
||||
},
|
||||
tags: {
|
||||
ref: "Tag",
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: [],
|
||||
default: []
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretBlindIndex: {
|
||||
type: String,
|
||||
select: false,
|
||||
select: false
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
type: String
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
type: String
|
||||
},
|
||||
secretCommentCiphertext: {
|
||||
type: String,
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
secretCommentIV: {
|
||||
type: String, // symmetric
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
secretCommentTag: {
|
||||
type: String, // symmetric
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
secretCommentHash: {
|
||||
type: String,
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
skipMultilineEncoding: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
enum: [ALGORITHM_AES_256_GCM],
|
||||
required: true,
|
||||
default: ALGORITHM_AES_256_GCM,
|
||||
default: ALGORITHM_AES_256_GCM
|
||||
},
|
||||
keyEncoding: {
|
||||
type: String,
|
||||
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||
required: true,
|
||||
default: ENCODING_SCHEME_UTF8,
|
||||
default: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
default: "root",
|
||||
default: "root"
|
||||
},
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
secretSchema.index({ tags: 1 }, { background: true });
|
||||
|
||||
export const Secret = model<ISecret>("Secret", secretSchema);
|
||||
export const Secret = model<ISecret>("Secret", secretSchema);
|
||||
|
@ -1,81 +0,0 @@
|
||||
import mongoose, { Schema, model } from "mongoose";
|
||||
import { ISecret, Secret } from "./secret";
|
||||
|
||||
interface ISecretApprovalRequest {
|
||||
secret: mongoose.Types.ObjectId;
|
||||
requestedChanges: ISecret;
|
||||
requestedBy: mongoose.Types.ObjectId;
|
||||
approvers: IApprover[];
|
||||
status: ApprovalStatus;
|
||||
timestamp: Date;
|
||||
requestType: RequestType;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
interface IApprover {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
status: ApprovalStatus;
|
||||
}
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
export enum RequestType {
|
||||
UPDATE = "update",
|
||||
DELETE = "delete",
|
||||
CREATE = "create"
|
||||
}
|
||||
|
||||
const approverSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [ApprovalStatus],
|
||||
default: ApprovalStatus.PENDING,
|
||||
},
|
||||
});
|
||||
|
||||
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||
{
|
||||
secret: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Secret",
|
||||
},
|
||||
requestedChanges: Secret,
|
||||
requestedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
},
|
||||
approvers: [approverSchema],
|
||||
status: {
|
||||
type: String,
|
||||
enum: ApprovalStatus,
|
||||
default: ApprovalStatus.PENDING,
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
requestType: {
|
||||
type: String,
|
||||
enum: RequestType,
|
||||
required: true,
|
||||
},
|
||||
requestId: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretApprovalRequest = model<ISecretApprovalRequest>("SecretApprovalRequest", secretApprovalRequestSchema);
|
@ -1,3 +1,4 @@
|
||||
// TODO: deprecate
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
export interface IServiceToken {
|
||||
_id: Types.ObjectId;
|
||||
|
@ -1,3 +1,4 @@
|
||||
// TODO: deprecate
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface IServiceTokenData extends Document {
|
||||
|
129
backend/src/models/serviceTokenDataV3.ts
Normal file
129
backend/src/models/serviceTokenDataV3.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
import { IPType } from "../ee/models";
|
||||
|
||||
export enum Permission {
|
||||
READ = "read",
|
||||
WRITE = "write"
|
||||
}
|
||||
|
||||
export interface IServiceTokenV3Scope {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
export interface IServiceTokenV3TrustedIp {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
prefix: number;
|
||||
}
|
||||
|
||||
export interface IServiceTokenDataV3 extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
publicKey: string;
|
||||
isActive: boolean;
|
||||
lastUsed?: Date;
|
||||
usageCount: number;
|
||||
expiresAt?: Date;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
|
||||
const serviceTokenDataV3Schema = new Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true
|
||||
},
|
||||
publicKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
required: false
|
||||
},
|
||||
usageCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
required: false,
|
||||
expires: 0
|
||||
},
|
||||
scopes: {
|
||||
type: [
|
||||
{
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
default: "/",
|
||||
required: true
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
enum: [Permission.READ, Permission.WRITE],
|
||||
default: [Permission.READ],
|
||||
required: true
|
||||
}
|
||||
}
|
||||
],
|
||||
required: true
|
||||
},
|
||||
trustedIps: {
|
||||
type: [
|
||||
{
|
||||
ipAddress: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [
|
||||
IPType.IPV4,
|
||||
IPType.IPV6
|
||||
],
|
||||
required: true
|
||||
},
|
||||
prefix: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
],
|
||||
default: [{
|
||||
ipAddress: "0.0.0.0",
|
||||
type: IPType.IPV4.toString(),
|
||||
prefix: 0
|
||||
}],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const ServiceTokenDataV3 = model<IServiceTokenDataV3>("ServiceTokenDataV3", serviceTokenDataV3Schema);
|
43
backend/src/models/serviceTokenDataV3Key.ts
Normal file
43
backend/src/models/serviceTokenDataV3Key.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface IServiceTokenDataV3Key extends Document {
|
||||
_id: Types.ObjectId;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
sender: Types.ObjectId;
|
||||
serviceTokenData: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
}
|
||||
|
||||
const serviceTokenDataV3KeySchema = new Schema(
|
||||
{
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
nonce: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
sender: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true
|
||||
},
|
||||
serviceTokenData: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "ServiceTokenDataV3",
|
||||
required: true,
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const ServiceTokenDataV3Key = model<IServiceTokenDataV3Key>("ServiceTokenDataV3Key", serviceTokenDataV3KeySchema);
|
@ -4,6 +4,7 @@ export enum AuthMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
GITHUB = "github",
|
||||
GITLAB = "gitlab",
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml",
|
||||
JUMPCLOUD_SAML = "jumpcloud-saml",
|
||||
|
@ -40,9 +40,9 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
|
||||
const prefix = (integration.metadata?.secretPrefix || "");
|
||||
const suffix = (integration.metadata?.secretSuffix || "");
|
||||
const newKey = prefix + key + suffix;
|
||||
|
||||
|
||||
suffixedSecrets[newKey] = secrets[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||
@ -60,13 +60,14 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
|
||||
integrationAuth,
|
||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
|
||||
accessId: access.accessId === undefined ? null : access.accessId,
|
||||
accessToken: access.accessToken
|
||||
accessToken: access.accessToken,
|
||||
appendices: { prefix: integration.metadata?.secretPrefix || "", suffix: integration.metadata?.secretSuffix || "" }
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
syncSecretsToThirdPartyServices.on("error", (error) => {
|
||||
console.log("QUEUE ERROR:", error) // eslint-disable-line
|
||||
// console.log("QUEUE ERROR:", error) // eslint-disable-line
|
||||
})
|
||||
|
||||
export const syncSecretsToActiveIntegrationsQueue = (jobDetails: TSyncSecretsToThirdPartyServices) => {
|
||||
|
@ -2,7 +2,7 @@ import Queue, { Job } from "bull";
|
||||
import { ProbotOctokit } from "probot"
|
||||
import TelemetryService from "../../services/TelemetryService";
|
||||
import { sendMail } from "../../helpers";
|
||||
import GitRisks from "../../ee/models/gitRisks";
|
||||
import { GitRisks } from "../../ee/models";
|
||||
import { MembershipOrg, User } from "../../models";
|
||||
import { ADMIN } from "../../variables";
|
||||
import { convertKeysToLowercase, scanFullRepoContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
|
||||
@ -13,7 +13,7 @@ export const githubFullRepositorySecretScan = new Queue("github-full-repository-
|
||||
|
||||
type TScanPushEventQueueDetails = {
|
||||
organizationId: string,
|
||||
installationId: number,
|
||||
installationId: string,
|
||||
repository: {
|
||||
id: number,
|
||||
fullName: string,
|
||||
@ -30,7 +30,8 @@ githubFullRepositorySecretScan.process(async (job: Job, done: Queue.DoneCallback
|
||||
installationId: installationId
|
||||
},
|
||||
});
|
||||
const findings: SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId, repository.fullName)
|
||||
|
||||
const findings: SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId as any, repository.fullName)
|
||||
for (const finding of findings) {
|
||||
await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint },
|
||||
{
|
||||
|
@ -3,7 +3,7 @@ import { ProbotOctokit } from "probot"
|
||||
import { Commit } from "@octokit/webhooks-types";
|
||||
import TelemetryService from "../../services/TelemetryService";
|
||||
import { sendMail } from "../../helpers";
|
||||
import GitRisks from "../../ee/models/gitRisks";
|
||||
import { GitRisks } from "../../ee/models";
|
||||
import { MembershipOrg, User } from "../../models";
|
||||
import { ADMIN } from "../../variables";
|
||||
import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
|
||||
|
@ -11,6 +11,7 @@ import key from "./key";
|
||||
import inviteOrg from "./inviteOrg";
|
||||
import secret from "./secret";
|
||||
import serviceToken from "./serviceToken";
|
||||
import sso from "./sso";
|
||||
import password from "./password";
|
||||
import integration from "./integration";
|
||||
import integrationAuth from "./integrationAuth";
|
||||
@ -37,5 +38,6 @@ export {
|
||||
integrationAuth,
|
||||
secretsFolder,
|
||||
webhooks,
|
||||
secretImps
|
||||
secretImps,
|
||||
sso
|
||||
};
|
||||
|
@ -13,15 +13,6 @@ router.get(
|
||||
organizationController.getOrganizations
|
||||
);
|
||||
|
||||
router.post(
|
||||
// not used on frontend
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
organizationController.createOrganization
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:organizationId",
|
||||
requireAuth({
|
||||
|
@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
secretImpsController.createSecretImp
|
||||
);
|
||||
@ -15,7 +15,7 @@ router.post(
|
||||
router.put(
|
||||
"/:id",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
secretImpsController.updateSecretImport
|
||||
);
|
||||
@ -23,7 +23,7 @@ router.put(
|
||||
router.delete(
|
||||
"/:id",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
secretImpsController.deleteSecretImport
|
||||
);
|
||||
@ -31,7 +31,7 @@ router.delete(
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
secretImpsController.getSecretImports
|
||||
);
|
||||
@ -39,7 +39,7 @@ router.get(
|
||||
router.get(
|
||||
"/secrets",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
secretImpsController.getAllSecretsFromImport
|
||||
);
|
||||
|
@ -12,23 +12,23 @@ import { AuthMode } from "../../variables";
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
createFolder
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:folderId",
|
||||
"/:folderName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
updateFolderById
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:folderId",
|
||||
"/:folderName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
deleteFolder
|
||||
);
|
||||
@ -36,7 +36,7 @@ router.delete(
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.API_KEY]
|
||||
}),
|
||||
getFolders
|
||||
);
|
||||
|
72
backend/src/routes/v1/sso.ts
Normal file
72
backend/src/routes/v1/sso.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import passport from "passport";
|
||||
import { authLimiter } from "../../helpers/rateLimiter";
|
||||
import { ssoController } from "../../ee/controllers/v1";
|
||||
|
||||
router.get("/redirect/google", authLimiter, (req, res, next) => {
|
||||
passport.authenticate("google", {
|
||||
scope: ["profile", "email"],
|
||||
session: false,
|
||||
...(req.query.callback_port
|
||||
? {
|
||||
state: req.query.callback_port as string
|
||||
}
|
||||
: {})
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
router.get(
|
||||
"/google",
|
||||
passport.authenticate("google", {
|
||||
failureRedirect: "/login/provider/error",
|
||||
session: false
|
||||
}),
|
||||
ssoController.redirectSSO
|
||||
);
|
||||
|
||||
router.get("/redirect/github", authLimiter, (req, res, next) => {
|
||||
passport.authenticate("github", {
|
||||
session: false,
|
||||
...(req.query.callback_port
|
||||
? {
|
||||
state: req.query.callback_port as string
|
||||
}
|
||||
: {})
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
router.get(
|
||||
"/github",
|
||||
authLimiter,
|
||||
passport.authenticate("github", {
|
||||
failureRedirect: "/login/provider/error",
|
||||
session: false
|
||||
}),
|
||||
ssoController.redirectSSO
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/redirect/gitlab",
|
||||
authLimiter,
|
||||
(req, res, next) => {
|
||||
passport.authenticate("gitlab", {
|
||||
session: false,
|
||||
...(req.query.callback_port ? {
|
||||
state: req.query.callback_port as string
|
||||
} : {})
|
||||
})(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/gitlab",
|
||||
authLimiter,
|
||||
passport.authenticate("gitlab", {
|
||||
failureRedirect: "/login/provider/error",
|
||||
session: false
|
||||
}),
|
||||
ssoController.redirectSSO
|
||||
);
|
||||
|
||||
export default router;
|
@ -54,4 +54,20 @@ router.get(
|
||||
organizationsController.getOrganizationServiceAccounts
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
organizationsController.createOrganization
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:organizationId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
organizationsController.deleteOrganizationById
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
@ -4,14 +4,6 @@ import { requireAuth } from "../../middleware";
|
||||
import { usersController } from "../../controllers/v2";
|
||||
import { AuthMode } from "../../variables";
|
||||
|
||||
router.get(
|
||||
"/me",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
usersController.getMe
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/me/mfa",
|
||||
requireAuth({
|
||||
@ -29,11 +21,11 @@ router.patch(
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/me/auth-methods",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
usersController.updateAuthMethods
|
||||
"/me/auth-methods",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
|
||||
}),
|
||||
usersController.updateAuthMethods,
|
||||
);
|
||||
|
||||
router.get(
|
||||
@ -84,4 +76,20 @@ router.delete(
|
||||
usersController.deleteMySessions
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/me",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
usersController.getMe
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/me",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
usersController.deleteMe
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
@ -7,5 +7,5 @@ export {
|
||||
auth,
|
||||
secrets,
|
||||
signup,
|
||||
workspaces,
|
||||
workspaces
|
||||
}
|
||||
|
@ -1,19 +1,13 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireBlindIndicesEnabled,
|
||||
requireE2EEOff
|
||||
} from "../../middleware";
|
||||
import { requireAuth, requireBlindIndicesEnabled, requireE2EEOff } from "../../middleware";
|
||||
import { secretsController } from "../../controllers/v3";
|
||||
import {
|
||||
AuthMode
|
||||
} from "../../variables";
|
||||
import { AuthMode } from "../../variables";
|
||||
|
||||
router.get(
|
||||
"/raw",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
secretsController.getSecretsRaw
|
||||
);
|
||||
@ -21,7 +15,7 @@ router.get(
|
||||
router.get(
|
||||
"/raw/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "query"
|
||||
@ -35,7 +29,7 @@ router.get(
|
||||
router.post(
|
||||
"/raw/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@ -49,7 +43,7 @@ router.post(
|
||||
router.patch(
|
||||
"/raw/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@ -63,7 +57,7 @@ router.patch(
|
||||
router.delete(
|
||||
"/raw/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@ -77,7 +71,7 @@ router.delete(
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "query"
|
||||
@ -85,10 +79,44 @@ router.get(
|
||||
secretsController.getSecrets
|
||||
);
|
||||
|
||||
// akhilmhdh: dont put batch router below the individual operation as those have arbitory name as params
|
||||
router.post(
|
||||
"/batch",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
}),
|
||||
secretsController.createSecretByNameBatch
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/batch",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
}),
|
||||
secretsController.updateSecretByNameBatch
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/batch",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
}),
|
||||
secretsController.deleteSecretByNameBatch
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@ -99,7 +127,7 @@ router.post(
|
||||
router.get(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "query"
|
||||
@ -110,7 +138,7 @@ router.get(
|
||||
router.patch(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@ -121,7 +149,7 @@ router.patch(
|
||||
router.delete(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
|
@ -34,4 +34,12 @@ router.post(
|
||||
|
||||
// --
|
||||
|
||||
router.get(
|
||||
"/:workspaceId/service-token",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
workspacesController.getWorkspaceServiceTokenData
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
@ -5,7 +5,7 @@ import path from "path";
|
||||
|
||||
type TAppendFolderDTO = {
|
||||
folderName: string;
|
||||
parentFolderId?: string;
|
||||
directory: string;
|
||||
};
|
||||
|
||||
type TRenameFolderDTO = {
|
||||
@ -50,9 +50,8 @@ export const folderBfsTraversal = async (
|
||||
// bfs and then append to the folder
|
||||
const appendChild = (folders: TFolderSchema, folderName: string) => {
|
||||
const folder = folders.children.find(({ name }) => name === folderName);
|
||||
if (folder) {
|
||||
throw new Error("Folder already exists");
|
||||
}
|
||||
if (folder) return { folder, hasCreated: false };
|
||||
|
||||
const id = generateFolderId();
|
||||
folders.version += 1;
|
||||
folders.children.push({
|
||||
@ -61,24 +60,32 @@ const appendChild = (folders: TFolderSchema, folderName: string) => {
|
||||
children: [],
|
||||
version: 1
|
||||
});
|
||||
return { id, name: folderName };
|
||||
// last element that is the new one
|
||||
return { folder: folders.children[folders.children.length - 1], hasCreated: true };
|
||||
};
|
||||
|
||||
// root of append child wrapper
|
||||
export const appendFolder = (
|
||||
folders: TFolderSchema,
|
||||
{ folderName, parentFolderId }: TAppendFolderDTO
|
||||
) => {
|
||||
const isRoot = !parentFolderId;
|
||||
{ folderName, directory }: TAppendFolderDTO
|
||||
): { parent: TFolderSchema; child: TFolderSchema; hasCreated?: boolean } => {
|
||||
if (directory === "/") {
|
||||
const newFolder = appendChild(folders, folderName);
|
||||
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
|
||||
}
|
||||
|
||||
if (isRoot) {
|
||||
return appendChild(folders, folderName);
|
||||
const segments = directory.split("/").filter(Boolean);
|
||||
const segment = segments.shift();
|
||||
if (segment) {
|
||||
const nestedFolders = appendChild(folders, segment);
|
||||
return appendFolder(nestedFolders.folder, {
|
||||
folderName,
|
||||
directory: path.join("/", ...segments)
|
||||
});
|
||||
}
|
||||
const folder = searchByFolderId(folders, parentFolderId);
|
||||
if (!folder) {
|
||||
throw new Error("Parent Folder not found");
|
||||
}
|
||||
return appendChild(folder, folderName);
|
||||
|
||||
const newFolder = appendChild(folders, folderName);
|
||||
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
|
||||
};
|
||||
|
||||
export const renameFolder = (
|
||||
|
@ -108,14 +108,6 @@ export const getAllImportedSecrets = async (
|
||||
type: "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "tags", // note this is the name of the collection in the database, not the Mongoose model name
|
||||
localField: "tags",
|
||||
foreignField: "_id",
|
||||
as: "tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
|
@ -1,21 +1,27 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
CreateSecretParams,
|
||||
DeleteSecretParams,
|
||||
GetSecretParams,
|
||||
GetSecretsParams,
|
||||
UpdateSecretParams,
|
||||
CreateSecretBatchParams,
|
||||
CreateSecretParams,
|
||||
DeleteSecretBatchParams,
|
||||
DeleteSecretParams,
|
||||
GetSecretParams,
|
||||
GetSecretsParams,
|
||||
UpdateSecretBatchParams,
|
||||
UpdateSecretParams
|
||||
} from "../interfaces/services/SecretService";
|
||||
import {
|
||||
createSecretBlindIndexDataHelper,
|
||||
createSecretHelper,
|
||||
deleteSecretHelper,
|
||||
generateSecretBlindIndexHelper,
|
||||
generateSecretBlindIndexWithSaltHelper,
|
||||
getSecretBlindIndexSaltHelper,
|
||||
getSecretHelper,
|
||||
getSecretsHelper,
|
||||
updateSecretHelper,
|
||||
import {
|
||||
createSecretBatchHelper,
|
||||
createSecretBlindIndexDataHelper,
|
||||
createSecretHelper,
|
||||
deleteSecretBatchHelper,
|
||||
deleteSecretHelper,
|
||||
generateSecretBlindIndexHelper,
|
||||
generateSecretBlindIndexWithSaltHelper,
|
||||
getSecretBlindIndexSaltHelper,
|
||||
getSecretHelper,
|
||||
getSecretsHelper,
|
||||
updateSecretBatchHelper,
|
||||
updateSecretHelper
|
||||
} from "../helpers/secrets";
|
||||
|
||||
class SecretService {
|
||||
@ -26,13 +32,9 @@ class SecretService {
|
||||
* @param {Buffer} obj.salt - 16-byte random salt
|
||||
* @param {Types.ObjectId} obj.workspaceId
|
||||
*/
|
||||
static async createSecretBlindIndexData({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
static async createSecretBlindIndexData({ workspaceId }: { workspaceId: Types.ObjectId }) {
|
||||
return await createSecretBlindIndexDataHelper({
|
||||
workspaceId,
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
@ -42,13 +44,9 @@ class SecretService {
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
|
||||
* @returns
|
||||
*/
|
||||
static async getSecretBlindIndexSalt({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
static async getSecretBlindIndexSalt({ workspaceId }: { workspaceId: Types.ObjectId }) {
|
||||
return await getSecretBlindIndexSaltHelper({
|
||||
workspaceId,
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
@ -61,14 +59,14 @@ class SecretService {
|
||||
*/
|
||||
static async generateSecretBlindIndexWithSalt({
|
||||
secretName,
|
||||
salt,
|
||||
salt
|
||||
}: {
|
||||
secretName: string;
|
||||
salt: string;
|
||||
}) {
|
||||
return await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt,
|
||||
salt
|
||||
});
|
||||
}
|
||||
|
||||
@ -81,14 +79,14 @@ class SecretService {
|
||||
*/
|
||||
static async generateSecretBlindIndex({
|
||||
secretName,
|
||||
workspaceId,
|
||||
workspaceId
|
||||
}: {
|
||||
secretName: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId,
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
@ -163,6 +161,18 @@ class SecretService {
|
||||
static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
|
||||
return await deleteSecretHelper(deleteSecretParams);
|
||||
}
|
||||
|
||||
static async createSecretBatch(createSecretParams: CreateSecretBatchParams) {
|
||||
return await createSecretBatchHelper(createSecretParams);
|
||||
}
|
||||
|
||||
static async updateSecretBatch(updateSecretParams: UpdateSecretBatchParams) {
|
||||
return await updateSecretBatchHelper(updateSecretParams);
|
||||
}
|
||||
|
||||
static async deleteSecretBatch(deleteSecretParams: DeleteSecretBatchParams) {
|
||||
return await deleteSecretBatchHelper(deleteSecretParams);
|
||||
}
|
||||
}
|
||||
|
||||
export default SecretService;
|
||||
|
@ -8,21 +8,25 @@ import {
|
||||
Organization,
|
||||
ServiceAccount,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
User
|
||||
} from "../models";
|
||||
import { createToken } from "../helpers/auth";
|
||||
import {
|
||||
getClientIdGitHubLogin,
|
||||
getClientIdGitLabLogin,
|
||||
getClientIdGoogleLogin,
|
||||
getClientSecretGitHubLogin,
|
||||
getClientSecretGitLabLogin,
|
||||
getClientSecretGoogleLogin,
|
||||
getJwtProviderAuthLifetime,
|
||||
getJwtProviderAuthSecret,
|
||||
getSiteURL,
|
||||
getUrlGitLabLogin
|
||||
} from "../config";
|
||||
import { getSSOConfigHelper } from "../ee/helpers/organizations";
|
||||
import { InternalServerError, OrganizationNotFoundError } from "./errors";
|
||||
import { ACCEPTED, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables";
|
||||
import { getSiteURL } from "../config";
|
||||
import { standardRequest } from "../config/request";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@ -30,6 +34,8 @@ const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const GitHubStrategy = require("passport-github").Strategy;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const GitLabStrategy = require("passport-gitlab2").Strategy;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
|
||||
|
||||
/**
|
||||
@ -49,6 +55,10 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
return { serviceTokenDataId: authData.authPayload._id };
|
||||
}
|
||||
|
||||
if (authData.authPayload instanceof ServiceTokenDataV3) {
|
||||
return { serviceTokenDataId: authData.authPayload._id };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -57,7 +67,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
|
||||
* @returns
|
||||
*/
|
||||
const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
||||
|
||||
if (authData.authPayload instanceof User) {
|
||||
return { user: authData.authPayload._id };
|
||||
}
|
||||
@ -67,7 +76,11 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
||||
}
|
||||
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
return { user: authData.authPayload.user };0
|
||||
return { user: authData.authPayload.user };
|
||||
}
|
||||
|
||||
if (authData.authPayload instanceof ServiceTokenDataV3) {
|
||||
return { user: authData.authPayload.user };
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +89,9 @@ const initializePassport = async () => {
|
||||
const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
|
||||
const clientIdGitHubLogin = await getClientIdGitHubLogin();
|
||||
const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
|
||||
const urlGitLab = await getUrlGitLabLogin();
|
||||
const clientIdGitLabLogin = await getClientIdGitLabLogin();
|
||||
const clientSecretGitLabLogin = await getClientSecretGitLabLogin();
|
||||
|
||||
if (clientIdGoogleLogin && clientSecretGoogleLogin) {
|
||||
passport.use(new GoogleStrategy({
|
||||
@ -209,6 +225,60 @@ const initializePassport = async () => {
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if (urlGitLab && clientIdGitLabLogin && clientSecretGitLabLogin) {
|
||||
passport.use(new GitLabStrategy({
|
||||
passReqToCallback: true,
|
||||
clientID: clientIdGitLabLogin,
|
||||
clientSecret: clientSecretGitLabLogin,
|
||||
callbackURL: "/api/v1/sso/gitlab",
|
||||
baseURL: urlGitLab
|
||||
},
|
||||
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
|
||||
const email = profile.emails[0].value;
|
||||
|
||||
let user = await User.findOne({
|
||||
email
|
||||
}).select("+publicKey");
|
||||
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email: email,
|
||||
authMethods: [AuthMethod.GITLAB],
|
||||
firstName: profile.displayName,
|
||||
lastName: ""
|
||||
}).save();
|
||||
}
|
||||
|
||||
let isLinkingRequired = false;
|
||||
if (!user.authMethods.includes(AuthMethod.GITLAB)) {
|
||||
isLinkingRequired = true;
|
||||
}
|
||||
|
||||
const isUserCompleted = !!user.publicKey;
|
||||
const providerAuthToken = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
authMethod: AuthMethod.GITLAB,
|
||||
isUserCompleted,
|
||||
isLinkingRequired,
|
||||
...(req.query.state ? {
|
||||
callbackPort: req.query.state as string
|
||||
} : {})
|
||||
},
|
||||
expiresIn: await getJwtProviderAuthLifetime(),
|
||||
secret: await getJwtProviderAuthSecret(),
|
||||
});
|
||||
|
||||
req.isUserCompleted = isUserCompleted;
|
||||
req.providerAuthToken = providerAuthToken;
|
||||
return done(null, profile);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
passport.use("saml", new MultiSamlStrategy(
|
||||
{
|
||||
@ -221,8 +291,7 @@ const initializePassport = async () => {
|
||||
});
|
||||
|
||||
interface ISAMLConfig {
|
||||
path: string;
|
||||
callbackURL: string;
|
||||
callbackUrl: string;
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
cert: string;
|
||||
@ -231,8 +300,7 @@ const initializePassport = async () => {
|
||||
}
|
||||
|
||||
const samlConfig: ISAMLConfig = ({
|
||||
path: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
|
||||
callbackURL: `${await getSiteURL()}/api/v1/sso/saml2${ssoIdentifier}`,
|
||||
callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
|
||||
entryPoint: ssoConfig.entryPoint,
|
||||
issuer: ssoConfig.issuer,
|
||||
cert: ssoConfig.cert,
|
||||
@ -243,6 +311,12 @@ const initializePassport = async () => {
|
||||
samlConfig.wantAuthnResponseSigned = false;
|
||||
}
|
||||
|
||||
if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) {
|
||||
if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) {
|
||||
samlConfig.audience = `spn:${ssoConfig.issuer}`;
|
||||
}
|
||||
}
|
||||
|
||||
req.ssoConfig = ssoConfig;
|
||||
|
||||
done(null, samlConfig);
|
||||
@ -335,7 +409,7 @@ const initializePassport = async () => {
|
||||
authMethod: req.ssoConfig.authProvider,
|
||||
isUserCompleted,
|
||||
...(req.body.RelayState ? {
|
||||
callbackPort: req.body.RelayState as string
|
||||
callbackPort: JSON.parse(req.body.RelayState).callbackPort as string
|
||||
} : {})
|
||||
},
|
||||
expiresIn: await getJwtProviderAuthLifetime(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import net from "net";
|
||||
import { IPType } from "../../ee/models";
|
||||
import { InternalServerError } from "../errors";
|
||||
import { InternalServerError, UnauthorizedRequestError } from "../errors";
|
||||
|
||||
/**
|
||||
* Return details of IP [ip]:
|
||||
@ -98,4 +98,39 @@ export const isValidIpOrCidr = (ip: string): boolean => {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the IP address [ipAddress] against the trusted IPs [trustedIps].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.ipAddress - IP address to check
|
||||
* @param {Object[]} obj.trustedIps - IPs to trust in blocklist
|
||||
*/
|
||||
export const checkIPAgainstBlocklist = ({
|
||||
ipAddress,
|
||||
trustedIps
|
||||
}: {
|
||||
ipAddress: string;
|
||||
trustedIps: {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
prefix: number;
|
||||
}[]
|
||||
}) => {
|
||||
const blockList = new net.BlockList();
|
||||
|
||||
for (const trustedIp of trustedIps) {
|
||||
if (trustedIp.prefix !== undefined) {
|
||||
blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
|
||||
} else {
|
||||
blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
|
||||
}
|
||||
}
|
||||
|
||||
const { type } = extractIPDetails(ipAddress);
|
||||
const check = blockList.check(ipAddress, type);
|
||||
|
||||
if (!check) throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate"
|
||||
});
|
||||
}
|
||||
|
@ -12,6 +12,27 @@ export enum LogLevel {
|
||||
EMERGENCY = 600,
|
||||
}
|
||||
|
||||
export const mapToWinstonLogLevel = (customLogLevel: LogLevel): string => {
|
||||
switch (customLogLevel) {
|
||||
case LogLevel.DEBUG:
|
||||
return "debug";
|
||||
case LogLevel.INFO:
|
||||
return "info";
|
||||
case LogLevel.NOTICE:
|
||||
return "notice";
|
||||
case LogLevel.WARNING:
|
||||
return "warn";
|
||||
case LogLevel.ERROR:
|
||||
return "error";
|
||||
case LogLevel.CRITICAL:
|
||||
return "crit";
|
||||
case LogLevel.ALERT:
|
||||
return "alert";
|
||||
case LogLevel.EMERGENCY:
|
||||
return "emerg";
|
||||
}
|
||||
}
|
||||
|
||||
export type RequestErrorContext = {
|
||||
logLevel?: LogLevel,
|
||||
statusCode: number,
|
||||
@ -87,7 +108,8 @@ export default class RequestError extends Error{
|
||||
}, this.context)
|
||||
|
||||
//* Omit sensitive information from context that can leak internal workings of this program if user is not developer
|
||||
if(!(await getVerboseErrorOutput())){
|
||||
const verboseErrorOutput = await getVerboseErrorOutput();
|
||||
if (verboseErrorOutput !== undefined) {
|
||||
_context = this._omit(_context, [
|
||||
"stacktrace",
|
||||
"exception",
|
||||
@ -110,4 +132,4 @@ export default class RequestError extends Error{
|
||||
return formatObject
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,14 @@ import { Types } from "mongoose";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
|
||||
import { EESecretService } from "../../ee/services";
|
||||
import { redisClient } from "../../services/RedisService"
|
||||
import { IPType, ISecretVersion, SecretSnapshot, SecretVersion, TrustedIP } from "../../ee/models";
|
||||
import {
|
||||
IPType,
|
||||
ISecretVersion,
|
||||
Role,
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
TrustedIP
|
||||
} from "../../ee/models";
|
||||
import {
|
||||
AuthMethod,
|
||||
BackupPrivateKey,
|
||||
@ -34,14 +41,12 @@ import {
|
||||
MEMBER,
|
||||
OWNER
|
||||
} from "../../variables";
|
||||
|
||||
import { InternalServerError } from "../errors";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
memberProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import Role from "../../ee/models/role";
|
||||
|
||||
/**
|
||||
* Backfill secrets to ensure that they're all versioned and have
|
||||
|
@ -55,9 +55,6 @@ export const setup = async () => {
|
||||
// initializing global feature set
|
||||
await EELicenseService.initGlobalFeatureSet();
|
||||
|
||||
// initializing the database connection
|
||||
await DatabaseService.initDatabase(await getMongoURL());
|
||||
|
||||
await initializePassport();
|
||||
|
||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||
|
@ -5,28 +5,30 @@ export const CreateFolderV1 = z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
folderName: z.string().trim(),
|
||||
parentFolderId: z.string().trim().optional()
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
export const UpdateFolderV1 = z.object({
|
||||
params: z.object({
|
||||
folderId: z.string().trim()
|
||||
folderName: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
name: z.string().trim()
|
||||
name: z.string().trim(),
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteFolderV1 = z.object({
|
||||
params: z.object({
|
||||
folderId: z.string().trim()
|
||||
folderName: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim()
|
||||
environment: z.string().trim(),
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
@ -34,7 +36,6 @@ export const GetFoldersV1 = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
parentFolderId: z.string().trim().optional(),
|
||||
parentFolderPath: z.string().trim().optional()
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
@ -9,3 +9,4 @@ export * from "./organization";
|
||||
export * from "./secrets";
|
||||
export * from "./serviceAccount";
|
||||
export * from "./serviceTokenData";
|
||||
export * from "./serviceTokenDataV3";
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user