mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-20 22:32:28 +00:00
Compare commits
198 Commits
Author | SHA1 | Date | |
---|---|---|---|
3cd9241aee | |||
9ca544f680 | |||
98d84b6717 | |||
3bc7f2aa7c | |||
72b8dbda15 | |||
439e86d763 | |||
71fbf519ce | |||
d386f2702d | |||
986434d66a | |||
30d84ede41 | |||
87a3f9a03c | |||
64d1f252e2 | |||
092e4a55bd | |||
a00e6df59f | |||
189d24589e | |||
17bae52830 | |||
323701d432 | |||
593765cb24 | |||
fa60784a6b | |||
eb9a8e0285 | |||
d1f296b7e7 | |||
dc6d036d86 | |||
58aee0239f | |||
799a839940 | |||
0242707e33 | |||
9974f889f3 | |||
a8f38a5367 | |||
61318f28f7 | |||
036d32aeba | |||
d03eff4f46 | |||
29592a1e9e | |||
0f151fcd7a | |||
cbd8302afe | |||
6992c51e17 | |||
91f1090568 | |||
6c61aef526 | |||
b67abf94d4 | |||
f57f3e6475 | |||
d958341154 | |||
d579684d2f | |||
35466a7f4a | |||
95177074e3 | |||
1ac94ee940 | |||
dc76be3d22 | |||
a707fe1498 | |||
71f60f1589 | |||
47fd48b7b0 | |||
07c65ded40 | |||
84700308f5 | |||
9116bf3344 | |||
3ad3e19bcf | |||
861639de27 | |||
37ed27111a | |||
c527efad94 | |||
389f5c4f21 | |||
acaae0b82c | |||
b8f102493e | |||
286184ab48 | |||
c0f0d699b4 | |||
0b281a02d0 | |||
d7b046236b | |||
d9b7f69838 | |||
16d2746749 | |||
9ce4a52b8d | |||
0fab5d32f2 | |||
3fd5b521bb | |||
b8a750a31d | |||
e51046fe62 | |||
7fde55414a | |||
db639b1a89 | |||
fbe2297ed6 | |||
63a739d626 | |||
2212c351ca | |||
946fbe4716 | |||
1dbd121aa4 | |||
357d15b034 | |||
a3db20cacf | |||
0ae73e873f | |||
b8edcab0d5 | |||
be8a274e5a | |||
06f8826d67 | |||
97f77dcada | |||
e4d302b7e1 | |||
3eb2209eb8 | |||
e7c75b544d | |||
07e6eb88ea | |||
c81320c09d | |||
b10e28b9b5 | |||
5409bdb0cb | |||
35c6e1d668 | |||
d1467348d1 | |||
b1ccb93d85 | |||
68c3b508e3 | |||
1f68b8966d | |||
ef2da28cbe | |||
7fe706ad0d | |||
a686462392 | |||
878ca69f43 | |||
ea9e185a65 | |||
1394368a43 | |||
77b34467b9 | |||
ee7cf7920d | |||
5bc8046f3f | |||
1423d05b52 | |||
1d0f51bb42 | |||
aaa771a7b7 | |||
2f67025376 | |||
f1c52fe332 | |||
1a90f27d6a | |||
de1b75d99e | |||
295e93ac17 | |||
0c59007fa8 | |||
cbfd35e181 | |||
9b266309c2 | |||
cc46b575b7 | |||
08ab27cad8 | |||
387ef17038 | |||
b71ba35a22 | |||
c2a03e4e0c | |||
266d8b7775 | |||
52f234675a | |||
0b2ac0470d | |||
b1f62ffd35 | |||
556a646dce | |||
9762b580a5 | |||
9aa8bfa1a2 | |||
60a03cad98 | |||
b702f29c46 | |||
12e104e12a | |||
b6ce660a3c | |||
b03bd5fa08 | |||
6bd908f4cb | |||
518606425a | |||
ce7d411f29 | |||
933fed5da6 | |||
486aa139c2 | |||
e3bf2791ee | |||
f9e6ac2496 | |||
a55b271525 | |||
b6189a90f4 | |||
d2c77d9985 | |||
6ce12c71e1 | |||
8d53d2e4b1 | |||
bd5dad71d4 | |||
0bf8661350 | |||
35d23cf55c | |||
69b819e7c4 | |||
d870ecc62a | |||
c0a0252cf5 | |||
2f5186634c | |||
36525325fd | |||
a990a5ee7d | |||
f2372bb265 | |||
8c0046be87 | |||
556858d1a8 | |||
2b147fce6e | |||
553be71ddf | |||
9241020eb2 | |||
7e33f48a3b | |||
0312891f8b | |||
6de4eca4fc | |||
b0fb86a6ac | |||
96b254d7c3 | |||
3f1eaa8d42 | |||
3e56fe95d2 | |||
15553e972a | |||
47ab0b4a0f | |||
f3f6871d81 | |||
a438b8b91b | |||
498571b4fb | |||
89136aab24 | |||
eed6c75836 | |||
51368e6598 | |||
7e534629ff | |||
2c221dbb03 | |||
88ca056abb | |||
17133cd61b | |||
2bbea36ce8 | |||
5e03a54fa8 | |||
53273df51f | |||
a04fe00563 | |||
6afb276b35 | |||
cb60151c0e | |||
4c32f3dfd0 | |||
c0d7b4ea88 | |||
e6c631586a | |||
3e102fee3d | |||
9386efd7c4 | |||
d90affbe87 | |||
08dc4532f4 | |||
12a9b60cc5 | |||
4c79aadc22 | |||
a87dc2fcb9 | |||
098ae8533f | |||
9239b66b4b | |||
3715114232 | |||
5ef4e4cecb | |||
4f808a24bb |
.github
MakefileREADME.mdbackend
api-documentation.jsonpackage-lock.jsonpackage.json
src
app.ts
swagger.tscontrollers
v1
v2
ee
controllers/v1
helpers
models
routes/v1
helpers
integrations
middleware
models
routes
services
types/express
variables
cli/packages
docker-compose.dev.ymldocs
api-reference
cli/commands
getting-started/dashboard
images
activity-logs.pngemail-aws-ses-console.pngemail-aws-ses-user.pngpit-commits.pngpit-snapshot.pngpit-snapshots.pngsecret-versioning.png
mint.jsonself-hosting/configuration
frontend
components
analytics
basic
Error.tsxEventFilter.tsxLayout.tsxListbox.tsxToggle.tsx
buttons
dialog
AddProjectMemberDialog.jsAddServiceTokenDialog.jsAddUpdateEnvironmentDialog.tsxDeleteActionModal.tsxDeleteEnvVar.tsx
popups
table
dashboard
CommentField.tsxDashboardInputField.tsxDeleteActionButton.tsxDownloadSecretsMenu.tsxDropZone.tsxKeyPair.tsxSideBar.tsx
integrations
navigation
signup
utilities
ee
api/secrets
components
hooks
next-i18next.config.jspackage-lock.jsonpackage.jsonpages
404.tsx
activity
api
environments
files
workspace
dashboard
email-not-verified.tsxintegrations
login.tsxsettings
signup.tsxsignupinvite.jsusers
public
data
locales
i18n
img
36
.github/values.yaml
vendored
Normal file
36
.github/values.yaml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
frontend:
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository:
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
kubeSecretRef: managed-secret-frontend
|
||||
|
||||
backend:
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository:
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
kubeSecretRef: managed-backend-secret
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
hostName: gamma.infisical.com
|
||||
frontend:
|
||||
path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
path: /api
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: echo-tls
|
||||
hosts:
|
||||
- gamma.infisical.com
|
||||
|
||||
backendEnvironmentVariables:
|
||||
|
||||
frontendEnvironmentVariables:
|
70
.github/workflows/docker-image.yml
vendored
70
.github/workflows/docker-image.yml
vendored
@ -1,5 +1,4 @@
|
||||
name: Push to Docker Hub
|
||||
|
||||
name: Build, Publish and Deploy to Gamma
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
@ -10,8 +9,9 @@ jobs:
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- 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
|
||||
@ -19,9 +19,13 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 📦 Build backend and export to Docker
|
||||
uses: docker/build-push-action@v3
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/backend:test
|
||||
@ -35,11 +39,14 @@ jobs:
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: backend
|
||||
tags: infisical/backend:latest
|
||||
tags: infisical/backend:${{ steps.commit.outputs.short }},
|
||||
infisical/backend:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
frontend-image:
|
||||
@ -49,8 +56,9 @@ jobs:
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- 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
|
||||
@ -58,10 +66,14 @@ jobs:
|
||||
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: docker/build-push-action@v3
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
load: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
project: 64mmf0n610
|
||||
context: frontend
|
||||
tags: infisical/frontend:test
|
||||
build-args: |
|
||||
@ -76,11 +88,45 @@ jobs:
|
||||
run: |
|
||||
docker stop infisical-frontend-test
|
||||
- name: 🏗️ Build frontend and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: frontend
|
||||
tags: infisical/frontend:latest
|
||||
tags: infisical/frontend:${{ steps.commit.outputs.short }},
|
||||
infisical/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]
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: Install infisical helm chart
|
||||
run: |
|
||||
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||
helm repo update
|
||||
- name: Install kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
|
||||
- name: switch to gamma namespace
|
||||
run: kubectl config set-context --current --namespace=gamma
|
||||
- name: test kubectl
|
||||
run: kubectl get ingress
|
||||
- name: Download helm values to file and upgrade gamma deploy
|
||||
run: |
|
||||
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
||||
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods
|
2
.github/workflows/release_build.yml
vendored
2
.github/workflows/release_build.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Go releaser
|
||||
name: Build and release CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
|
3
Makefile
3
Makefile
@ -7,6 +7,9 @@ push:
|
||||
up-dev:
|
||||
docker-compose -f docker-compose.dev.yml up --build
|
||||
|
||||
i-dev:
|
||||
infisical export && infisical export > .env && docker-compose -f docker-compose.dev.yml up --build
|
||||
|
||||
up-prod:
|
||||
docker-compose -f docker-compose.yml up --build
|
||||
|
||||
|
16
README.md
16
README.md
@ -21,7 +21,7 @@
|
||||
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
|
||||
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
|
||||
</a>
|
||||
<a href="">
|
||||
<a href="https://github.com/Infisical/infisical/issues">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||
</a>
|
||||
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
|
||||
@ -40,13 +40,15 @@
|
||||
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow
|
||||
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
||||
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
|
||||
- **Personal/Shared** scoping for environment variables
|
||||
- **Personal overrides** for environment variables
|
||||
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
|
||||
- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** - check the history of change for any secret
|
||||
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** - check what user in the project is performing what actions with secrets
|
||||
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** - roll back to any snapshot of you secrets
|
||||
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
|
||||
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
|
||||
- 🔜 **Automatic Secret Rotation**
|
||||
- 🔜 **2FA**
|
||||
- 🔜 **Access Logs**
|
||||
- 🔜 **Slack Integration & MS Teams** integrations
|
||||
|
||||
And more.
|
||||
@ -65,7 +67,7 @@ To quickly get started, visit our [get started guide](https://infisical.com/docs
|
||||
|
||||
Infisical makes secret management simple and end-to-end encrypted by default. We're on a mission to make it more accessible to all developers, <i>not just security teams</i>.
|
||||
|
||||
According to a [report](https://www.ekransystem.com/en/blog/secrets-management) in 2019, only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
|
||||
According to a [report](https://www.ekransystem.com/en/blog/secrets-management), only 10% of organizations use secret management solutions despite all using digital secrets to some extent.
|
||||
|
||||
If you care about efficiency and security, then Infisical is right for you.
|
||||
|
||||
@ -319,7 +321,7 @@ Looking to report a security vulnerability? Please don't post about it in GitHub
|
||||
|
||||
## 🚨 Stay Up-to-Date
|
||||
|
||||
Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of new features are coming very quickly. Watch **releases** of this repository to be notified about future updates:
|
||||
Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot of new features coming very frequently. Watch **releases** of this repository to be notified about future updates:
|
||||
|
||||

|
||||
|
||||
@ -331,10 +333,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/JoaoVictor6"><img src="https://avatars.githubusercontent.com/u/68869379?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mocherfaoui"><img src="https://avatars.githubusercontent.com/u/37941426?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
|
||||
## 🌎 Translations
|
||||
|
||||
Infisical is currently aviable in English and Korean. Help us translate Infisical to your language!
|
||||
Infisical is currently available in English and Korean. Help us translate Infisical to your language!
|
||||
|
||||
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).
|
2568
backend/api-documentation.json
Normal file
2568
backend/api-documentation.json
Normal file
File diff suppressed because it is too large
Load Diff
82
backend/package-lock.json
generated
82
backend/package-lock.json
generated
@ -37,6 +37,8 @@
|
||||
"query-string": "^7.1.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"stripe": "^10.7.0",
|
||||
"swagger-autogen": "^2.22.0",
|
||||
"swagger-ui-express": "^4.6.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.9.3",
|
||||
@ -4549,7 +4551,6 @@
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -6690,7 +6691,6 @@
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
@ -11174,6 +11174,47 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-autogen": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.22.0.tgz",
|
||||
"integrity": "sha512-MPdtwgx/RL3og0RjFVV9hPoQv3x+c3ZRhS0Vjp9k94DLV7iUgIuCg8H+uAT8oD5w48ATTRT1VjcOHlCGH62pdA==",
|
||||
"dependencies": {
|
||||
"acorn": "^7.4.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"glob": "^7.1.7",
|
||||
"json5": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-autogen/node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "4.15.5",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz",
|
||||
"integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA=="
|
||||
},
|
||||
"node_modules/swagger-ui-express": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz",
|
||||
"integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==",
|
||||
"dependencies": {
|
||||
"swagger-ui-dist": ">=4.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= v0.10.32"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
||||
@ -15582,8 +15623,7 @@
|
||||
"deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
@ -17200,8 +17240,7 @@
|
||||
"json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
|
||||
},
|
||||
"jsonwebtoken": {
|
||||
"version": "9.0.0",
|
||||
@ -20424,6 +20463,37 @@
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true
|
||||
},
|
||||
"swagger-autogen": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.22.0.tgz",
|
||||
"integrity": "sha512-MPdtwgx/RL3og0RjFVV9hPoQv3x+c3ZRhS0Vjp9k94DLV7iUgIuCg8H+uAT8oD5w48ATTRT1VjcOHlCGH62pdA==",
|
||||
"requires": {
|
||||
"acorn": "^7.4.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"glob": "^7.1.7",
|
||||
"json5": "^2.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"swagger-ui-dist": {
|
||||
"version": "4.15.5",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz",
|
||||
"integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA=="
|
||||
},
|
||||
"swagger-ui-express": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz",
|
||||
"integrity": "sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA==",
|
||||
"requires": {
|
||||
"swagger-ui-dist": ">=4.11.0"
|
||||
}
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
|
||||
|
@ -1,46 +1,11 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@godaddy/terminus": "^4.11.2",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@sentry/node": "^7.14.0",
|
||||
"@sentry/tracing": "^7.19.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"await-to-js": "^3.0.0",
|
||||
"axios": "^1.1.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bigint-conversion": "^2.2.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-validator": "^6.14.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"helmet": "^5.1.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsrp": "^0.2.4",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"mongoose": "^6.7.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"posthog-node": "^2.2.2",
|
||||
"query-string": "^7.1.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"stripe": "^10.7.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.9.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loki": "^6.0.6"
|
||||
},
|
||||
"name": "infisical-api",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "npm run build && node build/index.js",
|
||||
"dev": "nodemon",
|
||||
"swagger-autogen": "node ./swagger.ts",
|
||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||
@ -108,5 +73,43 @@
|
||||
"suiteNameTemplate": "{filepath}",
|
||||
"classNameTemplate": "{classname}",
|
||||
"titleTemplate": "{title}"
|
||||
},
|
||||
"dependencies": {
|
||||
"@godaddy/terminus": "^4.11.2",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@sentry/node": "^7.14.0",
|
||||
"@sentry/tracing": "^7.19.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"await-to-js": "^3.0.0",
|
||||
"axios": "^1.1.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bigint-conversion": "^2.2.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-validator": "^6.14.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"helmet": "^5.1.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsrp": "^0.2.4",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"mongoose": "^6.7.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"posthog-node": "^2.2.2",
|
||||
"query-string": "^7.1.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"stripe": "^10.7.0",
|
||||
"swagger-autogen": "^2.22.0",
|
||||
"swagger-ui-express": "^4.6.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.9.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loki": "^6.0.6"
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
||||
|
||||
import express from 'express';
|
||||
import express, { Request, Response } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import dotenv from 'dotenv';
|
||||
import swaggerUi = require('swagger-ui-express');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const swaggerFile = require('../api-documentation.json')
|
||||
|
||||
|
||||
dotenv.config();
|
||||
import { PORT, NODE_ENV, SITE_URL } from './config';
|
||||
@ -38,11 +42,15 @@ import {
|
||||
} from './routes/v1';
|
||||
import {
|
||||
secret as v2SecretRouter,
|
||||
secrets as v2SecretsRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
apiKeyData as v2APIKeyDataRouter,
|
||||
environment as v2EnvironmentRouter,
|
||||
} from './routes/v2';
|
||||
|
||||
import { healthCheck } from './routes/status';
|
||||
|
||||
import { getLogger } from './utils/logger';
|
||||
import { RouteNotFoundError } from './utils/errors';
|
||||
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
||||
@ -89,18 +97,26 @@ app.use('/api/v1/membership', v1MembershipRouter);
|
||||
app.use('/api/v1/key', v1KeyRouter);
|
||||
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
||||
app.use('/api/v1/secret', v1SecretRouter);
|
||||
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
|
||||
app.use('/api/v1/service-token', v1ServiceTokenRouter); // stop supporting
|
||||
app.use('/api/v1/password', v1PasswordRouter);
|
||||
app.use('/api/v1/stripe', v1StripeRouter);
|
||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||
|
||||
// v2 routes
|
||||
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
||||
app.use('/api/v2/secret', v2SecretRouter);
|
||||
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
|
||||
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
||||
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
||||
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
|
||||
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
|
||||
|
||||
// api docs
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||
|
||||
// Server status
|
||||
app.use('/api', healthCheck)
|
||||
|
||||
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||
app.use((req, res, next) => {
|
||||
if (res.headersSent) return next();
|
||||
|
@ -3,7 +3,7 @@ import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { readFileSync } from 'fs';
|
||||
import { IntegrationAuth, Integration } from '../../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||
import { IntegrationService } from '../../services';
|
||||
import { getApps, revokeAccess } from '../../integrations';
|
||||
|
||||
@ -31,11 +31,17 @@ export const oAuthExchange = async (
|
||||
|
||||
if (!INTEGRATION_SET.has(integration))
|
||||
throw new Error('Failed to validate integration');
|
||||
|
||||
const environments = req.membership.workspace?.environments || [];
|
||||
if(environments.length === 0){
|
||||
throw new Error("Failed to get environments")
|
||||
}
|
||||
|
||||
await IntegrationService.handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
environment: environments[0].slug,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -115,13 +115,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
if (!membershipOrg) {
|
||||
throw new Error('Failed to validate organization membership');
|
||||
}
|
||||
|
||||
|
||||
invitee = await User.findOne({
|
||||
email: inviteeEmail
|
||||
});
|
||||
}).select('+publicKey');
|
||||
|
||||
if (invitee) {
|
||||
// case: invitee is an existing user
|
||||
|
||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||
user: invitee._id,
|
||||
organization: organizationId
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { EventService } from '../../services';
|
||||
import { ENV_SET } from '../../variables';
|
||||
import { postHogClient } from '../../services';
|
||||
|
||||
interface PushSecret {
|
||||
@ -44,7 +43,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
@ -116,7 +116,8 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
@ -183,7 +184,8 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ServiceToken } from '../../models';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { ENV_SET } from '../../variables';
|
||||
import { JWT_SERVICE_SECRET } from '../../config';
|
||||
|
||||
/**
|
||||
@ -36,7 +35,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
||||
} = req.body;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,6 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
|
204
backend/src/controllers/v2/environmentController.ts
Normal file
204
backend/src/controllers/v2/environmentController.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret,
|
||||
ServiceToken,
|
||||
Workspace,
|
||||
Integration,
|
||||
ServiceTokenData,
|
||||
} from '../../models';
|
||||
import { SecretVersion } from '../../ee/models';
|
||||
|
||||
/**
|
||||
* Create new workspace environment named [environmentName] under workspace with id
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createWorkspaceEnvironment = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { environmentName, environmentSlug } = req.body;
|
||||
try {
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (
|
||||
!workspace ||
|
||||
workspace?.environments.find(
|
||||
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||
)
|
||||
) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
workspace?.environments.push({
|
||||
name: environmentName.toLowerCase(),
|
||||
slug: environmentSlug.toLowerCase(),
|
||||
});
|
||||
await workspace.save();
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create new workspace environment',
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully created new environment',
|
||||
workspace: workspaceId,
|
||||
environment: {
|
||||
name: environmentName,
|
||||
slug: environmentSlug,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
|
||||
* Old slug [oldEnvironmentSlug] must be provided
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const renameWorkspaceEnvironment = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
||||
try {
|
||||
// user should pass both new slug and env name
|
||||
if (!environmentSlug || !environmentName) {
|
||||
throw new Error('Invalid environment given.');
|
||||
}
|
||||
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
const isEnvExist = workspace.environments.some(
|
||||
({ name, slug }) =>
|
||||
slug !== oldEnvironmentSlug &&
|
||||
(name === environmentName || slug === environmentSlug)
|
||||
);
|
||||
if (isEnvExist) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
const envIndex = workspace?.environments.findIndex(
|
||||
({ slug }) => slug === oldEnvironmentSlug
|
||||
);
|
||||
if (envIndex === -1) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
workspace.environments[envIndex].name = environmentName.toLowerCase();
|
||||
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
||||
|
||||
await workspace.save();
|
||||
await Secret.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await SecretVersion.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceToken.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceTokenData.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await Integration.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update workspace environment',
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully update environment',
|
||||
workspace: workspaceId,
|
||||
environment: {
|
||||
name: environmentName,
|
||||
slug: environmentSlug,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspaceEnvironment = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { environmentSlug } = req.body;
|
||||
try {
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw new Error('Failed to create workspace environment');
|
||||
}
|
||||
|
||||
const envIndex = workspace?.environments.findIndex(
|
||||
({ slug }) => slug === environmentSlug
|
||||
);
|
||||
if (envIndex === -1) {
|
||||
throw new Error('Invalid environment given');
|
||||
}
|
||||
|
||||
workspace.environments.splice(envIndex, 1);
|
||||
await workspace.save();
|
||||
|
||||
// clean up
|
||||
await Secret.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await SecretVersion.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await ServiceToken.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await ServiceTokenData.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
await Integration.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete workspace environment',
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully deleted environment',
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug,
|
||||
});
|
||||
};
|
@ -2,10 +2,14 @@ import * as workspaceController from './workspaceController';
|
||||
import * as serviceTokenDataController from './serviceTokenDataController';
|
||||
import * as apiKeyDataController from './apiKeyDataController';
|
||||
import * as secretController from './secretController';
|
||||
import * as secretsController from './secretsController';
|
||||
import * as environmentController from './environmentController';
|
||||
|
||||
export {
|
||||
workspaceController,
|
||||
serviceTokenDataController,
|
||||
apiKeyDataController,
|
||||
secretController
|
||||
secretController,
|
||||
secretsController,
|
||||
environmentController
|
||||
}
|
||||
|
@ -7,10 +7,68 @@ const { ValidationError } = mongoose.Error;
|
||||
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||
import { AnyBulkWriteOperation } from 'mongodb';
|
||||
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
||||
import { postHogClient } from '../../services';
|
||||
|
||||
export const batchCreateSecrets = async (req: Request, res: Response) => {
|
||||
/**
|
||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||
const { workspaceId, environment } = req.params
|
||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
|
||||
secretKeyIV: secretToCreate.secretKeyIV,
|
||||
secretKeyTag: secretToCreate.secretKeyTag,
|
||||
secretKeyHash: secretToCreate.secretKeyHash,
|
||||
secretValueCiphertext: secretToCreate.secretValueCiphertext,
|
||||
secretValueIV: secretToCreate.secretValueIV,
|
||||
secretValueTag: secretToCreate.secretValueTag,
|
||||
secretValueHash: secretToCreate.secretValueHash,
|
||||
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
|
||||
secretCommentIV: secretToCreate.secretCommentIV,
|
||||
secretCommentTag: secretToCreate.secretCommentTag,
|
||||
secretCommentHash: secretToCreate.secretCommentHash,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type: secretToCreate.type,
|
||||
user: new Types.ObjectId(req.user._id)
|
||||
}
|
||||
|
||||
|
||||
const [error, secret] = await to(Secret.create(sanitizedSecret).then())
|
||||
if (error instanceof ValidationError) {
|
||||
throw RouteValidationError({ message: error.message, stack: error.stack })
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets added',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secret
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create many secrets for workspace wiht id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecrets = async (req: Request, res: Response) => {
|
||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const { workspaceId, environment } = req.params
|
||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
||||
|
||||
secretsToCreate.forEach(rawSecret => {
|
||||
@ -28,7 +86,7 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentTag: rawSecret.secretCommentTag,
|
||||
secretCommentHash: rawSecret.secretCommentHash,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment: environmentName,
|
||||
environment,
|
||||
type: rawSecret.type,
|
||||
user: new Types.ObjectId(req.user._id)
|
||||
}
|
||||
@ -36,7 +94,7 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
||||
sanitizedSecretesToCreate.push(safeUpdateFields)
|
||||
})
|
||||
|
||||
const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
|
||||
const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
|
||||
if (bulkCreateError) {
|
||||
if (bulkCreateError instanceof ValidationError) {
|
||||
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
|
||||
@ -45,20 +103,31 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
||||
throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack })
|
||||
}
|
||||
|
||||
res.status(200).send()
|
||||
}
|
||||
|
||||
|
||||
export const createSingleSecret = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const secretFromDB = await Secret.findById(req.params.secretId)
|
||||
return res.status(200).send(secretFromDB);
|
||||
} catch (e) {
|
||||
throw BadRequestError({ message: "Unable to find the requested secret" })
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets added',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: (secretsToCreate ?? []).length,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secrets
|
||||
})
|
||||
}
|
||||
|
||||
export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
||||
/**
|
||||
* Delete secrets in workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretIdsToDelete: string[] = req.body.secretIds
|
||||
|
||||
@ -70,10 +139,12 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
||||
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
|
||||
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
|
||||
|
||||
let numSecretsDeleted = 0;
|
||||
secretIdsToDelete.forEach(secretIdToDelete => {
|
||||
if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
|
||||
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
|
||||
deleteOperationsToPerform.push(deleteOperation)
|
||||
numSecretsDeleted++;
|
||||
} else {
|
||||
throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
|
||||
}
|
||||
@ -87,10 +158,57 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
||||
throw InternalServerError()
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets deleted',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: numSecretsDeleted,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send()
|
||||
}
|
||||
|
||||
export const batchModifySecrets = async (req: Request, res: Response) => {
|
||||
/**
|
||||
* Delete secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecret = async (req: Request, res: Response) => {
|
||||
await Secret.findByIdAndDelete(req._secret._id)
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets deleted',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req._secret.workspace.toString(),
|
||||
environment: req._secret.environment,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secret: req._secret
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update secrets for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||
@ -101,7 +219,6 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
|
||||
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
|
||||
const updateOperationsToPerform: any = []
|
||||
|
||||
|
||||
secretsModificationsRequested.forEach(userModifiedSecret => {
|
||||
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
|
||||
const sanitizedSecret: SanitizedSecretModify = {
|
||||
@ -135,23 +252,99 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
|
||||
throw InternalServerError()
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets modified',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send()
|
||||
}
|
||||
|
||||
export const fetchAllSecrets = async (req: Request, res: Response) => {
|
||||
/**
|
||||
* Update a secret within workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecret = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environmentName } = req.params
|
||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||
|
||||
const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
|
||||
throw BadRequestError()
|
||||
}
|
||||
|
||||
const sanitizedSecret: SanitizedSecretModify = {
|
||||
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
|
||||
secretKeyIV: secretModificationsRequested.secretKeyIV,
|
||||
secretKeyTag: secretModificationsRequested.secretKeyTag,
|
||||
secretKeyHash: secretModificationsRequested.secretKeyHash,
|
||||
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
|
||||
secretValueIV: secretModificationsRequested.secretValueIV,
|
||||
secretValueTag: secretModificationsRequested.secretValueTag,
|
||||
secretValueHash: secretModificationsRequested.secretValueHash,
|
||||
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
|
||||
secretCommentIV: secretModificationsRequested.secretCommentIV,
|
||||
secretCommentTag: secretModificationsRequested.secretCommentTag,
|
||||
secretCommentHash: secretModificationsRequested.secretCommentHash,
|
||||
}
|
||||
|
||||
const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then())
|
||||
if (error instanceof ValidationError) {
|
||||
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets modified',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send(singleModificationUpdate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return secrets for workspace with id [workspaceId], environment [environment] and user
|
||||
* with id [req.user._id]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const { environment } = req.query;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
let userId: string | undefined = undefined // Used for choosing the personal secrets to fetch in
|
||||
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
||||
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
|
||||
if (req.user) {
|
||||
userId = req.user._id.toString();
|
||||
userId = req.user._id;
|
||||
userEmail = req.user.email;
|
||||
}
|
||||
|
||||
if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user._id
|
||||
userEmail = req.serviceTokenData.user.email;
|
||||
}
|
||||
|
||||
const [retriveAllSecretsError, allSecrets] = await to(Secret.find(
|
||||
const [err, secrets] = await to(Secret.find(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
@ -160,9 +353,49 @@ export const fetchAllSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
).then())
|
||||
|
||||
if (retriveAllSecretsError instanceof ValidationError) {
|
||||
throw RouteValidationError({ message: "Unable to get secrets, please try again", stack: retriveAllSecretsError.stack })
|
||||
if (err) {
|
||||
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
|
||||
}
|
||||
|
||||
return res.json(allSecrets)
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pulled',
|
||||
distinctId: userEmail,
|
||||
properties: {
|
||||
numberOfSecrets: (secrets ?? []).length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.json(secrets)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecret = async (req: Request, res: Response) => {
|
||||
// if (postHogClient) {
|
||||
// postHogClient.capture({
|
||||
// event: 'secrets pulled',
|
||||
// distinctId: req.user.email,
|
||||
// properties: {
|
||||
// numberOfSecrets: 1,
|
||||
// workspaceId: req._secret.workspace.toString(),
|
||||
// environment: req._secret.environment,
|
||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
// userAgent: req.headers?.['user-agent']
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
return res.status(200).send({
|
||||
secret: req._secret
|
||||
});
|
||||
}
|
476
backend/src/controllers/v2/secretsController.ts
Normal file
476
backend/src/controllers/v2/secretsController.ts
Normal file
@ -0,0 +1,476 @@
|
||||
import to from 'await-to-js';
|
||||
import { Types } from 'mongoose';
|
||||
import { Request, Response } from 'express';
|
||||
import { ISecret, Secret } from '../../models';
|
||||
import {
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
ACTION_ADD_SECRETS,
|
||||
ACTION_READ_SECRETS,
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ACTION_DELETE_SECRETS
|
||||
} from '../../variables';
|
||||
import { ValidationError } from '../../utils/errors';
|
||||
import { EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { EESecretService, EELogService } from '../../ee/services';
|
||||
import { postHogClient } from '../../services';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
|
||||
/**
|
||||
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecrets = async (req: Request, res: Response) => {
|
||||
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||
const { workspaceId, environment } = req.body;
|
||||
|
||||
let toAdd;
|
||||
if (Array.isArray(req.body.secrets)) {
|
||||
// case: create multiple secrets
|
||||
toAdd = req.body.secrets;
|
||||
} else if (typeof req.body.secrets === 'object') {
|
||||
// case: create 1 secret
|
||||
toAdd = [req.body.secrets];
|
||||
}
|
||||
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map(({
|
||||
type,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
}: {
|
||||
type: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}) => ({
|
||||
version: 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type,
|
||||
user: type === SECRET_PERSONAL ? req.user : undefined,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag
|
||||
}))
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
// trigger event - push secrets
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
_id: new Types.ObjectId(),
|
||||
secret: _id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
});
|
||||
|
||||
const addAction = await EELogService.createActionSecret({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
secretIds: newSecrets.map((n) => n._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
addAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
actions: [addAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets added',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: toAdd.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: newSecrets
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return secret(s) for workspace with id [workspaceId], environment [environment] and user
|
||||
* with id [req.user._id]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment } = req.query;
|
||||
|
||||
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
||||
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
|
||||
if (req.user) {
|
||||
userId = req.user._id;
|
||||
userEmail = req.user.email;
|
||||
}
|
||||
|
||||
if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user._id
|
||||
userEmail = req.serviceTokenData.user.email;
|
||||
}
|
||||
|
||||
const [err, secrets] = await to(Secret.find(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
$or: [
|
||||
{ user: userId },
|
||||
{ user: { $exists: false } }
|
||||
],
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
}
|
||||
).then())
|
||||
|
||||
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
|
||||
|
||||
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||
|
||||
const readAction = await EELogService.createActionSecret({
|
||||
name: ACTION_READ_SECRETS,
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: workspaceId as string,
|
||||
secretIds: secrets.map((n: any) => n._id)
|
||||
});
|
||||
|
||||
readAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: workspaceId as string,
|
||||
actions: [readAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pulled',
|
||||
distinctId: userEmail,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel,
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update secret(s)
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateSecrets = async (req: Request, res: Response) => {
|
||||
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||
|
||||
// TODO: move type
|
||||
interface PatchSecret {
|
||||
id: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
}
|
||||
|
||||
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
|
||||
const {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
} = secret;
|
||||
|
||||
return ({
|
||||
updateOne: {
|
||||
filter: { _id: new Types.ObjectId(secret.id) },
|
||||
update: {
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
...((
|
||||
secretCommentCiphertext &&
|
||||
secretCommentIV &&
|
||||
secretCommentTag
|
||||
) ? {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
} : {}),
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await Secret.bulkWrite(updateOperationsToPerform);
|
||||
|
||||
const secretModificationsBySecretId: { [key: string]: PatchSecret } = {};
|
||||
req.body.secrets.forEach((secret: PatchSecret) => {
|
||||
secretModificationsBySecretId[secret.id] = secret;
|
||||
});
|
||||
|
||||
const ListOfSecretsBeforeModifications = req.secrets
|
||||
const secretVersions = {
|
||||
secretVersions: ListOfSecretsBeforeModifications.map((secret: ISecret) => {
|
||||
const {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
} = secretModificationsBySecretId[secret._id.toString()]
|
||||
|
||||
return ({
|
||||
secret: secret._id,
|
||||
version: secret.version + 1,
|
||||
workspace: secret.workspace,
|
||||
type: secret.type,
|
||||
environment: secret.environment,
|
||||
secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
|
||||
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
|
||||
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
|
||||
secretValueCiphertext: secretValueCiphertext ? secretValueCiphertext : secret.secretValueCiphertext,
|
||||
secretValueIV: secretValueIV ? secretValueIV : secret.secretValueIV,
|
||||
secretValueTag: secretValueTag ? secretValueTag : secret.secretValueTag,
|
||||
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
|
||||
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
|
||||
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
await EESecretService.addSecretVersions(secretVersions);
|
||||
|
||||
|
||||
// group secrets into workspaces so updated secrets can
|
||||
// be logged and snapshotted separately for each workspace
|
||||
const workspaceSecretObj: any = {};
|
||||
req.secrets.forEach((s: any) => {
|
||||
if (s.workspace.toString() in workspaceSecretObj) {
|
||||
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||
} else {
|
||||
workspaceSecretObj[s.workspace.toString()] = [s]
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||
// trigger event - push secrets
|
||||
setTimeout(async () => {
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: key
|
||||
})
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const updateAction = await EELogService.createActionSecret({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: key,
|
||||
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
updateAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: key,
|
||||
actions: [updateAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: key
|
||||
})
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets modified',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: workspaceSecretObj[key].length,
|
||||
environment: workspaceSecretObj[key][0].environment,
|
||||
workspaceId: key,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: await Secret.find({
|
||||
_id: {
|
||||
$in: req.secrets.map((secret: ISecret) => secret._id)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete secret(s) with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||
const toDelete = req.secrets.map((s: any) => s._id);
|
||||
|
||||
await Secret.deleteMany({
|
||||
_id: {
|
||||
$in: toDelete
|
||||
}
|
||||
});
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: toDelete
|
||||
});
|
||||
|
||||
// group secrets into workspaces so deleted secrets can
|
||||
// be logged and snapshotted separately for each workspace
|
||||
const workspaceSecretObj: any = {};
|
||||
req.secrets.forEach((s: any) => {
|
||||
if (s.workspace.toString() in workspaceSecretObj) {
|
||||
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||
} else {
|
||||
workspaceSecretObj[s.workspace.toString()] = [s]
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||
// trigger event - push secrets
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: key
|
||||
})
|
||||
});
|
||||
const deleteAction = await EELogService.createActionSecret({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: key,
|
||||
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
deleteAction && await EELogService.createLog({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: key,
|
||||
actions: [deleteAction],
|
||||
channel,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: key
|
||||
})
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets deleted',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: workspaceSecretObj[key].length,
|
||||
environment: workspaceSecretObj[key][0].environment,
|
||||
workspaceId: key,
|
||||
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
userAgent: req.headers?.['user-agent']
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: req.secrets
|
||||
});
|
||||
}
|
@ -19,7 +19,6 @@ import {
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { postHogClient, EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { ENV_SET } from '../../variables';
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
@ -52,7 +51,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
@ -129,6 +129,11 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
} else if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user._id
|
||||
}
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
secrets = await pull({
|
||||
userId,
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Secret } from '../../../models';
|
||||
import { SecretVersion } from '../../models';
|
||||
import { EESecretService } from '../../services';
|
||||
|
||||
/**
|
||||
* Return secret versions for secret with id [secretId]
|
||||
@ -33,4 +35,103 @@ import { SecretVersion } from '../../models';
|
||||
return res.status(200).send({
|
||||
secretVersions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll back secret with id [secretId] to version [version]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
let secret;
|
||||
try {
|
||||
const { secretId } = req.params;
|
||||
const { version } = req.body;
|
||||
|
||||
// validate secret version
|
||||
const oldSecretVersion = await SecretVersion.findOne({
|
||||
secret: secretId,
|
||||
version
|
||||
});
|
||||
|
||||
if (!oldSecretVersion) throw new Error('Failed to find secret version');
|
||||
|
||||
const {
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
} = oldSecretVersion;
|
||||
|
||||
// update secret
|
||||
secret = await Secret.findByIdAndUpdate(
|
||||
secretId,
|
||||
{
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!secret) throw new Error('Failed to find and update secret');
|
||||
|
||||
// add new secret version
|
||||
await new SecretVersion({
|
||||
secret: secretId,
|
||||
version: secret.version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}).save();
|
||||
|
||||
// take secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: secret.workspace.toString()
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to roll back secret version'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secret
|
||||
});
|
||||
}
|
@ -2,6 +2,12 @@ import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretSnapshot } from '../../models';
|
||||
|
||||
/**
|
||||
* Return secret snapshot with id [secretSnapshotId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||
let secretSnapshot;
|
||||
try {
|
||||
|
@ -1,9 +1,17 @@
|
||||
import e, { Request, Response } from 'express';
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Secret
|
||||
} from '../../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
Log
|
||||
Log,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
} from '../../models';
|
||||
import { EESecretService } from '../../services';
|
||||
import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
||||
|
||||
/**
|
||||
* Return secret snapshots for workspace with id [workspaceId]
|
||||
@ -63,6 +71,159 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { version } = req.body;
|
||||
|
||||
// validate secret snapshot
|
||||
const secretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId,
|
||||
version
|
||||
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
|
||||
|
||||
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
||||
|
||||
// TODO: fix any
|
||||
const oldSecretVersionsObj: any = secretSnapshot.secretVersions
|
||||
.reduce((accumulator, s) => ({
|
||||
...accumulator,
|
||||
[`${s.secret.toString()}`]: s
|
||||
}), {});
|
||||
|
||||
const latestSecretVersionIds = await getLatestSecretVersionIds({
|
||||
secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
||||
});
|
||||
|
||||
// TODO: fix any
|
||||
const latestSecretVersions: any = (await SecretVersion.find({
|
||||
_id: {
|
||||
$in: latestSecretVersionIds.map((s) => s.versionId)
|
||||
}
|
||||
}, 'secret version'))
|
||||
.reduce((accumulator, s) => ({
|
||||
...accumulator,
|
||||
[`${s.secret.toString()}`]: s
|
||||
}), {});
|
||||
|
||||
// delete existing secrets
|
||||
await Secret.deleteMany({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
// add secrets
|
||||
secrets = await Secret.insertMany(
|
||||
secretSnapshot.secretVersions.map((sv) => {
|
||||
const secretId = sv.secret;
|
||||
const {
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
createdAt
|
||||
} = oldSecretVersionsObj[secretId.toString()];
|
||||
|
||||
return ({
|
||||
_id: secretId,
|
||||
version: latestSecretVersions[secretId.toString()].version + 1,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext: '',
|
||||
secretCommentIV: '',
|
||||
secretCommentTag: '',
|
||||
createdAt
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// add secret versions
|
||||
await SecretVersion.insertMany(
|
||||
secrets.map(({
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
_id: new Types.ObjectId(),
|
||||
secret: _id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
);
|
||||
|
||||
// update secret versions of restored secrets as not deleted
|
||||
await SecretVersion.updateMany({
|
||||
secret: {
|
||||
$in: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
||||
}
|
||||
}, {
|
||||
isDeleted: false
|
||||
});
|
||||
|
||||
// take secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to roll back secret snapshot'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return (audit) logs for workspace with id [workspaceId]
|
||||
* @param req
|
||||
|
@ -1,7 +1,10 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { Secret } from '../../models';
|
||||
import { SecretVersion, Action } from '../models';
|
||||
import {
|
||||
getLatestSecretVersionIds,
|
||||
getLatestNSecretSecretVersionIds
|
||||
} from '../helpers/secretVersion';
|
||||
import { ACTION_UPDATE_SECRETS } from '../../variables';
|
||||
|
||||
/**
|
||||
@ -30,65 +33,23 @@ const createActionSecretHelper = async ({
|
||||
if (name === ACTION_UPDATE_SECRETS) {
|
||||
// case: action is updating secrets
|
||||
// -> add old and new secret versions
|
||||
|
||||
// TODO: make query more efficient
|
||||
latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 },
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
versions: { $push: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
secret: "$_id",
|
||||
versions: { $slice: ["$versions", 2] },
|
||||
},
|
||||
}
|
||||
]))
|
||||
.map((s) => ({
|
||||
oldSecretVersion: s.versions[0]._id,
|
||||
newSecretVersion: s.versions[1]._id
|
||||
}));
|
||||
|
||||
|
||||
latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||
secretIds,
|
||||
n: 2
|
||||
}))
|
||||
.map((s) => ({
|
||||
oldSecretVersion: s.versions[0]._id,
|
||||
newSecretVersion: s.versions[1]._id
|
||||
}));
|
||||
} else {
|
||||
// case: action is adding, deleting, or reading secrets
|
||||
// -> add new secret versions
|
||||
latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$secret',
|
||||
version: { $max: '$version' },
|
||||
versionId: { $max: '$_id' } // secret version id
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 }
|
||||
}
|
||||
])
|
||||
.exec())
|
||||
.map((s) => ({
|
||||
newSecretVersion: s.versionId
|
||||
}));
|
||||
latestSecretVersions = (await getLatestSecretVersionIds({
|
||||
secretIds
|
||||
}))
|
||||
.map((s) => ({
|
||||
newSecretVersion: s.versionId
|
||||
}));
|
||||
}
|
||||
|
||||
action = await new Action({
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret,
|
||||
Secret,
|
||||
ISecret
|
||||
} from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
} from '../models';
|
||||
@ -18,24 +18,24 @@ import {
|
||||
* @param {String} obj.workspaceId
|
||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||
*/
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
|
||||
|
||||
let secretSnapshot;
|
||||
try {
|
||||
const secretIds = (await Secret.find({
|
||||
workspace: workspaceId
|
||||
}, '_id')).map((s) => s._id);
|
||||
|
||||
|
||||
const latestSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
}
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -48,14 +48,14 @@ import {
|
||||
{
|
||||
$sort: { version: -1 }
|
||||
}
|
||||
])
|
||||
])
|
||||
.exec())
|
||||
.map((s) => s.versionId);
|
||||
|
||||
|
||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId
|
||||
}).sort({ version: -1 });
|
||||
|
||||
|
||||
secretSnapshot = await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||
@ -66,7 +66,7 @@ import {
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to take a secret snapshot');
|
||||
}
|
||||
|
||||
|
||||
return secretSnapshot;
|
||||
}
|
||||
|
||||
@ -87,9 +87,9 @@ const addSecretVersionsHelper = async ({
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to add secret versions');
|
||||
throw new Error(`Failed to add secret versions [err=${err}]`);
|
||||
}
|
||||
|
||||
|
||||
return newSecretVersions;
|
||||
}
|
||||
|
||||
@ -120,39 +120,39 @@ const markDeletedSecretVersionsHelper = async ({
|
||||
const initSecretVersioningHelper = async () => {
|
||||
try {
|
||||
|
||||
await Secret.updateMany(
|
||||
await Secret.updateMany(
|
||||
{ version: { $exists: false } },
|
||||
{ $set: { version: 1 } }
|
||||
);
|
||||
|
||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||
{
|
||||
$lookup: {
|
||||
from: 'secretversions',
|
||||
localField: '_id',
|
||||
foreignField: 'secret',
|
||||
as: 'versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
versions: { $size: 0 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (unversionedSecrets.length > 0) {
|
||||
await addSecretVersionsHelper({
|
||||
secretVersions: unversionedSecrets.map((s, idx) => ({
|
||||
...s,
|
||||
secret: s._id,
|
||||
version: s.version ? s.version : 1,
|
||||
isDeleted: false,
|
||||
workspace: s.workspace,
|
||||
environment: s.environment
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||
{
|
||||
$lookup: {
|
||||
from: 'secretversions',
|
||||
localField: '_id',
|
||||
foreignField: 'secret',
|
||||
as: 'versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
versions: { $size: 0 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (unversionedSecrets.length > 0) {
|
||||
await addSecretVersionsHelper({
|
||||
secretVersions: unversionedSecrets.map((s, idx) => ({
|
||||
...s,
|
||||
secret: s._id,
|
||||
version: s.version ? s.version : 1,
|
||||
isDeleted: false,
|
||||
workspace: s.workspace,
|
||||
environment: s.environment
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
@ -162,7 +162,7 @@ const initSecretVersioningHelper = async () => {
|
||||
}
|
||||
|
||||
export {
|
||||
takeSecretSnapshotHelper,
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper,
|
||||
markDeletedSecretVersionsHelper,
|
||||
initSecretVersioningHelper
|
||||
|
110
backend/src/ee/helpers/secretVersion.ts
Normal file
110
backend/src/ee/helpers/secretVersion.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Types } from 'mongoose';
|
||||
import { SecretVersion } from '../models';
|
||||
|
||||
/**
|
||||
* Return latest secret versions for secrets with ids [secretIds]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||
* @returns
|
||||
*/
|
||||
const getLatestSecretVersionIds = async ({
|
||||
secretIds
|
||||
}: {
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
|
||||
interface LatestSecretVersionId {
|
||||
_id: Types.ObjectId;
|
||||
version: number;
|
||||
versionId: Types.ObjectId;
|
||||
}
|
||||
|
||||
let latestSecretVersionIds: LatestSecretVersionId[];
|
||||
try {
|
||||
latestSecretVersionIds = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$secret',
|
||||
version: { $max: '$version' },
|
||||
versionId: { $max: '$_id' } // id of latest secret version
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 }
|
||||
}
|
||||
])
|
||||
.exec());
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get latest secret versions');
|
||||
}
|
||||
|
||||
return latestSecretVersionIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return latest [n] secret versions for secrets with ids [secretIds]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||
* @param {Number} obj.n - number of latest secret versions to return for each secret
|
||||
* @returns
|
||||
*/
|
||||
const getLatestNSecretSecretVersionIds = async ({
|
||||
secretIds,
|
||||
n
|
||||
}: {
|
||||
secretIds: Types.ObjectId[];
|
||||
n: number;
|
||||
}) => {
|
||||
|
||||
// TODO: optimize query
|
||||
let latestNSecretVersions;
|
||||
try {
|
||||
latestNSecretVersions = (await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 },
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
versions: { $push: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
secret: "$_id",
|
||||
versions: { $slice: ["$versions", n] },
|
||||
},
|
||||
}
|
||||
]));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get latest n secret versions');
|
||||
}
|
||||
|
||||
return latestNSecretVersions;
|
||||
}
|
||||
|
||||
export {
|
||||
getLatestSecretVersionIds,
|
||||
getLatestNSecretSecretVersionIds
|
||||
}
|
@ -2,31 +2,18 @@ import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../../variables';
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* 1. Modify SecretVersion to also contain XX
|
||||
* - type
|
||||
* - user
|
||||
* - environment
|
||||
* 2. Modify SecretSnapshot to point to arrays of SecretVersion
|
||||
*/
|
||||
|
||||
export interface ISecretVersion {
|
||||
_id?: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
_id: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId; // new
|
||||
type: string; // new
|
||||
user: Types.ObjectId; // new
|
||||
environment: string; // new
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
@ -37,17 +24,17 @@ export interface ISecretVersion {
|
||||
}
|
||||
|
||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
{
|
||||
secret: { // could be deleted
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Secret',
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
secret: { // could be deleted
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Secret',
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
@ -65,15 +52,14 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
isDeleted: { // consider removing field
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
@ -86,8 +72,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
type: String
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
@ -102,13 +87,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
type: String
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
requireSecretAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { query, param } from 'express-validator';
|
||||
import { query, param, body } from 'express-validator';
|
||||
import { secretController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER } from '../../../variables';
|
||||
|
||||
@ -24,4 +24,17 @@ router.get(
|
||||
secretController.getSecretVersions
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:secretId/secret-versions/rollback',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('secretId').exists().trim(),
|
||||
body('version').exists().isInt(),
|
||||
secretController.rollbackSecretVersion
|
||||
);
|
||||
|
||||
export default router;
|
@ -7,7 +7,7 @@ import {
|
||||
requireAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { param } from 'express-validator';
|
||||
import { param, body } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../../variables';
|
||||
import { secretSnapshotController } from '../../controllers/v1';
|
||||
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { param, query } from 'express-validator';
|
||||
import { param, query, body } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../../variables';
|
||||
import { workspaceController } from '../../controllers/v1';
|
||||
|
||||
@ -37,6 +37,20 @@ router.get(
|
||||
workspaceController.getWorkspaceSecretSnapshotsCount
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/secret-snapshots/rollback',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('version').exists().isInt(),
|
||||
validateRequest,
|
||||
workspaceController.rollbackWorkspaceSecretSnapshot
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/logs',
|
||||
requireAuth({
|
||||
|
@ -72,7 +72,7 @@ const getSecretsHelper = async ({
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const secrets = await Secret.find({
|
||||
workspaceId,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
type: SECRET_SHARED
|
||||
});
|
||||
@ -84,7 +84,7 @@ const getSecretsHelper = async ({
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
|
@ -7,8 +7,6 @@ import {
|
||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||
import { BotService } from '../services';
|
||||
import {
|
||||
ENV_DEV,
|
||||
EVENT_PUSH_SECRETS,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
@ -36,11 +34,13 @@ interface Update {
|
||||
const handleOAuthExchangeHelper = async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
environment: string;
|
||||
}) => {
|
||||
let action;
|
||||
let integrationAuth;
|
||||
@ -102,9 +102,9 @@ const handleOAuthExchangeHelper = async ({
|
||||
// initialize new integration after exchange
|
||||
await new Integration({
|
||||
workspace: workspaceId,
|
||||
environment: ENV_DEV,
|
||||
isActive: false,
|
||||
app: null,
|
||||
environment,
|
||||
integration,
|
||||
integrationAuth: integrationAuth._id
|
||||
}).save();
|
||||
|
@ -6,7 +6,9 @@ const apiLimiter = rateLimit({
|
||||
max: 450,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (request) => request.path === '/healthcheck'
|
||||
skip: (request) => {
|
||||
return request.path === '/healthcheck' || request.path === '/api/status'
|
||||
}
|
||||
});
|
||||
|
||||
// 5 requests per hour
|
||||
|
@ -3,6 +3,7 @@ import { Types } from 'mongoose';
|
||||
import {
|
||||
Secret,
|
||||
ISecret,
|
||||
Membership
|
||||
} from '../models';
|
||||
import {
|
||||
EESecretService,
|
||||
@ -20,6 +21,46 @@ import {
|
||||
ACTION_READ_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] can modify secrets with ids [secretIds]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.userId - id of user to validate
|
||||
* @param {Object} obj.secretIds - secret ids
|
||||
* @returns {Secret[]} secrets
|
||||
*/
|
||||
const validateSecrets = async ({
|
||||
userId,
|
||||
secretIds
|
||||
}: {
|
||||
userId: string;
|
||||
secretIds: string[];
|
||||
}) =>{
|
||||
let secrets;
|
||||
try {
|
||||
secrets = await Secret.find({
|
||||
_id: {
|
||||
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
|
||||
}
|
||||
});
|
||||
|
||||
const workspaceIdsSet = new Set((await Membership.find({
|
||||
user: userId
|
||||
}, 'workspace'))
|
||||
.map((m) => m.workspace.toString()));
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
if (!workspaceIdsSet.has(secret.workspace.toString())) {
|
||||
throw new Error('Failed to validate secret');
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
throw new Error('Failed to validate secrets');
|
||||
}
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
interface V1PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
@ -187,6 +228,7 @@ const v1PushSecrets = async ({
|
||||
}) => {
|
||||
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
||||
return ({
|
||||
_id: new Types.ObjectId(),
|
||||
secret: _id,
|
||||
version: version ? version + 1 : 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
@ -258,6 +300,7 @@ const v1PushSecrets = async ({
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
_id: new Types.ObjectId(),
|
||||
secret: _id,
|
||||
version,
|
||||
workspace,
|
||||
@ -280,7 +323,7 @@ const v1PushSecrets = async ({
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@ -527,6 +570,7 @@ const v1PushSecrets = async ({
|
||||
environment: string;
|
||||
}): Promise<ISecret[]> => {
|
||||
let secrets: any; // TODO: FIX any
|
||||
|
||||
try {
|
||||
// get shared workspace secrets
|
||||
const sharedSecrets = await Secret.find({
|
||||
@ -655,6 +699,7 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||
};
|
||||
|
||||
export {
|
||||
validateSecrets,
|
||||
v1PushSecrets,
|
||||
v2PushSecrets,
|
||||
pullSecrets,
|
||||
|
@ -9,14 +9,9 @@ import {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_GITHUB_API_URL
|
||||
INTEGRATION_NETLIFY_API_URL
|
||||
} from '../variables';
|
||||
|
||||
interface GitHubApp {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of names of apps for integration named [integration]
|
||||
* @param {Object} obj
|
||||
@ -47,6 +42,7 @@ const getApps = async ({
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
apps = await getAppsVercel({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
@ -110,17 +106,28 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
||||
* @returns {Object[]} apps - names of Vercel apps
|
||||
* @returns {String} apps.name - name of Vercel app
|
||||
*/
|
||||
const getAppsVercel = async ({ accessToken }: { accessToken: string }) => {
|
||||
const getAppsVercel = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (
|
||||
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
...( integrationAuth?.teamId ? {
|
||||
params: {
|
||||
teamId: integrationAuth.teamId
|
||||
}
|
||||
} : {})
|
||||
})
|
||||
).data;
|
||||
|
||||
|
||||
apps = res.projects.map((a: any) => ({
|
||||
name: a.name
|
||||
}));
|
||||
|
@ -8,8 +8,7 @@ import {
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_API_URL
|
||||
INTEGRATION_GITHUB_TOKEN_URL
|
||||
} from '../variables';
|
||||
import {
|
||||
SITE_URL,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
// import * as sodium from 'libsodium-wrappers';
|
||||
@ -12,14 +12,10 @@ import {
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_GITHUB_API_URL
|
||||
INTEGRATION_NETLIFY_API_URL
|
||||
} from '../variables';
|
||||
import { access, appendFile } from 'fs';
|
||||
|
||||
// TODO: need a helper function in the future to handle integration
|
||||
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
* @param {Object} obj
|
||||
@ -53,6 +49,7 @@ const syncSecrets = async ({
|
||||
case INTEGRATION_VERCEL:
|
||||
await syncSecretsVercel({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
@ -139,14 +136,15 @@ const syncSecretsHeroku = async ({
|
||||
*/
|
||||
const syncSecretsVercel = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration,
|
||||
integrationAuth: IIntegrationAuth,
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
|
||||
interface VercelSecret {
|
||||
id?: string;
|
||||
type: string;
|
||||
@ -156,129 +154,135 @@ const syncSecretsVercel = async ({
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all (decrypted) secrets back from Vercel in
|
||||
// decrypted format
|
||||
const params = new URLSearchParams({
|
||||
decrypt: "true"
|
||||
});
|
||||
|
||||
const res = (await Promise.all((await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
|
||||
{
|
||||
// Get all (decrypted) secrets back from Vercel in
|
||||
// decrypted format
|
||||
const params: { [key: string]: string } = {
|
||||
decrypt: 'true',
|
||||
...( integrationAuth?.teamId ? {
|
||||
teamId: integrationAuth.teamId
|
||||
} : {})
|
||||
}
|
||||
|
||||
const res = (await Promise.all((await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.envs
|
||||
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
||||
.map(async (secret: VercelSecret) => (await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.envs
|
||||
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
||||
.map(async (secret: VercelSecret) => (await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
}
|
||||
)).data)
|
||||
)).reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}), {});
|
||||
|
||||
const updateSecrets: VercelSecret[] = [];
|
||||
const deleteSecrets: VercelSecret[] = [];
|
||||
const newSecrets: VercelSecret[] = [];
|
||||
|
||||
// Identify secrets to create
|
||||
Object.keys(secrets).map((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: secret has been created
|
||||
newSecrets.push({
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Identify secrets to update and delete
|
||||
Object.keys(res).map((key) => {
|
||||
if (key in secrets) {
|
||||
if (res[key].value !== secrets[key]) {
|
||||
// case: secret value has changed
|
||||
updateSecrets.push({
|
||||
id: res[key].id,
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: secret has been deleted
|
||||
deleteSecrets.push({
|
||||
id: res[key].id,
|
||||
key: key,
|
||||
value: res[key].value,
|
||||
type: 'encrypted',
|
||||
target: [integration.target],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sync/push new secrets
|
||||
if (newSecrets.length > 0) {
|
||||
await axios.post(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Sync/push updated secrets
|
||||
if (updateSecrets.length > 0) {
|
||||
updateSecrets.forEach(async (secret: VercelSecret) => {
|
||||
const {
|
||||
id,
|
||||
...updatedSecret
|
||||
} = secret;
|
||||
await axios.patch(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
updatedSecret,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Delete secrets
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
}
|
||||
)).data)
|
||||
)).reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}), {});
|
||||
|
||||
const updateSecrets: VercelSecret[] = [];
|
||||
const deleteSecrets: VercelSecret[] = [];
|
||||
const newSecrets: VercelSecret[] = [];
|
||||
|
||||
// Identify secrets to create
|
||||
Object.keys(secrets).map((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: secret has been created
|
||||
newSecrets.push({
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Identify secrets to update and delete
|
||||
Object.keys(res).map((key) => {
|
||||
if (key in secrets) {
|
||||
if (res[key].value !== secrets[key]) {
|
||||
// case: secret value has changed
|
||||
updateSecrets.push({
|
||||
id: res[key].id,
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: secret has been deleted
|
||||
deleteSecrets.push({
|
||||
id: res[key].id,
|
||||
key: key,
|
||||
value: res[key].value,
|
||||
type: 'encrypted',
|
||||
target: [integration.target],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sync/push new secrets
|
||||
if (newSecrets.length > 0) {
|
||||
await axios.post(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Sync/push updated secrets
|
||||
if (updateSecrets.length > 0) {
|
||||
updateSecrets.forEach(async (secret: VercelSecret) => {
|
||||
const {
|
||||
id,
|
||||
...updatedSecret
|
||||
} = secret;
|
||||
await axios.patch(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
updatedSecret,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Delete secrets
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Vercel');
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Vercel');
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,188 +306,188 @@ const syncSecretsNetlify = async ({
|
||||
}) => {
|
||||
try {
|
||||
|
||||
interface NetlifyValue {
|
||||
id?: string;
|
||||
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface NetlifySecret {
|
||||
key: string;
|
||||
values: NetlifyValue[];
|
||||
}
|
||||
|
||||
interface NetlifySecretsRes {
|
||||
[index: string]: NetlifySecret;
|
||||
}
|
||||
|
||||
const getParams = new URLSearchParams({
|
||||
context_name: 'all', // integration.context or all
|
||||
site_id: integration.siteId
|
||||
});
|
||||
|
||||
const res = (await axios.get(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
{
|
||||
params: getParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}), {});
|
||||
|
||||
const newSecrets: NetlifySecret[] = []; // createEnvVars
|
||||
const deleteSecrets: string[] = []; // deleteEnvVar
|
||||
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
|
||||
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
|
||||
interface NetlifyValue {
|
||||
id?: string;
|
||||
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface NetlifySecret {
|
||||
key: string;
|
||||
values: NetlifyValue[];
|
||||
}
|
||||
|
||||
interface NetlifySecretsRes {
|
||||
[index: string]: NetlifySecret;
|
||||
}
|
||||
|
||||
const getParams = new URLSearchParams({
|
||||
context_name: 'all', // integration.context or all
|
||||
site_id: integration.siteId
|
||||
});
|
||||
|
||||
const res = (await axios.get(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
{
|
||||
params: getParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}), {});
|
||||
|
||||
const newSecrets: NetlifySecret[] = []; // createEnvVars
|
||||
const deleteSecrets: string[] = []; // deleteEnvVar
|
||||
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
|
||||
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
|
||||
|
||||
// identify secrets to create and update
|
||||
Object.keys(secrets).map((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: Infisical secret does not exist in Netlify -> create secret
|
||||
newSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
value: secrets[key],
|
||||
context: integration.context
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
// case: Infisical secret exists in Netlify
|
||||
const contexts = res[key].values
|
||||
.reduce((obj: any, value: NetlifyValue) => ({
|
||||
...obj,
|
||||
[value.context]: value
|
||||
}), {});
|
||||
|
||||
if (integration.context in contexts) {
|
||||
// case: Netlify secret value exists in integration context
|
||||
if (secrets[key] !== contexts[integration.context].value) {
|
||||
// case: Infisical and Netlify secret values are different
|
||||
// -> update Netlify secret context and value
|
||||
updateSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
context: integration.context,
|
||||
value: secrets[key]
|
||||
}]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: Netlify secret value does not exist in integration context
|
||||
// -> add the new Netlify secret context and value
|
||||
updateSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
context: integration.context,
|
||||
value: secrets[key]
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// identify secrets to delete
|
||||
// TODO: revise (patch case where 1 context was deleted but others still there
|
||||
Object.keys(res).map((key) => {
|
||||
// loop through each key's context
|
||||
if (!(key in secrets)) {
|
||||
// case: Netlify secret does not exist in Infisical
|
||||
|
||||
const numberOfValues = res[key].values.length;
|
||||
|
||||
res[key].values.forEach((value: NetlifyValue) => {
|
||||
if (value.context === integration.context) {
|
||||
if (numberOfValues <= 1) {
|
||||
// case: Netlify secret value has less than 1 context -> delete secret
|
||||
deleteSecrets.push(key);
|
||||
} else {
|
||||
// case: Netlify secret value has more than 1 context -> delete secret value context
|
||||
deleteSecretValues.push({
|
||||
key,
|
||||
values: [{
|
||||
id: value.id,
|
||||
context: integration.context,
|
||||
value: value.value
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// identify secrets to create and update
|
||||
Object.keys(secrets).map((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: Infisical secret does not exist in Netlify -> create secret
|
||||
newSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
value: secrets[key],
|
||||
context: integration.context
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
// case: Infisical secret exists in Netlify
|
||||
const contexts = res[key].values
|
||||
.reduce((obj: any, value: NetlifyValue) => ({
|
||||
...obj,
|
||||
[value.context]: value
|
||||
}), {});
|
||||
|
||||
if (integration.context in contexts) {
|
||||
// case: Netlify secret value exists in integration context
|
||||
if (secrets[key] !== contexts[integration.context].value) {
|
||||
// case: Infisical and Netlify secret values are different
|
||||
// -> update Netlify secret context and value
|
||||
updateSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
context: integration.context,
|
||||
value: secrets[key]
|
||||
}]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: Netlify secret value does not exist in integration context
|
||||
// -> add the new Netlify secret context and value
|
||||
updateSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
context: integration.context,
|
||||
value: secrets[key]
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// identify secrets to delete
|
||||
// TODO: revise (patch case where 1 context was deleted but others still there
|
||||
Object.keys(res).map((key) => {
|
||||
// loop through each key's context
|
||||
if (!(key in secrets)) {
|
||||
// case: Netlify secret does not exist in Infisical
|
||||
|
||||
const numberOfValues = res[key].values.length;
|
||||
|
||||
res[key].values.forEach((value: NetlifyValue) => {
|
||||
if (value.context === integration.context) {
|
||||
if (numberOfValues <= 1) {
|
||||
// case: Netlify secret value has less than 1 context -> delete secret
|
||||
deleteSecrets.push(key);
|
||||
} else {
|
||||
// case: Netlify secret value has more than 1 context -> delete secret value context
|
||||
deleteSecretValues.push({
|
||||
key,
|
||||
values: [{
|
||||
id: value.id,
|
||||
context: integration.context,
|
||||
value: value.value
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
const syncParams = new URLSearchParams({
|
||||
site_id: integration.siteId
|
||||
});
|
||||
|
||||
const syncParams = new URLSearchParams({
|
||||
site_id: integration.siteId
|
||||
});
|
||||
if (newSecrets.length > 0) {
|
||||
await axios.post(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (newSecrets.length > 0) {
|
||||
await axios.post(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (updateSecrets.length > 0) {
|
||||
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
||||
await axios.patch(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||
{
|
||||
context: secret.values[0].context,
|
||||
value: secret.values[0].value
|
||||
},
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (updateSecrets.length > 0) {
|
||||
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
||||
await axios.patch(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||
{
|
||||
context: secret.values[0].context,
|
||||
value: secret.values[0].value
|
||||
},
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (key: string) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (key: string) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSecretValues.length > 0) {
|
||||
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
if (deleteSecretValues.length > 0) {
|
||||
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Heroku');
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Heroku');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizati
|
||||
import requireServiceTokenAuth from './requireServiceTokenAuth';
|
||||
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
|
||||
import requireSecretAuth from './requireSecretAuth';
|
||||
import requireSecretsAuth from './requireSecretsAuth';
|
||||
import validateRequest from './validateRequest';
|
||||
|
||||
export {
|
||||
@ -21,5 +22,6 @@ export {
|
||||
requireServiceTokenAuth,
|
||||
requireServiceTokenDataAuth,
|
||||
requireSecretAuth,
|
||||
requireSecretsAuth,
|
||||
validateRequest
|
||||
};
|
||||
|
@ -5,6 +5,9 @@ import {
|
||||
validateMembership
|
||||
} from '../helpers/membership';
|
||||
|
||||
// note: used for old /v1/secret and /v2/secret routes.
|
||||
// newer /v2/secrets routes use [requireSecretsAuth] middleware
|
||||
|
||||
/**
|
||||
* Validate if user on request has proper membership to modify secret.
|
||||
* @param {Object} obj
|
||||
@ -34,7 +37,7 @@ const requireSecretAuth = ({
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
req.secret = secret as any;
|
||||
req._secret = secret;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
|
49
backend/src/middleware/requireSecretsAuth.ts
Normal file
49
backend/src/middleware/requireSecretsAuth.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
import { Secret, Membership } from '../models';
|
||||
import { validateSecrets } from '../helpers/secret';
|
||||
|
||||
// TODO: make this work for delete route
|
||||
|
||||
const requireSecretsAuth = ({
|
||||
acceptedRoles
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
let secrets;
|
||||
try {
|
||||
if (Array.isArray(req.body.secrets)) {
|
||||
// case: validate multiple secrets
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: req.body.secrets.map((s: any) => s.id)
|
||||
});
|
||||
} else if (typeof req.body.secrets === 'object') { // change this to check for object
|
||||
// case: validate 1 secret
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: req.body.secrets.id
|
||||
});
|
||||
} else if (Array.isArray(req.body.secretIds)) {
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: req.body.secretIds
|
||||
});
|
||||
} else if (typeof req.body.secretIds === 'string') {
|
||||
// case: validate secretIds
|
||||
secrets = await validateSecrets({
|
||||
userId: req.user._id.toString(),
|
||||
secretIds: [req.body.secretIds]
|
||||
});
|
||||
}
|
||||
|
||||
req.secrets = secrets;
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default requireSecretsAuth;
|
@ -1,9 +1,5 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -13,7 +9,7 @@ import {
|
||||
export interface IIntegration {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
||||
environment: string;
|
||||
isActive: boolean;
|
||||
app: string;
|
||||
target: string;
|
||||
@ -32,7 +28,6 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
|
@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../variables';
|
||||
|
||||
export interface ISecret {
|
||||
@ -53,7 +49,6 @@ const secretSchema = new Schema<ISecret>(
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
@ -69,8 +64,7 @@ const secretSchema = new Schema<ISecret>(
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
type: String
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
@ -85,8 +79,7 @@ const secretSchema = new Schema<ISecret>(
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
type: String
|
||||
},
|
||||
secretCommentCiphertext: {
|
||||
type: String,
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
|
||||
|
||||
// TODO: deprecate
|
||||
export interface IServiceToken {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
@ -33,7 +30,6 @@ const serviceTokenSchema = new Schema<IServiceToken>(
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
expiresAt: {
|
||||
|
@ -4,6 +4,10 @@ export interface IWorkspace {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
organization: Types.ObjectId;
|
||||
environments: Array<{
|
||||
name: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const workspaceSchema = new Schema<IWorkspace>({
|
||||
@ -15,7 +19,33 @@ const workspaceSchema = new Schema<IWorkspace>({
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Organization',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
environments: {
|
||||
type: [
|
||||
{
|
||||
name: String,
|
||||
slug: String,
|
||||
},
|
||||
],
|
||||
default: [
|
||||
{
|
||||
name: "development",
|
||||
slug: "dev"
|
||||
},
|
||||
{
|
||||
name: "test",
|
||||
slug: "test"
|
||||
},
|
||||
{
|
||||
name: "staging",
|
||||
slug: "staging"
|
||||
},
|
||||
{
|
||||
name: "production",
|
||||
slug: "prod"
|
||||
}
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const Workspace = model<IWorkspace>('Workspace', workspaceSchema);
|
||||
|
5
backend/src/routes/status/index.ts
Normal file
5
backend/src/routes/status/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import healthCheck from './status';
|
||||
|
||||
export {
|
||||
healthCheck
|
||||
}
|
15
backend/src/routes/status/status.ts
Normal file
15
backend/src/routes/status/status.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/status',
|
||||
(req: Request, res: Response) => {
|
||||
res.status(200).json({
|
||||
date: new Date(),
|
||||
message: 'Ok',
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
export default router
|
57
backend/src/routes/v2/environment.ts
Normal file
57
backend/src/routes/v2/environment.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import express, { Response, Request } from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { environmentController } from '../../controllers/v2';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/environments',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('environmentSlug').exists().trim(),
|
||||
body('environmentName').exists().trim(),
|
||||
validateRequest,
|
||||
environmentController.createWorkspaceEnvironment
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:workspaceId/environments',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('environmentSlug').exists().trim(),
|
||||
body('environmentName').exists().trim(),
|
||||
body('oldEnvironmentSlug').exists().trim(),
|
||||
validateRequest,
|
||||
environmentController.renameWorkspaceEnvironment
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:workspaceId/environments',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt'],
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('environmentSlug').exists().trim(),
|
||||
validateRequest,
|
||||
environmentController.deleteWorkspaceEnvironment
|
||||
);
|
||||
|
||||
export default router;
|
@ -1,11 +1,15 @@
|
||||
import secret from './secret';
|
||||
import secret from './secret'; // stop-supporting
|
||||
import secrets from './secrets';
|
||||
import workspace from './workspace';
|
||||
import serviceTokenData from './serviceTokenData';
|
||||
import apiKeyData from './apiKeyData';
|
||||
import environment from "./environment"
|
||||
|
||||
export {
|
||||
secret,
|
||||
secrets,
|
||||
workspace,
|
||||
serviceTokenData,
|
||||
apiKeyData
|
||||
}
|
||||
apiKeyData,
|
||||
environment
|
||||
}
|
@ -1,18 +1,21 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
|
||||
import express from 'express';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireSecretAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
||||
import { secretController } from '../../controllers/v2';
|
||||
import { fetchAllSecrets } from '../../controllers/v2/secretController';
|
||||
|
||||
// note to devs: stop supporting
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Create many secrets for a given workspace and environmentName
|
||||
*/
|
||||
router.post(
|
||||
'/batch-create/workspace/:workspaceId/environment/:environmentName',
|
||||
'/batch-create/workspace/:workspaceId/environment/:environment',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
@ -20,15 +23,29 @@ router.post(
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environmentName').exists().trim(),
|
||||
param('environment').exists().trim(),
|
||||
body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')),
|
||||
body('channel'),
|
||||
validateRequest,
|
||||
secretController.batchCreateSecrets
|
||||
secretController.createSecrets
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/workspace/:workspaceId/environment/:environment',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environment').exists().trim(),
|
||||
body('secret').exists().isObject(),
|
||||
body('channel'),
|
||||
validateRequest,
|
||||
secretController.createSecret
|
||||
);
|
||||
|
||||
/**
|
||||
* Get all secrets for a given environment and workspace id
|
||||
*/
|
||||
router.get(
|
||||
'/workspace/:workspaceId',
|
||||
param('workspaceId').exists().trim(),
|
||||
@ -39,13 +56,23 @@ router.get(
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
query('channel'),
|
||||
validateRequest,
|
||||
fetchAllSecrets
|
||||
secretController.getSecrets
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:secretId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.getSecret
|
||||
);
|
||||
|
||||
/**
|
||||
* Batch delete secrets in a given workspace and environment name
|
||||
*/
|
||||
router.delete(
|
||||
'/batch/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth({
|
||||
@ -58,13 +85,22 @@ router.delete(
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.batchDeleteSecrets
|
||||
|
||||
secretController.deleteSecrets
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:secretId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('secretId').isMongoId(),
|
||||
validateRequest,
|
||||
secretController.deleteSecret
|
||||
);
|
||||
|
||||
/**
|
||||
* Apply modifications to many existing secrets in a given workspace and environment
|
||||
*/
|
||||
router.patch(
|
||||
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth({
|
||||
@ -77,7 +113,23 @@ router.patch(
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.batchModifySecrets
|
||||
secretController.updateSecrets
|
||||
);
|
||||
|
||||
|
||||
router.patch(
|
||||
'/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
body('secret').isObject(),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environmentName').exists().trim(),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.updateSecret
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
157
backend/src/routes/v2/secrets.ts
Normal file
157
backend/src/routes/v2/secrets.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireSecretsAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { query, check, body } from 'express-validator';
|
||||
import { secretsController } from '../../controllers/v2';
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED
|
||||
} from '../../variables';
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('secrets')
|
||||
.exists()
|
||||
.custom((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
// case: create multiple secrets
|
||||
if (value.length === 0) throw new Error('secrets cannot be an empty array')
|
||||
for (const secret of value) {
|
||||
if (
|
||||
!secret.type ||
|
||||
!(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
|
||||
!secret.secretKeyCiphertext ||
|
||||
!secret.secretKeyIV ||
|
||||
!secret.secretKeyTag ||
|
||||
!secret.secretValueCiphertext ||
|
||||
!secret.secretValueIV ||
|
||||
!secret.secretValueTag
|
||||
) {
|
||||
throw new Error('secrets array must contain objects that have required secret properties');
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
// case: update 1 secret
|
||||
if (
|
||||
!value.type ||
|
||||
!(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) ||
|
||||
!value.secretKeyCiphertext ||
|
||||
!value.secretKeyIV ||
|
||||
!value.secretKeyTag ||
|
||||
!value.secretValueCiphertext ||
|
||||
!value.secretValueIV ||
|
||||
!value.secretValueTag
|
||||
) {
|
||||
throw new Error('secrets object is missing required secret properties');
|
||||
}
|
||||
} else {
|
||||
throw new Error('secrets must be an object or an array of objects')
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
secretsController.createSecrets
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
query('workspaceId').exists().trim(),
|
||||
query('environment').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'query'
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/',
|
||||
body('secrets')
|
||||
.exists()
|
||||
.custom((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
// case: update multiple secrets
|
||||
if (value.length === 0) throw new Error('secrets cannot be an empty array')
|
||||
for (const secret of value) {
|
||||
if (
|
||||
!secret.id
|
||||
) {
|
||||
throw new Error('Each secret must contain a ID property');
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
// case: update 1 secret
|
||||
if (
|
||||
!value.id
|
||||
) {
|
||||
throw new Error('secret must contain a ID property');
|
||||
}
|
||||
} else {
|
||||
throw new Error('secrets must be an object or an array of objects')
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
secretsController.updateSecrets
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
body('secretIds')
|
||||
.exists()
|
||||
.custom((value) => {
|
||||
// case: delete 1 secret
|
||||
if (typeof value === 'string') return true;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// case: delete multiple secrets
|
||||
if (value.length === 0) throw new Error('secrets cannot be an empty array');
|
||||
return value.every((id: string) => typeof id === 'string')
|
||||
}
|
||||
|
||||
throw new Error('secretIds must be a string or an array of strings');
|
||||
})
|
||||
.not()
|
||||
.isEmpty(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
secretsController.deleteSecrets
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
|
@ -11,10 +11,6 @@ import {
|
||||
setIntegrationAuthAccessHelper,
|
||||
} from '../helpers/integration';
|
||||
import { exchangeCode } from '../integrations';
|
||||
import {
|
||||
ENV_DEV,
|
||||
EVENT_PUSH_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
// should sync stuff be here too? Probably.
|
||||
// TODO: move bot functions to IntegrationService.
|
||||
@ -32,22 +28,26 @@ class IntegrationService {
|
||||
* - Create bot sequence for integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - workspace environment
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.code - code
|
||||
*/
|
||||
static async handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
environment: string;
|
||||
}) {
|
||||
await handleOAuthExchangeHelper({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
environment
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
} from '../config';
|
||||
import { getLogger } from '../utils/logger';
|
||||
|
||||
if(TELEMETRY_ENABLED){
|
||||
if(!TELEMETRY_ENABLED){
|
||||
getLogger("backend-main").info([
|
||||
"",
|
||||
"Infisical collects telemetry data about general usage.",
|
||||
"The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.",
|
||||
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
|
||||
"To improve, Infisical collects telemetry data about general usage.",
|
||||
"This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.",
|
||||
"To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.",
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,13 @@ if (SMTP_SECURE) {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mailOpts.secure = true;
|
||||
if (SMTP_HOST.includes('amazonaws.com')) {
|
||||
mailOpts.tls = {
|
||||
ciphers: 'TLSv1.2'
|
||||
}
|
||||
} else {
|
||||
mailOpts.secure = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
4
backend/src/types/express/index.d.ts
vendored
4
backend/src/types/express/index.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
import * as express from 'express';
|
||||
import { ISecret } from '../../models';
|
||||
|
||||
// TODO: fix (any) types
|
||||
declare global {
|
||||
@ -12,7 +13,8 @@ declare global {
|
||||
integration: any;
|
||||
integrationAuth: any;
|
||||
bot: any;
|
||||
secret: any;
|
||||
_secret: any;
|
||||
secrets: any;
|
||||
secretSnapshot: any;
|
||||
serviceToken: any;
|
||||
accessToken: any;
|
||||
|
@ -19,7 +19,6 @@ import {
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_GITHUB_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
} from './integration';
|
||||
import {
|
||||
@ -66,7 +65,6 @@ export {
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_GITHUB_API_URL,
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS,
|
||||
ACTION_ADD_SECRETS,
|
||||
|
@ -11,10 +11,10 @@ const INTEGRATION_VERCEL = 'vercel';
|
||||
const INTEGRATION_NETLIFY = 'netlify';
|
||||
const INTEGRATION_GITHUB = 'github';
|
||||
const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB
|
||||
]);
|
||||
|
||||
// integration types
|
||||
@ -23,22 +23,21 @@ const INTEGRATION_OAUTH2 = 'oauth2';
|
||||
// integration oauth endpoints
|
||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||
const INTEGRATION_VERCEL_TOKEN_URL =
|
||||
'https://api.vercel.com/v2/oauth/access_token';
|
||||
'https://api.vercel.com/v2/oauth/access_token';
|
||||
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
|
||||
const INTEGRATION_GITHUB_TOKEN_URL =
|
||||
'https://github.com/login/oauth/access_token';
|
||||
'https://github.com/login/oauth/access_token';
|
||||
|
||||
// integration apps endpoints
|
||||
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
||||
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
||||
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
||||
const INTEGRATION_GITHUB_API_URL = 'https://api.github.com';
|
||||
|
||||
const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Heroku',
|
||||
slug: 'heroku',
|
||||
image: 'Heroku',
|
||||
image: 'Heroku',
|
||||
isAvailable: true,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_HEROKU,
|
||||
@ -47,8 +46,8 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Vercel',
|
||||
slug: 'vercel',
|
||||
image: 'Vercel',
|
||||
isAvailable: false,
|
||||
image: 'Vercel',
|
||||
isAvailable: true,
|
||||
type: 'vercel',
|
||||
clientId: '',
|
||||
clientSlug: CLIENT_SLUG_VERCEL,
|
||||
@ -57,7 +56,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Netlify',
|
||||
slug: 'netlify',
|
||||
image: 'Netlify',
|
||||
image: 'Netlify',
|
||||
isAvailable: false,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_NETLIFY,
|
||||
@ -66,17 +65,17 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'GitHub',
|
||||
slug: 'github',
|
||||
image: 'GitHub',
|
||||
image: 'GitHub',
|
||||
isAvailable: false,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_GITHUB,
|
||||
docsLink: ''
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
name: 'Google Cloud Platform',
|
||||
slug: 'gcp',
|
||||
image: 'Google Cloud Platform',
|
||||
image: 'Google Cloud Platform',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@ -85,7 +84,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Amazon Web Services',
|
||||
slug: 'aws',
|
||||
image: 'Amazon Web Services',
|
||||
image: 'Amazon Web Services',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@ -94,7 +93,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Microsoft Azure',
|
||||
slug: 'azure',
|
||||
image: 'Microsoft Azure',
|
||||
image: 'Microsoft Azure',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@ -103,7 +102,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Travis CI',
|
||||
slug: 'travisci',
|
||||
image: 'Travis CI',
|
||||
image: 'Travis CI',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@ -112,7 +111,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Circle CI',
|
||||
slug: 'circleci',
|
||||
image: 'Circle CI',
|
||||
image: 'Circle CI',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@ -121,19 +120,18 @@ const INTEGRATION_OPTIONS = [
|
||||
]
|
||||
|
||||
export {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_GITHUB_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_GITHUB_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
};
|
||||
|
22
backend/swagger.ts
Normal file
22
backend/swagger.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
|
||||
|
||||
const doc = {
|
||||
info: {
|
||||
title: 'Infisical API',
|
||||
description: 'List of all available APIs that can be consumed',
|
||||
},
|
||||
host: ['https://infisical.com'],
|
||||
securityDefinitions: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const outputFile = './api-documentation.json';
|
||||
const endpointsFiles = ['./src/app.ts'];
|
||||
|
||||
swaggerAutogen(outputFile, endpointsFiles, doc);
|
@ -16,10 +16,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
FormatDotenv string = "dotenv"
|
||||
FormatJson string = "json"
|
||||
FormatCSV string = "csv"
|
||||
FormatYaml string = "yaml"
|
||||
FormatDotenv string = "dotenv"
|
||||
FormatJson string = "json"
|
||||
FormatCSV string = "csv"
|
||||
FormatYaml string = "yaml"
|
||||
FormatDotEnvExport string = "dotenv-export"
|
||||
)
|
||||
|
||||
// exportCmd represents the export command
|
||||
@ -31,8 +32,8 @@ var exportCmd = &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
toggleDebug(cmd, args)
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
// util.RequireLogin()
|
||||
// util.RequireLocalWorkspaceFile()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
envName, err := cmd.Flags().GetString("env")
|
||||
@ -85,6 +86,8 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
|
||||
switch strings.ToLower(format) {
|
||||
case FormatDotenv:
|
||||
return formatAsDotEnv(envs), nil
|
||||
case FormatDotEnvExport:
|
||||
return formatAsDotEnvExport(envs), nil
|
||||
case FormatJson:
|
||||
return formatAsJson(envs), nil
|
||||
case FormatCSV:
|
||||
@ -92,7 +95,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
|
||||
case FormatYaml:
|
||||
return formatAsYaml(envs), nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml})
|
||||
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport})
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,6 +120,15 @@ func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string {
|
||||
return dotenv
|
||||
}
|
||||
|
||||
// Format environment variables as a dotenv file with export at the beginning
|
||||
func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string {
|
||||
var dotenv string
|
||||
for _, env := range envs {
|
||||
dotenv += fmt.Sprintf("export %s='%s'\n", env.Key, env.Value)
|
||||
}
|
||||
return dotenv
|
||||
}
|
||||
|
||||
func formatAsYaml(envs []models.SingleEnvironmentVariable) string {
|
||||
var dotenv string
|
||||
for _, env := range envs {
|
||||
|
@ -6,6 +6,7 @@ package cmd
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -17,6 +18,7 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/srp"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/manifoldco/promptui"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -31,7 +33,9 @@ var loginCmd = &cobra.Command{
|
||||
PreRun: toggleDebug,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||
if err != nil {
|
||||
if err != nil && strings.Contains(err.Error(), "The specified item could not be found in the keyring") { // if the key can't be found allow them to override
|
||||
log.Debug(err)
|
||||
} else if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
@ -97,7 +101,7 @@ var loginCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
|
||||
}
|
||||
|
||||
log.Infoln("Nice! You are loggin as:", email)
|
||||
color.Green("Nice! You are logged in as: %v", email)
|
||||
|
||||
},
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
|
||||
Short: "Infisical CLI is used to inject environment variables into any process",
|
||||
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
|
||||
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
|
||||
Version: "0.2.0",
|
||||
Version: "0.2.2",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
|
@ -12,8 +12,8 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/fatih/color"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -85,16 +85,50 @@ var runCmd = &cobra.Command{
|
||||
secrets = util.OverrideWithPersonalSecrets(secrets)
|
||||
}
|
||||
|
||||
secretsByKey := getSecretsByKeys(secrets)
|
||||
environmentVariables := make(map[string]string)
|
||||
|
||||
// add all existing environment vars
|
||||
for _, s := range os.Environ() {
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
key := kv[0]
|
||||
value := kv[1]
|
||||
environmentVariables[key] = value
|
||||
}
|
||||
|
||||
// check to see if there are any reserved key words in secrets to inject
|
||||
reservedEnvironmentVariables := []string{"HOME", "PATH", "PS1", "PS2"}
|
||||
for _, reservedEnvName := range reservedEnvironmentVariables {
|
||||
if _, ok := secretsByKey[reservedEnvName]; ok {
|
||||
delete(secretsByKey, reservedEnvName)
|
||||
util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it is a reserved secret name", reservedEnvName))
|
||||
}
|
||||
}
|
||||
|
||||
// now add infisical secrets
|
||||
for k, v := range secretsByKey {
|
||||
environmentVariables[k] = v.Value
|
||||
}
|
||||
|
||||
// turn it back into a list of envs
|
||||
var env []string
|
||||
for key, value := range environmentVariables {
|
||||
s := key + "=" + value
|
||||
env = append(env, s)
|
||||
}
|
||||
|
||||
log.Debugf("injecting the following environment variables into shell: %v", env)
|
||||
|
||||
if cmd.Flags().Changed("command") {
|
||||
command := cmd.Flag("command").Value.String()
|
||||
|
||||
err = executeMultipleCommandWithEnvs(command, secrets)
|
||||
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to execute your chained command")
|
||||
}
|
||||
|
||||
} else {
|
||||
err = executeSingleCommandWithEnvs(args, secrets)
|
||||
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to execute your single command")
|
||||
}
|
||||
@ -111,24 +145,21 @@ func init() {
|
||||
}
|
||||
|
||||
// Will execute a single command and pass in the given secrets into the process
|
||||
func executeSingleCommandWithEnvs(args []string, secrets []models.SingleEnvironmentVariable) error {
|
||||
func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error {
|
||||
command := args[0]
|
||||
argsForCommand := args[1:]
|
||||
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
|
||||
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
|
||||
log.Debugf("executing command: %s %s \n", command, strings.Join(argsForCommand, " "))
|
||||
log.Debugf("Secrets injected: %v", secrets)
|
||||
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
|
||||
|
||||
cmd := exec.Command(command, argsForCommand...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = getAllEnvs(secrets)
|
||||
cmd.Env = env
|
||||
|
||||
return execCmd(cmd)
|
||||
}
|
||||
|
||||
func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleEnvironmentVariable) error {
|
||||
func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error {
|
||||
shell := [2]string{"sh", "-c"}
|
||||
if runtime.GOOS == "windows" {
|
||||
shell = [2]string{"cmd", "/C"}
|
||||
@ -140,12 +171,10 @@ func executeMultipleCommandWithEnvs(fullCommand string, secrets []models.SingleE
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = getAllEnvs(secrets)
|
||||
cmd.Env = env
|
||||
|
||||
numberOfSecretsInjected := fmt.Sprintf("\u2713 Injected %v Infisical secrets into your application process successfully", len(secrets))
|
||||
log.Infof("\x1b[%dm%s\x1b[0m", 32, numberOfSecretsInjected)
|
||||
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
|
||||
log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
|
||||
log.Debugf("Secrets injected: %v", secrets)
|
||||
|
||||
return execCmd(cmd)
|
||||
}
|
||||
@ -175,23 +204,3 @@ func execCmd(cmd *exec.Cmd) error {
|
||||
os.Exit(waitStatus.ExitStatus())
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAllEnvs(envsToInject []models.SingleEnvironmentVariable) []string {
|
||||
env_map := make(map[string]string)
|
||||
|
||||
for _, env := range os.Environ() {
|
||||
splitEnv := strings.Split(env, "=")
|
||||
env_map[splitEnv[0]] = splitEnv[1]
|
||||
}
|
||||
|
||||
for _, env := range envsToInject {
|
||||
env_map[env.Key] = env.Value // overrite any envs with ones to inject if they clash
|
||||
}
|
||||
|
||||
var allEnvs []string
|
||||
for key, value := range env_map {
|
||||
allEnvs = append(allEnvs, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
|
||||
return allEnvs
|
||||
}
|
||||
|
@ -311,14 +311,25 @@ var secretsDeleteCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
secretsCmd.AddCommand(secretsGetCmd)
|
||||
secretsCmd.AddCommand(secretsSetCmd)
|
||||
secretsCmd.AddCommand(secretsDeleteCmd)
|
||||
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
secretsCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||
secretsGetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
}
|
||||
|
||||
secretsCmd.AddCommand(secretsSetCmd)
|
||||
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
}
|
||||
|
||||
secretsCmd.AddCommand(secretsDeleteCmd)
|
||||
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLogin()
|
||||
util.RequireLocalWorkspaceFile()
|
||||
}
|
||||
|
||||
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
rootCmd.AddCommand(secretsCmd)
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,10 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func PrintWarning(message string) {
|
||||
color.Yellow("Warning: %v", message)
|
||||
}
|
||||
|
||||
func PrintMessageAndExit(messages ...string) {
|
||||
if len(messages) > 0 {
|
||||
for _, message := range messages {
|
@ -33,6 +33,7 @@ func GetKeyRing() (keyring.Keyring, error) {
|
||||
LibSecretCollectionName: KEYRING_SERVICE_NAME,
|
||||
KWalletAppID: KEYRING_SERVICE_NAME,
|
||||
KWalletFolder: KEYRING_SERVICE_NAME,
|
||||
KeychainName: "login", // default so user will not be prompted
|
||||
KeychainTrustApplication: true,
|
||||
WinCredPrefix: KEYRING_SERVICE_NAME,
|
||||
FileDir: fmt.Sprintf("~/%s-file-vault", KEYRING_SERVICE_NAME),
|
||||
@ -53,9 +54,10 @@ func GetKeyRing() (keyring.Keyring, error) {
|
||||
}
|
||||
|
||||
func fileKeyringPassphrasePrompt(prompt string) (string, error) {
|
||||
fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password")
|
||||
if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
|
||||
return password, nil
|
||||
} else {
|
||||
fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password")
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s:", prompt)
|
||||
|
@ -28,6 +28,8 @@ services:
|
||||
- ./backend/src:/app/src
|
||||
- ./backend/nodemon.json:/app/nodemon.json
|
||||
- /app/node_modules
|
||||
- ./backend/api-documentation.json:/app/api-documentation.json
|
||||
- ./backend/swagger.ts:/app/swagger.ts
|
||||
command: npm run dev
|
||||
env_file: .env
|
||||
environment:
|
||||
@ -48,6 +50,7 @@ services:
|
||||
- ./frontend/public:/app/public
|
||||
- ./frontend/styles:/app/styles
|
||||
- ./frontend/components:/app/components
|
||||
- ./frontend/ee:/app/ee
|
||||
- ./frontend/locales:/app/locales
|
||||
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js
|
||||
env_file: .env
|
||||
|
4
docs/api-reference/endpoints/secrets/create.mdx
Normal file
4
docs/api-reference/endpoints/secrets/create.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v2/secrets/"
|
||||
---
|
4
docs/api-reference/endpoints/secrets/delete.mdx
Normal file
4
docs/api-reference/endpoints/secrets/delete.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v2/secrets/"
|
||||
---
|
4
docs/api-reference/endpoints/secrets/read.mdx
Normal file
4
docs/api-reference/endpoints/secrets/read.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Read"
|
||||
openapi: "GET /api/v2/secrets/"
|
||||
---
|
4
docs/api-reference/endpoints/secrets/update.mdx
Normal file
4
docs/api-reference/endpoints/secrets/update.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v2/secrets/"
|
||||
---
|
3
docs/api-reference/overview/authentication.mdx
Normal file
3
docs/api-reference/overview/authentication.mdx
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: "Authentication"
|
||||
---
|
3
docs/api-reference/overview/introduction.mdx
Normal file
3
docs/api-reference/overview/introduction.mdx
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: "Introduction"
|
||||
---
|
@ -12,12 +12,12 @@ Export environment variables from the platform into a file format.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Default value |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
|
||||
| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` |
|
||||
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
|
||||
| `--format` | Format of the output file. Accepted values: `dotenv`, `csv` and `json` | `dotenv` |
|
||||
| Option | Description | Default value |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
|
||||
| `--projectId` | Only required if injecting via the [service token method](../token). If you are not using service token, the project id will be automatically retrieved from the `.infisical.json` located at the root of your local project. | `None` |
|
||||
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
|
||||
| `--format` | Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv` and `json` | `dotenv` |
|
||||
|
||||
## Examples
|
||||
|
||||
@ -25,6 +25,9 @@ Export environment variables from the platform into a file format.
|
||||
# Export variables to a .env file
|
||||
infisical export > .env
|
||||
|
||||
# Export variables to a .env file (with export keyword)
|
||||
infisical export --format=dotenv-export > .env
|
||||
|
||||
# Export variables to a CSV file
|
||||
infisical export --format=csv > secrets.csv
|
||||
|
||||
@ -33,4 +36,5 @@ infisical export --format=json > secrets.json
|
||||
|
||||
# Export variables to a YAML file
|
||||
infisical export --format=yaml > secrets.yaml
|
||||
|
||||
```
|
||||
|
@ -2,8 +2,10 @@
|
||||
title: "Activity Logs"
|
||||
---
|
||||
|
||||
Activity logs record all actions going through Infisical including CRUD operations applied to environment variables. They help answer questions like:
|
||||
Activity logs record all actions going through Infisical including who performed which CRUD operations on environment variables and from what IP address. They help answer questions like:
|
||||
|
||||
- Who added or updated environment variables recently?
|
||||
- Did Bob read environment variables last week (if at all)?
|
||||
- What IP address was used for that action?
|
||||
|
||||

|
||||
|
@ -2,4 +2,22 @@
|
||||
title: "Point-in-Time Recovery"
|
||||
---
|
||||
|
||||
Point-in-time (PIT) recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables.
|
||||
Point-in-time recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables.
|
||||
|
||||
## Commits
|
||||
|
||||
Similar to Git, a commit in Infisical is a snapshot of your project's secrets at a specific point in time. You can browse and view your project's snapshots via the "Point-in-Time Recovery" sidebar.
|
||||
|
||||

|
||||

|
||||
|
||||
## Rolling back
|
||||
|
||||
Environment variables can be rolled back to any point in time via the "Rollback to this snapshot" button.
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
Rolling back environment variables to a past snapshot creates a new commit and
|
||||
snapshot at the top of the stack and updates secret versions.
|
||||
</Note>
|
||||
|
14
docs/getting-started/dashboard/secret-versioning.mdx
Normal file
14
docs/getting-started/dashboard/secret-versioning.mdx
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
title: "Secret Versioning"
|
||||
---
|
||||
|
||||
Secret versioning records changes made to every secret.
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
You can copy and paste a secret version value to the "Value" input field "roll
|
||||
back" to that secret version. This creates a new secret version at the top of
|
||||
the stack. We're releasing the ability to press and automatically roll back to
|
||||
a secret version soon.
|
||||
</Note>
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
title: "Secret Versioning"
|
||||
---
|
||||
|
||||
Secret versioning allows an individual environment variable to be rolled back without touching other project environment variables.
|
BIN
docs/images/activity-logs.png
Normal file
BIN
docs/images/activity-logs.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 394 KiB |
BIN
docs/images/email-aws-ses-console.png
Normal file
BIN
docs/images/email-aws-ses-console.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 370 KiB |
BIN
docs/images/email-aws-ses-user.png
Normal file
BIN
docs/images/email-aws-ses-user.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 172 KiB |
BIN
docs/images/pit-commits.png
Normal file
BIN
docs/images/pit-commits.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 271 KiB |
BIN
docs/images/pit-snapshot.png
Normal file
BIN
docs/images/pit-snapshot.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 269 KiB |
BIN
docs/images/pit-snapshots.png
Normal file
BIN
docs/images/pit-snapshots.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 364 KiB |
BIN
docs/images/secret-versioning.png
Normal file
BIN
docs/images/secret-versioning.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 337 KiB |
@ -81,7 +81,7 @@
|
||||
"getting-started/dashboard/project",
|
||||
"getting-started/dashboard/integrations",
|
||||
"getting-started/dashboard/pit-recovery",
|
||||
"getting-started/dashboard/versioning",
|
||||
"getting-started/dashboard/secret-versioning",
|
||||
"getting-started/dashboard/audit-logs",
|
||||
"getting-started/dashboard/token"
|
||||
]
|
||||
|
@ -48,7 +48,7 @@ SMTP_FROM_NAME=Infisical
|
||||
```
|
||||
|
||||
<Info>
|
||||
Remember that you will need to restart Infisical for this to work properly.
|
||||
Remember that you will need to restart Infisical for this to work properly.
|
||||
</Info>
|
||||
|
||||
## Mailgun
|
||||
@ -70,6 +70,28 @@ SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out em
|
||||
SMTP_FROM_NAME=Infisical
|
||||
```
|
||||
|
||||
## AWS SES
|
||||
|
||||
1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console.
|
||||
2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables:
|
||||
|
||||
```
|
||||
SMTP_HOST=smtp.mailgun.org # obtained from credentials page
|
||||
SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
|
||||
SMTP_USERNAME=xxx # your SMTP username
|
||||
SMTP_PASSWORD=xxx # your SMTP password
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=true
|
||||
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
|
||||
SMTP_FROM_NAME=Infisical
|
||||
```
|
||||
|
||||
<Info>
|
||||
Remember that you will need to restart Infisical for this to work properly.
|
||||
</Info>
|
||||
Remember that you will need to restart Infisical for this to work properly.
|
||||
</Info>
|
||||
|
@ -9,7 +9,6 @@ export const initPostHog = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
console.log("Outside of posthog", "POSTHOG_API_KEY", POSTHOG_API_KEY, "POSTHOG_HOST", POSTHOG_HOST)
|
||||
posthog.init(POSTHOG_API_KEY, {
|
||||
api_host: POSTHOG_HOST
|
||||
});
|
||||
|
@ -4,13 +4,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export default function Error({ text }: { text: string }): JSX.Element {
|
||||
return (
|
||||
<div className="relative bg-red-500 opacity-100 border flex flex-row justify-center m-auto items-center w-fit rounded-full mb-4">
|
||||
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
|
||||
<FontAwesomeIcon
|
||||
icon={faExclamationTriangle}
|
||||
className="text-white mt-1.5 mb-2 mx-2"
|
||||
className="text-red mt-1.5 mb-2 mx-2"
|
||||
/>
|
||||
{text && (
|
||||
<p className="relative top-0 text-white mr-2 text-sm py-1">{text}</p>
|
||||
<p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -55,7 +55,7 @@ export default function EventFilter({
|
||||
{selected != '' ? (
|
||||
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
|
||||
) : (
|
||||
<p className="select-none">Select an event</p>
|
||||
<p className="select-none">{String(t("common:select-event"))}</p>
|
||||
)}
|
||||
{selected != '' ? (
|
||||
<FontAwesomeIcon
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faTimeline,
|
||||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
@ -20,7 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import getOrganizations from "~/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
|
||||
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
|
||||
import checkUserAction from "~/pages/api/userActions/checkUserAction";
|
||||
import getUser from "~/pages/api/user/getUser";
|
||||
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
|
||||
import createWorkspace from "~/pages/api/workspace/createWorkspace";
|
||||
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
||||
@ -40,6 +39,7 @@ import Listbox from "./Listbox";
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const crypto = require("crypto");
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const router = useRouter();
|
||||
@ -84,12 +84,31 @@ export default function Layout({ children }: LayoutProps) {
|
||||
});
|
||||
const newWorkspaceId = newWorkspace._id;
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
|
||||
await uploadKeys(
|
||||
newWorkspaceId,
|
||||
myUser._id,
|
||||
ciphertext,
|
||||
nonce
|
||||
);
|
||||
|
||||
if (addAllUsers) {
|
||||
console.log('adding other users')
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: tempLocalStorage("orgData.id"),
|
||||
});
|
||||
orgUsers.map(async (user: any) => {
|
||||
if (user.status == "accepted") {
|
||||
if (user.status == "accepted" && user.email != myUser.email) {
|
||||
const result = await addUserToWorkspace(
|
||||
user.user.email,
|
||||
newWorkspaceId
|
||||
@ -184,6 +203,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
if (
|
||||
userWorkspaces.length == 0 &&
|
||||
router.asPath != "/noprojects" &&
|
||||
!router.asPath.includes("home")&&
|
||||
!router.asPath.includes("settings")
|
||||
) {
|
||||
router.push("/noprojects");
|
||||
@ -191,9 +211,16 @@ export default function Layout({ children }: LayoutProps) {
|
||||
const intendedWorkspaceId = router.asPath
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0];
|
||||
|
||||
if (
|
||||
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId)
|
||||
) {
|
||||
localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
}
|
||||
|
||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||
if (
|
||||
intendedWorkspaceId != "heroku" &&
|
||||
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId) &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
@ -239,14 +266,14 @@ export default function Layout({ children }: LayoutProps) {
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0]
|
||||
) {
|
||||
router.push(
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any]
|
||||
);
|
||||
localStorage.setItem(
|
||||
"projectData.id",
|
||||
`${workspaceMapping[workspaceSelected as any]}`
|
||||
);
|
||||
router.push(
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any]
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
@ -46,7 +46,7 @@ export default function ListBox({
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
{text}
|
||||
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
|
||||
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize">
|
||||
{' '}
|
||||
{selected}
|
||||
</span>
|
||||
@ -69,7 +69,7 @@ export default function ListBox({
|
||||
<Listbox.Option
|
||||
key={personIdx}
|
||||
className={({ active, selected }) =>
|
||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
|
||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
|
||||
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
||||
} ${
|
||||
active && !selected
|
||||
|
@ -1,27 +1,11 @@
|
||||
import React from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
addOverride: (value: OverrideProps) => void;
|
||||
keyName: string;
|
||||
value: string;
|
||||
addOverride: (value: string | undefined, pos: number) => void;
|
||||
pos: number;
|
||||
id: string;
|
||||
comment: string;
|
||||
deleteOverride: (id: string) => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,41 +14,23 @@ interface ToggleProps {
|
||||
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||
* @param {function} obj.setEnabled - change the state of the toggle
|
||||
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||
* @param {string} obj.keyName - key of a certain secret
|
||||
* @param {string} obj.value - value of a certain secret
|
||||
* @param {number} obj.pos - position of a certain secret
|
||||
#TODO: make the secret id persistent?
|
||||
* @param {string} obj.id - id of a certain secret
|
||||
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
|
||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||
* @returns
|
||||
*/
|
||||
export default function Toggle ({
|
||||
enabled,
|
||||
setEnabled,
|
||||
addOverride,
|
||||
keyName,
|
||||
value,
|
||||
pos,
|
||||
id,
|
||||
comment,
|
||||
deleteOverride,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
pos
|
||||
}: ToggleProps): JSX.Element {
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled == false) {
|
||||
addOverride({ id, keyName, value, pos, comment });
|
||||
setSharedToHide([
|
||||
...sharedToHide!,
|
||||
id
|
||||
])
|
||||
addOverride('', pos);
|
||||
} else {
|
||||
deleteOverride(id);
|
||||
addOverride(undefined, pos);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
|
@ -3,7 +3,6 @@ import Image from "next/image";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconProps,
|
||||
} from "@fortawesome/react-fontawesome";
|
||||
|
||||
const classNames = require("classnames");
|
||||
@ -101,7 +100,7 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
<div
|
||||
className={`${
|
||||
props.loading == true ? "opacity-100" : "opacity-0"
|
||||
} absolute flex items-center px-2 duration-200`}
|
||||
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
|
||||
>
|
||||
<Image
|
||||
src="/images/loading/loadingblack.gif"
|
||||
|
@ -109,7 +109,7 @@ const AddProjectMemberDialog = ({
|
||||
selected={email ? email : data[0]}
|
||||
onChange={setEmail}
|
||||
data={data}
|
||||
width="full"
|
||||
isFull={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -8,7 +8,6 @@ import nacl from "tweetnacl";
|
||||
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import { envMapping } from "../../../public/data/frequentConstants";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
@ -34,11 +33,12 @@ const AddServiceTokenDialog = ({
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
serviceTokens,
|
||||
environments,
|
||||
setServiceTokens
|
||||
}) => {
|
||||
const [serviceToken, setServiceToken] = useState("");
|
||||
const [serviceTokenName, setServiceTokenName] = useState("");
|
||||
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
|
||||
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]);
|
||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
|
||||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@ -66,15 +66,13 @@ const AddServiceTokenDialog = ({
|
||||
let newServiceToken = await addServiceToken({
|
||||
name: serviceTokenName,
|
||||
workspaceId,
|
||||
environment: envMapping[serviceTokenEnv],
|
||||
environment: selectedServiceTokenEnv?.slug ? selectedServiceTokenEnv.slug : environments[0]?.name,
|
||||
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
|
||||
console.log('newServiceToken', newServiceToken);
|
||||
|
||||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
||||
setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
|
||||
};
|
||||
@ -103,155 +101,159 @@ const AddServiceTokenDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-50">
|
||||
<div className='z-50'>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||
<Dialog as='div' className='relative' onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
|
||||
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
{serviceToken == "" ? (
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
{serviceToken == '' ? (
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
>
|
||||
{t("section-token:add-dialog.title", {
|
||||
{t('section-token:add-dialog.title', {
|
||||
target: workspaceName,
|
||||
})}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("section-token:add-dialog.description")}
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-token:add-dialog.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-28 mb-2">
|
||||
<div className='max-h-28 mb-2'>
|
||||
<InputField
|
||||
label={t("section-token:add-dialog.name")}
|
||||
label={t('section-token:add-dialog.name')}
|
||||
onChangeHandler={setServiceTokenName}
|
||||
type="varName"
|
||||
type='varName'
|
||||
value={serviceTokenName}
|
||||
placeholder=""
|
||||
placeholder=''
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-28 mb-2">
|
||||
<div className='max-h-28 mb-2'>
|
||||
<ListBox
|
||||
selected={serviceTokenEnv}
|
||||
onChange={setServiceTokenEnv}
|
||||
data={[
|
||||
"Development",
|
||||
"Staging",
|
||||
"Production",
|
||||
"Testing",
|
||||
]}
|
||||
selected={selectedServiceTokenEnv?.name ? selectedServiceTokenEnv?.name : environments[0]?.name}
|
||||
data={environments.map(({ name }) => name)}
|
||||
onChange={(envName) =>
|
||||
setSelectedServiceTokenEnv(
|
||||
environments.find(
|
||||
({ name }) => envName === name
|
||||
) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull={true}
|
||||
text={`${t("common:environment")}: `}
|
||||
text={`${t('common:environment')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
<div className='max-h-28'>
|
||||
<ListBox
|
||||
selected={serviceTokenExpiresIn}
|
||||
onChange={setServiceTokenExpiresIn}
|
||||
data={[
|
||||
"1 day",
|
||||
"7 days",
|
||||
"1 month",
|
||||
"6 months",
|
||||
"12 months",
|
||||
'1 day',
|
||||
'7 days',
|
||||
'1 month',
|
||||
'6 months',
|
||||
'12 months',
|
||||
]}
|
||||
isFull={true}
|
||||
text={`${t("common:expired-in")}: `}
|
||||
text={`${t('common:expired-in')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-max">
|
||||
<div className="mt-6 flex flex-col justify-start w-max">
|
||||
<div className='max-w-max'>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<Button
|
||||
onButtonPressed={() => generateServiceToken()}
|
||||
color="mineshaft"
|
||||
text={t("section-token:add-dialog.add")}
|
||||
textDisabled={t("section-token:add-dialog.add")}
|
||||
size="md"
|
||||
active={serviceTokenName == "" ? false : true}
|
||||
color='mineshaft'
|
||||
text={t('section-token:add-dialog.add')}
|
||||
textDisabled={t('section-token:add-dialog.add')}
|
||||
size='md'
|
||||
active={serviceTokenName == '' ? false : true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
) : (
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
>
|
||||
{t("section-token:add-dialog.copy-service-token")}
|
||||
{t('section-token:add-dialog.copy-service-token')}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t(
|
||||
"section-token:add-dialog.copy-service-token-description"
|
||||
'section-token:add-dialog.copy-service-token-description'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20">
|
||||
<div className='w-full'>
|
||||
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
|
||||
<input
|
||||
type="text"
|
||||
type='text'
|
||||
value={serviceToken}
|
||||
id="serviceToken"
|
||||
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"
|
||||
id='serviceToken'
|
||||
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||
></input>
|
||||
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none">
|
||||
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||
{serviceToken}
|
||||
</div>
|
||||
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
||||
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
|
||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||
>
|
||||
{serviceTokenCopied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="pr-0.5"
|
||||
className='pr-0.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
)}
|
||||
</button>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
|
||||
{t("common:click-to-copy")}
|
||||
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
|
||||
{t('common:click-to-copy')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col justify-start w-max">
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<Button
|
||||
onButtonPressed={() => closeAddServiceTokenModal()}
|
||||
color="mineshaft"
|
||||
text="Close"
|
||||
size="md"
|
||||
color='mineshaft'
|
||||
text='Close'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
145
frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx
Normal file
145
frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { FormEventHandler, Fragment, useEffect, useState } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
|
||||
type FormFields = { name: string; slug: string };
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
isEditMode?: boolean;
|
||||
// on edit mode load up initial values
|
||||
initialValues?: FormFields;
|
||||
onClose: () => void;
|
||||
onCreateSubmit: (data: FormFields) => void;
|
||||
onEditSubmit: (data: FormFields) => void;
|
||||
};
|
||||
|
||||
// TODO: Migrate to better form management and validation. Preferable react-hook-form + yup
|
||||
/**
|
||||
* The dialog modal for when the user wants to create a new workspace
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const AddUpdateEnvironmentDialog = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCreateSubmit,
|
||||
onEditSubmit,
|
||||
initialValues,
|
||||
isEditMode,
|
||||
}: Props) => {
|
||||
const [formInput, setFormInput] = useState<FormFields>({
|
||||
name: '',
|
||||
slug: '',
|
||||
});
|
||||
|
||||
// This use effect can be removed when the unmount is happening from outside the component
|
||||
// When unmount happens outside state gets unmounted also
|
||||
useEffect(() => {
|
||||
setFormInput(initialValues || { name: '', slug: '' });
|
||||
}, [isOpen]);
|
||||
|
||||
// REFACTOR: Move to react-hook-form with yup for better form management
|
||||
const onInputChange = (fieldName: string, fieldValue: string) => {
|
||||
setFormInput((state) => ({ ...state, [fieldName]: fieldValue }));
|
||||
};
|
||||
|
||||
const onFormSubmit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
name: formInput.name.toLowerCase(),
|
||||
slug: formInput.slug.toLowerCase(),
|
||||
};
|
||||
if (isEditMode) {
|
||||
onEditSubmit(data);
|
||||
return;
|
||||
}
|
||||
onCreateSubmit(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative z-20' onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-out duration-150'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 overflow-y-auto z-50'>
|
||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400'
|
||||
>
|
||||
{isEditMode
|
||||
? 'Update environment'
|
||||
: 'Create a new environment'}
|
||||
</Dialog.Title>
|
||||
<form onSubmit={onFormSubmit}>
|
||||
<div className='max-h-28 mt-4'>
|
||||
<InputField
|
||||
label='Environment Name'
|
||||
onChangeHandler={(val) => onInputChange('name', val)}
|
||||
type='varName'
|
||||
value={formInput.name}
|
||||
placeholder=''
|
||||
isRequired
|
||||
// error={error.length > 0}
|
||||
// errorText={error}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28 mt-4'>
|
||||
<InputField
|
||||
label='Environment Slug'
|
||||
onChangeHandler={(val) => onInputChange('slug', val)}
|
||||
type='varName'
|
||||
value={formInput.slug}
|
||||
placeholder=''
|
||||
isRequired
|
||||
// error={error.length > 0}
|
||||
// errorText={error}
|
||||
/>
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 mt-2'>
|
||||
Slugs are shorthands used in cli to access environment
|
||||
</p>
|
||||
<div className='mt-4 max-w-min'>
|
||||
<Button
|
||||
onButtonPressed={() => null}
|
||||
type='submit'
|
||||
color='mineshaft'
|
||||
text={isEditMode ? 'Update' : 'Create'}
|
||||
active={formInput.name !== '' && formInput.slug !== ''}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
104
frontend/components/basic/dialog/DeleteActionModal.tsx
Normal file
104
frontend/components/basic/dialog/DeleteActionModal.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import InputField from '../InputField';
|
||||
|
||||
// REFACTOR: Move all these modals into one reusable one
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onClose: ()=>void;
|
||||
title: string;
|
||||
onSubmit:()=>void;
|
||||
deleteKey?:string;
|
||||
}
|
||||
|
||||
const DeleteActionModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
onSubmit,
|
||||
deleteKey
|
||||
}:Props) => {
|
||||
const [deleteInputField, setDeleteInputField] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
setDeleteInputField("");
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-150'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
||||
</Transition.Child>
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400'
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
This action is irrevertible.
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<InputField
|
||||
isRequired
|
||||
label={`Type ${deleteKey} to delete the resource`}
|
||||
onChangeHandler={(val) => setDeleteInputField(val)}
|
||||
value={deleteInputField}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={onSubmit}
|
||||
disabled={
|
||||
Boolean(deleteKey) && deleteInputField !== deleteKey
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteActionModal;
|
77
frontend/components/basic/dialog/DeleteEnvVar.tsx
Normal file
77
frontend/components/basic/dialog/DeleteEnvVar.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
|
||||
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={() => {}}>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" onClick={onClose} />
|
||||
</Transition.Child>
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
{t('dashboard:sidebar.delete-key-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-start">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-red-700 hover:bg-red-600 px-4 py-2 text-sm font-medium text-bunker-200 hover:text-white text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-mineshaft-500 hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -36,7 +36,7 @@ export default function BottonRightPopup({
|
||||
}: PopupProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||
|
167
frontend/components/basic/table/EnvironmentsTable.tsx
Normal file
167
frontend/components/basic/table/EnvironmentsTable.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { usePopUp } from '../../../hooks/usePopUp';
|
||||
import Button from '../buttons/Button';
|
||||
import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog';
|
||||
import DeleteActionModal from '../dialog/DeleteActionModal';
|
||||
|
||||
type Env = { name: string; slug: string };
|
||||
|
||||
type Props = {
|
||||
data: Env[];
|
||||
onCreateEnv: (arg0: Env) => Promise<void>;
|
||||
onUpdateEnv: (oldSlug: string, arg0: Env) => Promise<void>;
|
||||
onDeleteEnv: (slug: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const EnvironmentTable = ({
|
||||
data = [],
|
||||
onCreateEnv,
|
||||
onDeleteEnv,
|
||||
onUpdateEnv,
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
'createUpdateEnv',
|
||||
'deleteEnv',
|
||||
] as const);
|
||||
|
||||
const onEnvCreateCB = async (env: Env) => {
|
||||
try {
|
||||
await onCreateEnv(env);
|
||||
handlePopUpClose('createUpdateEnv');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onEnvUpdateCB = async (env: Env) => {
|
||||
try {
|
||||
await onUpdateEnv(
|
||||
(popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug,
|
||||
env
|
||||
);
|
||||
handlePopUpClose('createUpdateEnv');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onEnvDeleteCB = async () => {
|
||||
try {
|
||||
await onDeleteEnv(
|
||||
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
|
||||
);
|
||||
handlePopUpClose('deleteEnv');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-row justify-between w-full'>
|
||||
<div className='flex flex-col w-full'>
|
||||
<p className='text-xl font-semibold mb-3'>Project Environments</p>
|
||||
<p className='text-base text-gray-400 mb-4'>
|
||||
Choose which environments will show up in your dashboard like
|
||||
development, staging, production
|
||||
</p>
|
||||
<p className='text-sm mr-1 text-gray-500 self-start'>
|
||||
Note: the text in slugs shows how these environmant should be
|
||||
accessed in CLI.
|
||||
</p>
|
||||
</div>
|
||||
<div className='w-48'>
|
||||
<Button
|
||||
text='Add New Env'
|
||||
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
|
||||
color='mineshaft'
|
||||
icon={faPlus}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1'>
|
||||
<div className='absolute rounded-t-md w-full h-12 bg-white/5'></div>
|
||||
<table className='w-full my-1'>
|
||||
<thead className='text-bunker-300'>
|
||||
<tr>
|
||||
<th className='text-left pl-6 pt-2.5 pb-2'>Name</th>
|
||||
<th className='text-left pl-6 pt-2.5 pb-2'>Slug</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.length > 0 ? (
|
||||
data.map(({ name, slug }) => {
|
||||
return (
|
||||
<tr
|
||||
key={name}
|
||||
className='bg-bunker-800 hover:bg-bunker-800/5 duration-100'
|
||||
>
|
||||
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize'>
|
||||
{name}
|
||||
</td>
|
||||
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300'>
|
||||
{slug}
|
||||
</td>
|
||||
<td className='py-2 border-mineshaft-700 border-t flex'>
|
||||
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
handlePopUpOpen('createUpdateEnv', { name, slug })
|
||||
}
|
||||
color='red'
|
||||
size='icon-sm'
|
||||
icon={faPencil}
|
||||
/>
|
||||
</div>
|
||||
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center'>
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
handlePopUpOpen('deleteEnv', { name, slug })
|
||||
}
|
||||
color='red'
|
||||
size='icon-sm'
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className='text-center pt-7 pb-4 text-bunker-400'
|
||||
>
|
||||
No environmants found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp['deleteEnv'].isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
|
||||
}?`}
|
||||
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
|
||||
onClose={() => handlePopUpClose('deleteEnv')}
|
||||
onSubmit={onEnvDeleteCB}
|
||||
/>
|
||||
<AddUpdateEnvironmentDialog
|
||||
isOpen={popUp.createUpdateEnv.isOpen}
|
||||
isEditMode={Boolean(popUp.createUpdateEnv?.data)}
|
||||
initialValues={popUp?.createUpdateEnv?.data as any}
|
||||
onClose={() => handlePopUpClose('createUpdateEnv')}
|
||||
onCreateSubmit={onEnvCreateCB}
|
||||
onEditSubmit={onEnvUpdateCB}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentTable;
|
@ -3,7 +3,6 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
|
||||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
||||
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
@ -60,7 +59,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
|
||||
{workspaceName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{reverseEnvMapping[row.environment]}
|
||||
{row.environment}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{new Date(row.expiresAt).toUTCString()}
|
||||
|
@ -117,13 +117,13 @@ const UserTable = ({
|
||||
|
||||
return (
|
||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<div className="absolute rounded-t-md w-full h-14 bg-white/5"></div>
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-gray-400">
|
||||
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5"></div>
|
||||
<table className="w-full my-0.5">
|
||||
<thead className="text-gray-400 text-sm font-light">
|
||||
<tr>
|
||||
<th className="text-left pl-6 py-3.5">First Name</th>
|
||||
<th className="text-left pl-6 py-3.5">Last Name</th>
|
||||
<th className="text-left pl-6 py-3.5">Email</th>
|
||||
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
|
||||
<th className="text-left pl-6 py-3.5">LAST NAME</th>
|
||||
<th className="text-left pl-6 py-3.5">EMAIL</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user