1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-21 11:37:53 +00:00

Compare commits

..

146 Commits

Author SHA1 Message Date
a6f480d3f8 increase CLI 2023-01-24 19:59:45 -08:00
0413059fbe patch executeMultipleCommandWithEnvs when no /bin/zsh 2023-01-24 19:59:45 -08:00
65f049f6ac Merge pull request from franky47/patch-1
docs: Fix typo in encryption overview
2023-01-24 19:51:10 -08:00
62f886a3b3 docs: Fix typo in encryption overview 2023-01-25 04:31:04 +01:00
271ca148e3 Make support link clickable 2023-01-24 11:01:49 -08:00
8aa294309f remove icon from support link 2023-01-24 10:53:46 -08:00
ca3233110b add support link for 1 on 1 in docs 2023-01-24 10:52:09 -08:00
1e4f6a4b9d increase CLI 2023-01-24 00:10:42 -08:00
a73fc6de19 add cli version check before every command 2023-01-24 00:08:42 -08:00
0bb750488b fix typo in docs 2023-01-23 23:00:24 -08:00
32f98f83c5 update nav name for development instructions 2023-01-23 22:56:41 -08:00
6943785ce5 remove stay alive 2023-01-23 22:55:14 -08:00
86558a8221 simplify contribution docs 2023-01-23 22:55:14 -08:00
f2c35a302d Merge pull request from akhilmhdh/feat/component-update-1
Infisical component library foundations
2023-01-23 21:47:28 -08:00
0794b6132a auto create user for dev mode 2023-01-23 20:57:59 -08:00
062c287e75 feat(ui): changed to interfonts 2023-01-23 22:10:51 +05:30
e67d68a7b9 feat(ui): implemented basic table component 2023-01-23 22:10:51 +05:30
054acc689a feat(ui): implemented dropdown component 2023-01-23 22:10:51 +05:30
9b95d18b85 feat(ui): implemented switch component 2023-01-23 22:10:51 +05:30
7f9bc77253 feat(ui): added checkbox component 2023-01-23 22:10:51 +05:30
b92907aca6 feat(ui): added textarea component 2023-01-23 22:10:51 +05:30
c4ee03c73b featIui): added menu component 2023-01-23 22:10:51 +05:30
89ba80740b feat: added card and modal component 2023-01-23 22:10:50 +05:30
606a5e5317 feat(ui): added card component 2023-01-23 22:10:50 +05:30
f859bf528e feat(ui): added icon button component, updated secondary button style and added select to barrel export 2023-01-23 22:10:50 +05:30
ad504fa84e feat(ui) added select, spinner components 2023-01-23 22:10:50 +05:30
e7ac74c5a0 feat(ui): implemented form control components 2023-01-23 22:10:50 +05:30
b80504ae00 feat(ui): implemented new input component 2023-01-23 22:10:50 +05:30
68f1887d66 feat(ui): implemented button component 2023-01-23 22:10:50 +05:30
201c8352e3 fix typo in k8 self host 2023-01-22 16:50:45 -08:00
a0f0ffe566 improve i-dev command 2023-01-22 14:03:09 -08:00
4b4e8e2bfc Update docker integration 2023-01-22 12:50:06 -08:00
4db4c172c1 Fix the start guide redirect issue 2023-01-22 00:35:36 -08:00
00fee63ff3 Merge pull request from Infisical/more-integrations
Adjust integration bot authorization sequence
2023-01-22 11:17:15 +07:00
6b80cd6590 Modify case where integration bot was authorized but user didn't finish inputting their PAT -> should result in not sharing keys with bot 2023-01-22 11:12:47 +07:00
840efbdc2f Update API_URL to INFISICAL_API_URL 2023-01-21 13:14:39 -08:00
b91dc9e43e increase cli version 2023-01-21 13:04:14 -08:00
7470cd7af5 Merge pull request from asheliahut/add-domain-env
Allow INFISICAL_URL to use domain for self-hosted
2023-01-21 12:56:24 -08:00
d3a6977938 update docs for change cli api 2023-01-21 12:53:18 -08:00
7cc341ea40 update INFISICAL_DEFAULT_API_URL constant name 2023-01-21 12:26:26 -08:00
5297133c07 Set INFISICAL_URL to nothing 2023-01-21 12:25:56 -08:00
7a6230f2f8 Change INFISICAL_URL to API_URL 2023-01-21 12:24:24 -08:00
ffe66a3b8e Merge pull request from caioluis/main
docs(README): fix typo - Portueguese should be Portuguese
2023-01-21 12:10:23 -08:00
936cd51f29 Update README.md 2023-01-21 11:13:38 -08:00
0c24671d8b Merge pull request from Infisical/more-integrations
Render & Fly.io integrations, reduce page reloads for integrations page.
2023-01-22 00:20:09 +07:00
6969593b38 Fix Mintlify mint.json issue and list Render, Fly.io integrations as availalbe 2023-01-22 00:18:00 +07:00
0c351c0925 Add Render, Fly.io integrations and reduce integrations page reloads 2023-01-22 00:09:37 +07:00
656c408034 docs(README): fix typo - Portueguese should be Portuguese 2023-01-21 15:43:38 +00:00
74fb64bbb9 fix edge case of input same as default clobber env 2023-01-21 01:05:25 -08:00
3af85f9fba fix typo of company name 2023-01-21 00:26:52 -08:00
3c282460b2 Allow INFISCAL_URL to use domain for self-hosted 2023-01-21 00:18:43 -08:00
68b7e6e5ab Merge pull request from alexdanilowicz/patch-1
docs(README): nit typo - Frence should say French
2023-01-20 18:10:42 -08:00
9594157f3e docs(README): nit typo - Frence to French
Update the translations section to say French instead of Frence.
2023-01-20 17:49:43 -08:00
b6ed6ad61e increase cli version 2023-01-19 17:10:03 -08:00
3fc68ffc50 patch secret override for run/export command 2023-01-19 17:05:18 -08:00
0613e1115d Update k8 self host docs 2023-01-19 15:38:06 -08:00
6567c3bddf Fixed the bug with redirect during signup invite 2023-01-18 13:21:30 -08:00
b7115d8862 Merge pull request from akhilmhdh/feat/storybook
feat(frontend): added storybook with tailwind integration
2023-01-18 13:11:42 -08:00
83899bebc8 feat(frontend): added storybook with tailwind integration
chore(frontend): added some required radix components
2023-01-18 23:45:54 +05:30
06803519e6 Reduce page reloads for integrations page 2023-01-18 17:38:52 +07:00
3a6b2084bc Patch GitHub integration for organization repos by including correct owner 2023-01-18 16:33:24 +07:00
2235069e78 Merge pull request from jon4hz/helm
Helm updates
2023-01-17 22:57:25 -08:00
15698c5036 Increase chart version 2023-01-17 22:44:34 -08:00
6ac8e057b0 set frontend env to empty {} 2023-01-17 22:38:56 -08:00
375412b45d Allow mongo connection string based on type 2023-01-17 22:34:19 -08:00
e47530dc71 Allowed upper case for environment names 2023-01-17 22:00:52 -08:00
93150199a4 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-18 10:54:07 +07:00
900f69f336 Uncomment GitHub/Netlify integrations 2023-01-18 10:53:57 +07:00
c556820646 Merge pull request from akhilmhdh/chore/eslint-frontend
airbnb eslint and sorting
2023-01-17 19:05:38 -08:00
18fbe82535 Fixed minor bugs during code cleaning 2023-01-17 19:03:43 -08:00
7ae73d1b62 chore(frontend): added rule to seperate out @app imports and linted 2023-01-17 21:06:14 +05:30
cf7834bfc3 chore(frontend): fixed all eslint errors 2023-01-17 21:06:14 +05:30
9f82e2d836 correct host api for k8 2023-01-16 18:01:20 -08:00
f20af1f5f8 Improve k8 docs and add docs for auto redeploy 2023-01-16 17:29:02 -08:00
8343f8ea0d Update k8 helm chart version 2023-01-16 16:55:24 -08:00
74c0dcd1f5 Remove the use of error channel from go routine 2023-01-16 16:52:59 -08:00
40696e4095 increase CLI version 2023-01-16 15:11:49 -08:00
614a2558f5 Patch IP recognition 2023-01-17 00:50:35 +07:00
56aec216c1 Adjust app-wide API limit 2023-01-17 00:10:11 +07:00
b359fb5f3b Adjust app-wide rate limiter 2023-01-16 23:48:03 +07:00
1fbbbab602 Allow new channel types 2023-01-16 08:45:21 -08:00
89697df85e Increase rate limits for API 2023-01-16 22:09:58 +07:00
37ee8148c6 revert default api domain 2023-01-15 23:57:13 -08:00
9e55102816 Switch to v2/secrets CURD api for cli 2023-01-15 23:56:06 -08:00
b8fa5e8a89 add method to get channel from user agent 2023-01-15 23:55:13 -08:00
3ba636f300 switch k8-operator to secrets v2api 2023-01-15 23:12:11 -08:00
da3742f600 set userid and email based on presence of service token/jwt 2023-01-15 22:03:14 -08:00
35f4d27ab0 Populate service token user 2023-01-15 21:40:19 -08:00
cf123d1887 service token create - change env to slug 2023-01-15 21:40:19 -08:00
b3816bd828 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-16 10:53:16 +07:00
7c7c9dea40 Add Infisical API to README 2023-01-16 10:53:07 +07:00
eabe406ab0 Merge pull request from Infisical/expand-open-api
Fill in more example values for OpenAPI schema
2023-01-16 10:14:50 +07:00
2ae617cda6 Fill in more example values for OpenAPI schema 2023-01-16 10:13:56 +07:00
1b16066335 Merge pull request from Infisical/expand-open-api
Add images to API reference authentication for getting API keys and n…
2023-01-16 10:02:08 +07:00
da251d3d2d Add images to API reference authentication for getting API keys and notes on crypto 2023-01-16 09:58:48 +07:00
818efe61f4 Merge pull request from Infisical/k8-new-service-token-and-auto-redeploy
add auto redeploy, new secrets api, and new service token
2023-01-15 17:03:11 -08:00
9f08b04c92 update secrets-operator helm chart 2023-01-15 17:01:31 -08:00
41d17c930a update kubectl install configs 2023-01-15 17:00:06 -08:00
63f22c554a add auto redeploy, new secrets api, and new service token 2023-01-15 16:47:09 -08:00
cba57cf317 Updated readme.md 2023-01-15 16:44:44 -08:00
9a28e5b4bc Added to auto-redirect to the no projects page 2023-01-15 16:06:44 -08:00
a2689002d3 Merge pull request from akhilmhdh/chore/move-to-src
chore(frontend): changed source code to src folder from root
2023-01-15 14:58:17 -08:00
e7a9b83877 Merge branch 'main' into chore/move-to-src 2023-01-15 14:37:04 -08:00
813db9dbbc Added volumes and deleted logs 2023-01-15 14:25:29 -08:00
72d52c9941 Fixing merge conflicts for the folder structure 2023-01-15 13:42:17 -08:00
4c2b9d4703 Solving merge conflicts 2023-01-15 13:40:03 -08:00
b1f7505f30 Fixed the redirectbug with deleting a certain workspace 2023-01-15 13:31:57 -08:00
63e9d83ba4 chore(frontend): changed source code to src folder from root 2023-01-16 00:34:22 +05:30
1534a47adc Fixed the redirectbug with adding a new workspace 2023-01-15 10:45:41 -08:00
c563548a1c Merge pull request from akhilmhdh/feat/migration-ts-v2
feat(frontend): migrated to ts completed.
2023-01-15 09:57:15 -08:00
a633a3534d Merge pull request from Infisical/expand-open-api
Add new organization endpoints to API reference
2023-01-16 00:04:08 +07:00
992357cbc4 Add new organization endpoints to API reference 2023-01-16 00:00:04 +07:00
ffc3562709 feat(frontend): migrated to ts completed. 2023-01-15 21:34:54 +05:30
f19db530b1 Merge pull request from Infisical/api-keys
API Keys V1
2023-01-15 14:49:21 +07:00
061a9c8583 Fix build errors 2023-01-15 14:47:23 +07:00
b8fbc36b2d Fix faulty import 2023-01-15 14:40:45 +07:00
e364faaffd Complete v1 API Key 2023-01-15 14:34:16 +07:00
b3246778f2 Merge remote-tracking branch 'origin' into api-keys 2023-01-15 14:26:27 +07:00
74b76eda7e Complete v1 API Key 2023-01-15 14:25:23 +07:00
564367d5fd Merge pull request from akhilmhdh/feat/migration-ts
migration(frontend): migrated frontend files to ts execpt dialog component
2023-01-14 16:00:53 -08:00
fd2966610c fix: typo 2023-01-14 19:58:08 +01:00
c23b291f25 fix: mongodb connection 2023-01-14 19:51:49 +01:00
67365e5480 migration(frontend): migrated frontend files except some components to ts 2023-01-15 00:13:25 +05:30
4df205dea6 Fix README 2023-01-14 21:43:36 +07:00
32928bf45c Fix merge conflicts 2023-01-14 21:27:55 +07:00
ea98f9be3c Merge pull request from Infisical/api-docs
API Reference Docs v1
2023-01-14 21:09:00 +07:00
5085376f11 Check v2 workspace membership routes (currently not routed) 2023-01-14 21:07:17 +07:00
e2b4adb2e9 Complete v1 API reference docs, pre-launch 2023-01-14 19:06:43 +07:00
315810bd74 Complete v1 API reference docs, pre-launch 2023-01-14 19:02:12 +07:00
7e9ba3b6e2 Merge branch 'main' of https://github.com/Infisical/infisical 2023-01-13 22:59:54 -08:00
08dd5174b3 Making the dashboard less clunky 2023-01-13 22:59:43 -08:00
e552be0a81 add deployment error check for gamma 2023-01-13 20:42:23 -08:00
b63360813a Continue API reference development 2023-01-14 09:48:13 +07:00
5d8c4ad03f Changed the formulation to secrets and configs 2023-01-13 17:32:36 -08:00
3e6206951e updated Readme with new contributors 2023-01-13 17:19:22 -08:00
9d4ea2dcda Continue api-reference docs 2023-01-13 15:04:46 +07:00
61f767e895 Corrected the telemetery event name 2023-01-12 22:12:23 -08:00
efd5016977 Added frontend for api-keys 2023-01-12 17:08:40 -08:00
0f043605d9 Fix merge conflicts 2023-01-11 10:08:44 +07:00
9ff0b7bc18 Minor changes to README 2023-01-11 10:03:40 +07:00
53502e22f4 fix: comments 2022-12-29 01:35:13 +01:00
d683e385ae fix: add support for custom annotations 2022-12-28 16:36:33 +01:00
4880cd84dc refactor: naming, labels and selectors 2022-12-28 15:42:47 +01:00
da5800c268 fix: allow setting of nodeport 2022-12-28 15:25:47 +01:00
21439761c3 fix: allow frontend service type overrides 2022-12-28 15:16:36 +01:00
bef857a7dc fix: allow image overrides 2022-12-28 15:02:52 +01:00
484 changed files with 47705 additions and 11625 deletions
.eslintignore
.github/workflows
MakefileREADME.md
backend
cli/packages
docker-compose.dev.yml
docs
frontend
.eslintrc.eslintrc.js.prettierrc
.storybook
components
ee
hooks
next.config.jspackage-lock.jsonpackage.json
pages
public
src
components
RouteGuard.tsx
analytics
basic
billing
context/Notifications
dashboard
integrations
navigation
signup
utilities
v2
const.ts
ee
hooks
pages
404.tsx_app.tsx
activity
api
apiKey
auth
bot
environments
files
integrations
organization
serviceToken
user
userActions
workspace
dashboard.tsx
dashboard
email-not-verified.tsxgithub.tsxheroku.tsx
home
index.tsx
integrations
login.tsxnetlify.tsxnoprojects.tsxpassword-reset.tsxrequestnewinvite.tsx
settings
billing
org
personal
project
signup.tsxsignupinvite.tsx
users
vercel.tsxverify-email.tsx
styles
styles
tailwind.config.jstsconfig.json
helm-charts
img
k8-operator

@ -1,3 +1,4 @@
node_modules
built
healthcheck.js
tailwind.config.js

@ -129,4 +129,10 @@ jobs:
- 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
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods
if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed"
exit 1
else
echo "Helm upgrade was successful"
fi

@ -8,7 +8,7 @@ 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
infisical run -- docker-compose -f docker-compose.dev.yml up --build
up-prod:
docker-compose -f docker-compose.yml up --build

@ -3,7 +3,7 @@
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
</h1>
<p align="center">
<p align="center">Open-source, E2EE, simple tool to manage and sync environment variables across your team and infrastructure.</p>
<p align="center">Open-source, E2EE, simple tool to manage secrets and configs across your team and infrastructure.</p>
</p>
<h4 align="center">
@ -24,6 +24,9 @@
<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://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-14.6k-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
</a>
@ -34,17 +37,18 @@
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
**[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync environment variables across their development workflow and infrastructure. It's designed to be simple and take minutes to get going.
**[Infisical](https://infisical.com)** is an open source, E2EE tool to help teams manage and sync secrets and configs across their development workflow and infrastructure. It's designed to be simple and take minutes to get going.
- **[User-Friendly Dashboard](https://infisical.com/docs/getting-started/dashboard/project)** to manage your team's environment variables within projects
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects environment variables into your local workflow
- **[User-Friendly Dashboard](https://infisical.com/docs/getting-started/dashboard/project)** to manage your team's secrets and configs within projects
- **[Language-Agnostic CLI](https://infisical.com/docs/cli/overview)** that pulls and injects esecrets and configs 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 overrides** for environment variables
- **Personal overrides** for secrets and configs
- **[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
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** - manage secrets via HTTPS requests to the platform
- **[Secret Versioning](https://infisical.com/docs/getting-started/dashboard/versioning)** to view the change history for any secret
- **[Activity Logs](https://infisical.com/docs/getting-started/dashboard/audit-logs)** to record every action taken in a project
- **[Point-in-time Secrets Recovery](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** for rolling back to any snapshot of your secrets
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
- 🔜 **Automatic Secret Rotation**
@ -333,10 +337,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o
<!-- 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/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>
<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/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?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/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?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/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?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/Grraahaam"><img src="https://avatars.githubusercontent.com/u/72856427?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Gabriellopes232"><img src="https://avatars.githubusercontent.com/u/74881862?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/cerrussell"><img src="https://avatars.githubusercontent.com/u/80227828?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/imakecodes"><img src="https://avatars.githubusercontent.com/u/35536648?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 available in English and Korean. Help us translate Infisical to your language!
Infisical is currently available in English, Korean, French, and Portuguese (Brazil). Help us translate Infisical to your language!
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).

@ -28,6 +28,7 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
@ -35,6 +36,7 @@
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
@ -3698,8 +3700,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/array-flatten": {
"version": "1.1.1",
@ -6638,7 +6639,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@ -10517,6 +10517,11 @@
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/request-ip": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz",
"integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA=="
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -14980,8 +14985,7 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"array-flatten": {
"version": "1.1.1",
@ -17197,7 +17201,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"requires": {
"argparse": "^2.0.1"
}
@ -19975,6 +19978,11 @@
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
"dev": true
},
"request-ip": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz",
"integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA=="
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

@ -5,7 +5,7 @@
"scripts": {
"start": "npm run build && node build/index.js",
"dev": "nodemon",
"swagger-autogen": "node ./swagger.ts",
"swagger-autogen": "node ./swagger/index.ts",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
@ -94,6 +94,7 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
@ -101,6 +102,7 @@
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",

File diff suppressed because it is too large Load Diff

@ -8,8 +8,9 @@ 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')
const swaggerFile = require('../spec.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require('request-ip');
dotenv.config();
import { PORT, NODE_ENV, SITE_URL } from './config';
@ -41,9 +42,11 @@ import {
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
secret as v2SecretRouter,
secrets as v2SecretsRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
@ -70,6 +73,8 @@ app.use(
})
);
app.use(requestIp.mw())
if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
@ -97,19 +102,21 @@ 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); // stop supporting
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated
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/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
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/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
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);
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
@ -126,7 +133,6 @@ app.use((req, res, next) => {
//* Error Handling Middleware (must be after all routing logic)
app.use(requestErrorHandler)
export const server = app.listen(PORT, () => {
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
});

@ -170,10 +170,11 @@ export const logout = async (req: Request, res: Response) => {
* @param res
* @returns
*/
export const checkAuth = async (req: Request, res: Response) =>
res.status(200).send({
export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({
message: 'Authenticated'
});
}
/**
* Return new token by redeeming refresh token

@ -1,8 +1,11 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import axios from 'axios';
import { readFileSync } from 'fs';
import { IntegrationAuth, Integration } from '../../models';
import {
Integration,
IntegrationAuth,
Bot
} from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations';
@ -44,7 +47,7 @@ export const oAuthExchange = async (
environment: environments[0].slug,
});
} catch (err) {
Sentry.setUser(null);
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get OAuth2 code-token exchange'
@ -56,6 +59,67 @@ export const oAuthExchange = async (
});
};
/**
* Save integration access token as part of integration [integration] for workspace with id [workspaceId]
* @param req
* @param res
*/
export const saveIntegrationAccessToken = async (
req: Request,
res: Response
) => {
// TODO: refactor
let integrationAuth;
try {
const {
workspaceId,
accessToken,
integration
}: {
workspaceId: string;
accessToken: string;
integration: string;
} = req.body;
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId),
integration
}, {
workspace: new Types.ObjectId(workspaceId),
integration
}, {
new: true,
upsert: true
});
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
// encrypt and save integration access token
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessToken,
accessExpiresAt: undefined
});
if (!integrationAuth) throw new Error('Failed to save integration access token');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to save access token for integration'
});
}
return res.status(200).send({
integrationAuth
});
}
/**
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
* @param req
@ -70,7 +134,7 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser(null);
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization applications'
@ -89,15 +153,14 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
const { integrationAuthId } = req.params;
await revokeAccess({
integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser(null);
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete integration authorization'
@ -105,6 +168,6 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => {
}
return res.status(200).send({
message: 'Successfully deleted integration authorization'
integrationAuth
});
}

@ -1,25 +1,43 @@
import { Request, Response } from 'express';
import { readFileSync } from 'fs';
import * as Sentry from '@sentry/node';
import { Integration, Bot, BotKey } from '../../models';
import {
Integration,
Workspace,
Bot,
BotKey
} from '../../models';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface Key {
encryptedKey: string;
nonce: string;
}
/**
* Create/initialize an (empty) integration for integration authorization
* @param req
* @param res
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
let integration;
try {
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
isActive: false,
app: null,
environment: req.integrationAuth.workspace?.environments[0].slug,
integration: req.integrationAuth.integration,
integrationAuth: req.integrationAuth._id
}).save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create integration'
});
}
interface PushSecret {
ciphertextKey: string;
ivKey: string;
tagKey: string;
hashKey: string;
ciphertextValue: string;
ivValue: string;
tagValue: string;
hashValue: string;
type: 'shared' | 'personal';
return res.status(200).send({
integration
});
}
/**
@ -36,12 +54,12 @@ export const updateIntegration = async (req: Request, res: Response) => {
try {
const {
app,
environment,
isActive,
target, // vercel-specific integration param
context, // netlify-specific integration param
siteId // netlify-specific integration param
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
@ -52,9 +70,9 @@ export const updateIntegration = async (req: Request, res: Response) => {
environment,
isActive,
app,
target,
context,
siteId
appId,
targetEnvironment,
owner
},
{
new: true
@ -90,36 +108,15 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
let deletedIntegration;
let integration;
try {
const { integrationId } = req.params;
deletedIntegration = await Integration.findOneAndDelete({
integration = await Integration.findOneAndDelete({
_id: integrationId
});
if (!deletedIntegration) throw new Error('Failed to find integration');
const integrations = await Integration.find({
workspace: deletedIntegration.workspace
});
if (integrations.length === 0) {
// case: no integrations left, deactivate bot
const bot = await Bot.findOneAndUpdate({
workspace: deletedIntegration.workspace
}, {
isActive: false
}, {
new: true
});
if (bot) {
await BotKey.deleteOne({
bot: bot._id
});
}
}
if (!integration) throw new Error('Failed to find integration');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
@ -127,8 +124,8 @@ export const deleteIntegration = async (req: Request, res: Response) => {
message: 'Failed to delete integration'
});
}
return res.status(200).send({
deletedIntegration
integration
});
};

@ -29,12 +29,6 @@ const productToPriceMap = {
cardAuth: STRIPE_PRODUCT_CARD_AUTH
};
/**
* Return organizations that user is part of
* @param req
* @param res
* @returns
*/
export const getOrganizations = async (req: Request, res: Response) => {
let organizations;
try {

@ -317,7 +317,7 @@ export const getWorkspaceServiceTokens = async (
let serviceTokens;
try {
const { workspaceId } = req.params;
// ?? FIX.
serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId

@ -33,7 +33,7 @@ export const createWorkspaceEnvironment = async (
}
workspace?.environments.push({
name: environmentName.toLowerCase(),
name: environmentName,
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
@ -96,7 +96,7 @@ export const renameWorkspaceEnvironment = async (
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName.toLowerCase();
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();

@ -1,3 +1,5 @@
import * as usersController from './usersController';
import * as organizationsController from './organizationsController';
import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
@ -6,6 +8,8 @@ import * as secretsController from './secretsController';
import * as environmentController from './environmentController';
export {
usersController,
organizationsController,
workspaceController,
serviceTokenDataController,
apiKeyDataController,

@ -0,0 +1,296 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
MembershipOrg,
Membership,
Workspace
} from '../../models';
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
/**
* Return memberships for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organization memberships'
#swagger.description = 'Return organization memberships'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"memberships": {
"type": "array",
"items": {
$ref: "#/components/schemas/MembershipOrg"
},
"description": "Memberships of organization"
}
}
}
}
}
}
*/
let memberships;
try {
const { organizationId } = req.params;
memberships = await MembershipOrg.find({
organization: organizationId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization memberships'
});
}
return res.status(200).send({
memberships
});
}
/**
* Update role of membership with id [membershipId] to role [role]
* @param req
* @param res
*/
export const updateOrganizationMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update organization membership'
#swagger.description = 'Update organization membership'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of organization membership to update",
"required": true,
"type": "string"
}
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "Role of organization membership - either owner, admin, or member",
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/MembershipOrg",
"description": "Updated organization membership"
}
}
}
}
}
}
*/
let membership;
try {
const { membershipId } = req.params;
const { role } = req.body;
membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
role
}, {
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update organization membership'
});
}
return res.status(200).send({
membership
});
}
/**
* Delete organization membership with id [membershipId]
* @param req
* @param res
* @returns
*/
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete organization membership'
#swagger.description = 'Delete organization membership'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of organization membership to delete",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/MembershipOrg",
"description": "Deleted organization membership"
}
}
}
}
}
}
*/
let membership;
try {
const { membershipId } = req.params;
// delete organization membership
membership = await deleteMembershipOrg({
membershipOrgId: membershipId
});
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
return res.status(200).send({
membership
});
}
/**
* Return workspaces for organization with id [organizationId] that user has
* access to
* @param req
* @param res
*/
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return projects in organization that user is part of'
#swagger.description = 'Return projects in organization that user is part of'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaces": {
"type": "array",
"items": {
$ref: "#/components/schemas/Project"
},
"description": "Projects of organization"
}
}
}
}
}
}
*/
let workspaces;
try {
const { organizationId } = req.params;
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization workspaces'
});
}
return res.status(200).send({
workspaces
});
}

@ -16,6 +16,7 @@ import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services';
import { postHogClient } from '../../services';
import { BadRequestError } from '../../utils/errors';
import { getChannelFromUserAgent } from '../../utils/posthog';
/**
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
@ -23,7 +24,59 @@ import { BadRequestError } from '../../utils/errors';
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
/*
#swagger.summary = 'Create new secret(s)'
#swagger.description = 'Create one or many secrets for a given project and environment.'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of project",
},
"environment": {
"type": "string",
"description": "Environment within project"
},
"secrets": {
$ref: "#/components/schemas/CreateSecret",
"description": "Secret(s) to create - object or array of objects"
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secrets": {
"type": "array",
"items": {
$ref: "#/components/schemas/Secret"
},
"description": "Newly-created secrets for the given project and environment"
}
}
}
}
}
}
*/
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment } = req.body;
let toAdd;
@ -142,7 +195,7 @@ export const createSecrets = async (req: Request, res: Response) => {
numberOfSecrets: toAdd.length,
environment,
workspaceId,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
channel: channel,
userAgent: req.headers?.['user-agent']
}
});
@ -161,10 +214,49 @@ export const createSecrets = async (req: Request, res: Response) => {
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Read secrets'
#swagger.description = 'Read secrets from a project and environment'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['environment'] = {
"description": "Environment within project",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secrets": {
"type": "array",
"items": {
$ref: "#/components/schemas/Secret"
},
"description": "Secrets for the given project and environment"
}
}
}
}
}
}
*/
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
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
@ -189,17 +281,17 @@ export const getSecrets = async (req: Request, res: Response) => {
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const readAction = await EELogService.createActionSecret({
name: ACTION_READ_SECRETS,
userId: req.user._id.toString(),
userId: userId,
workspaceId: workspaceId as string,
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId: req.user._id.toString(),
userId: userId,
workspaceId: workspaceId as string,
actions: [readAction],
channel,
@ -231,8 +323,53 @@ export const getSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const updateSecrets = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update secret(s)'
#swagger.description = 'Update secret(s)'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secrets": {
$ref: "#/components/schemas/UpdateSecret",
"description": "Secret(s) to update - object or array of objects"
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secrets": {
"type": "array",
"items": {
$ref: "#/components/schemas/Secret"
},
"description": "Updated secrets"
}
}
}
}
}
}
*/
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type
interface PatchSecret {
id: string;
@ -381,7 +518,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,
workspaceId: key,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
channel: channel,
userAgent: req.headers?.['user-agent']
}
});
@ -403,7 +540,51 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
/*
#swagger.summary = 'Delete secret(s)'
#swagger.description = 'Delete one or many secrets by their ID(s)'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretIds": {
"type": "string",
"description": "ID(s) of secrets - string or array of strings"
},
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secrets": {
"type": "array",
"items": {
$ref: "#/components/schemas/Secret"
},
"description": "Deleted secrets"
}
}
}
}
}
}
*/
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id);
await Secret.deleteMany({
@ -463,7 +644,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,
workspaceId: key,
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
channel: channel,
userAgent: req.headers?.['user-agent']
}
});

@ -0,0 +1,109 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
User,
MembershipOrg
} from '../../models';
/**
* Return the current user.
* @param req
* @param res
* @returns
*/
export const getMe = async (req: Request, res: Response) => {
/*
#swagger.summary = "Retrieve the current user on the request"
#swagger.description = "Retrieve the current user on the request"
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
$ref: "#/components/schemas/CurrentUser",
"description": "Current user on request"
}
}
}
}
}
}
*/
let user;
try {
user = await User
.findById(req.user._id)
.select('+publicKey +encryptedPrivateKey +iv +tag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get current user'
});
}
return res.status(200).send({
user
});
}
/**
* Return organizations that the current user is part of.
* @param req
* @param res
*/
export const getMyOrganizations = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organizations that current user is part of'
#swagger.description = 'Return organizations that current user is part of'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organizations": {
"type": "array",
"items": {
$ref: "#/components/schemas/Organization"
},
"description": "Organizations that user is part of"
}
}
}
}
}
}
*/
let organizations;
try {
organizations = (
await MembershipOrg.find({
user: req.user._id
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get current user's organizations"
});
}
return res.status(200).send({
organizations
});
}

@ -1,7 +1,9 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Workspace,
Secret,
Membership,
MembershipOrg,
Integration,
@ -174,6 +176,34 @@ export const pullSecrets = async (req: Request, res: Response) => {
};
export const getWorkspaceKey = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return encrypted project key'
#swagger.description = 'Return encrypted project key'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "array",
"items": {
$ref: "#/components/schemas/ProjectKey"
},
"description": "Encrypted project key for the given project"
}
}
}
}
*/
let key;
try {
const { workspaceId } = req.params;
@ -219,4 +249,222 @@ export const getWorkspaceServiceTokenData = async (
return res.status(200).send({
serviceTokenData
});
}
/**
* Return memberships for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return project memberships'
#swagger.description = 'Return project memberships'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"memberships": {
"type": "array",
"items": {
$ref: "#/components/schemas/Membership"
},
"description": "Memberships of project"
}
}
}
}
}
}
*/
let memberships;
try {
const { workspaceId } = req.params;
memberships = await Membership.find({
workspace: workspaceId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace memberships'
});
}
return res.status(200).send({
memberships
});
}
/**
* Update role of membership with id [membershipId] to role [role]
* @param req
* @param res
* @returns
*/
export const updateWorkspaceMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update project membership'
#swagger.description = 'Update project membership'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of project membership to update",
"required": true,
"type": "string"
}
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "Role of membership - either admin or member",
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/Membership",
"description": "Updated membership"
}
}
}
}
}
}
*/
let membership;
try {
const {
membershipId
} = req.params;
const { role } = req.body;
membership = await Membership.findByIdAndUpdate(
membershipId,
{
role
}, {
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace membership'
});
}
return res.status(200).send({
membership
});
}
/**
* Delete workspace membership with id [membershipId]
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete project membership'
#swagger.description = 'Delete project membership'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of project membership to delete",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/Membership",
"description": "Deleted membership"
}
}
}
}
}
}
*/
let membership;
try {
const {
membershipId
} = req.params;
membership = await Membership.findByIdAndDelete(membershipId);
if (!membership) throw new Error('Failed to delete workspace membership');
await Key.deleteMany({
receiver: membership.user,
workspace: membership.workspace
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace membership'
});
}
return res.status(200).send({
membership
});
}

@ -10,6 +10,51 @@ import { EESecretService } from '../../services';
* @param res
*/
export const getSecretVersions = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return secret versions'
#swagger.description = 'Return secret versions'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['secretId'] = {
"description": "ID of secret",
"required": true,
"type": "string"
}
#swagger.parameters['offset'] = {
"description": "Number of versions to skip",
"required": false,
"type": "string"
}
#swagger.parameters['limit'] = {
"description": "Maximum number of versions to return",
"required": false,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
"type": "object",
"properties": {
"secretVersions": {
"type": "array",
"items": {
$ref: "#/components/schemas/SecretVersion"
},
"description": "Secret versions"
}
}
}
}
}
}
*/
let secretVersions;
try {
const { secretId } = req.params;
@ -44,6 +89,54 @@ import { EESecretService } from '../../services';
* @returns
*/
export const rollbackSecretVersion = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Roll back secret to a version.'
#swagger.description = 'Roll back secret to a version.'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['secretId'] = {
"description": "ID of secret",
"required": true,
"type": "string"
}
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"version": {
"type": "integer",
"description": "Version of secret to roll back to"
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
"type": "object",
"properties": {
"secret": {
"type": "object",
$ref: "#/components/schemas/Secret",
"description": "Secret rolled back to"
}
}
}
}
}
}
*/
let secret;
try {
const { secretId } = req.params;

@ -19,6 +19,51 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
* @param res
*/
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return project secret snapshot ids'
#swagger.description = 'Return project secret snapshots ids'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['offset'] = {
"description": "Number of secret snapshots to skip",
"required": false,
"type": "string"
}
#swagger.parameters['limit'] = {
"description": "Maximum number of secret snapshots to return",
"required": false,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
"type": "object",
"properties": {
"secretSnapshots": {
"type": "array",
"items": {
$ref: "#/components/schemas/SecretSnapshot"
},
"description": "Project secret snapshots"
}
}
}
}
}
}
*/
let secretSnapshots;
try {
const { workspaceId } = req.params;
@ -78,16 +123,66 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
* @returns
*/
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.'
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"version": {
"type": "integer",
"description": "Version of secret snapshot to roll back to",
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
"type": "object",
"properties": {
"secrets": {
"type": "array",
"items": {
$ref: "#/components/schemas/Secret"
},
"description": "Secrets rolled back to"
}
}
}
}
}
}
*/
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');
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
@ -231,6 +326,72 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
* @returns
*/
export const getWorkspaceLogs = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return project (audit) logs'
#swagger.description = 'Return project (audit) logs'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
"required": true,
"type": "string"
}
#swagger.parameters['userId'] = {
"description": "ID of project member",
"required": false,
"type": "string"
}
#swagger.parameters['offset'] = {
"description": "Number of logs to skip",
"required": false,
"type": "string"
}
#swagger.parameters['limit'] = {
"description": "Maximum number of logs to return",
"required": false,
"type": "string"
}
#swagger.parameters['sortBy'] = {
"description": "Order to sort the logs by",
"schema": {
"type": "string",
"@enum": ["oldest", "recent"]
},
"required": false
}
#swagger.parameters['actionNames'] = {
"description": "Names of log actions (comma-separated)",
"required": false,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
"type": "object",
"properties": {
"logs": {
"type": "array",
"items": {
$ref: "#/components/schemas/Log"
},
"description": "Project logs"
}
}
}
}
}
}
*/
let logs
try {
const { workspaceId } = req.params;

@ -41,17 +41,17 @@ const logSchema = new Schema<ILog>(
ref: 'Action',
required: true
}],
channel: {
channel: {
type: String,
enum: ['web', 'cli', 'auto'],
enum: ['web', 'cli', 'auto', 'k8-operator', 'other'],
required: true
},
ipAddress: {
type: String
}
}, {
timestamps: true
}
timestamps: true
}
);
const Log = model<ILog>('Log', logSchema);

@ -12,7 +12,7 @@ import { ADMIN, MEMBER } from '../../../variables';
router.get(
'/:secretId/secret-versions',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -27,7 +27,7 @@ router.get(
router.post(
'/:secretId/secret-versions/rollback',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]

@ -12,7 +12,7 @@ import { workspaceController } from '../../controllers/v1';
router.get(
'/:workspaceId/secret-snapshots',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -40,7 +40,7 @@ router.get(
router.post(
'/:workspaceId/secret-snapshots/rollback',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -54,7 +54,7 @@ router.post(
router.get(
'/:workspaceId/logs',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]

@ -16,49 +16,66 @@ import {
AccountNotFoundError,
ServiceTokenDataNotFoundError,
APIKeyDataNotFoundError,
UnauthorizedRequestError
UnauthorizedRequestError,
BadRequestError
} from '../utils/errors';
// TODO 1: check if API key works
// TODO 2: optimize middleware
/**
* Validate that auth token value [authTokenValue] falls under one of
* accepted auth modes [acceptedAuthModes].
*
* @param {Object} obj
* @param {String} obj.authTokenValue - auth token value (e.g. JWT or service token value)
* @param {String[]} obj.acceptedAuthModes - accepted auth modes (e.g. jwt, serviceToken)
* @returns {String} authMode - auth mode
* @param {Object} obj.headers - HTTP request headers object
*/
const validateAuthMode = ({
authTokenValue,
headers,
acceptedAuthModes
}: {
authTokenValue: string;
acceptedAuthModes: string[];
headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[]
}) => {
let authMode;
try {
switch (authTokenValue.split('.', 1)[0]) {
// TODO: refactor middleware
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];
let authTokenType, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
}
if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey';
authTokenValue = apiKey;
}
if (typeof authHeader === 'string') {
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
const [tokenType, tokenValue] = <[string, string]>authHeader.split(' ', 2) ?? [null, null]
if (tokenType === null)
throw BadRequestError({ message: `Missing Authorization Header in the request header.` });
if (tokenType.toLowerCase() !== 'bearer')
throw BadRequestError({ message: `The provided authentication type '${tokenType}' is not supported.` });
if (tokenValue === null)
throw BadRequestError({ message: 'Missing Authorization Body in the request header.' });
switch (tokenValue.split('.', 1)[0]) {
case 'st':
authMode = 'serviceToken';
break;
case 'ak':
authMode = 'apiKey';
authTokenType = 'serviceToken';
break;
default:
authMode = 'jwt';
break;
authTokenType = 'jwt';
}
if (!acceptedAuthModes.includes(authMode))
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
} catch (err) {
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
authTokenValue = tokenValue;
}
return authMode;
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
return ({
authTokenType,
authTokenValue
});
}
/**
@ -91,7 +108,7 @@ const getAuthUserPayload = async ({
message: 'Failed to authenticate JWT token'
});
}
return user;
}
@ -113,7 +130,7 @@ const getAuthSTDPayload = async ({
// TODO: optimize double query
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
@ -131,14 +148,14 @@ const getAuthSTDPayload = async ({
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag');
.select('+encryptedKey +iv +tag').populate('user');
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
}
return serviceTokenData;
}
@ -156,11 +173,11 @@ const getAuthAPIKeyPayload = async ({
let user;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey');
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
@ -175,14 +192,14 @@ const getAuthAPIKeyPayload = async ({
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
user = apiKeyData.user;
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
}
return user;
}
@ -275,12 +292,12 @@ const createToken = ({
}
};
export {
export {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload,
createToken,
issueTokens,
clearTokens
createToken,
issueTokens,
clearTokens
};

@ -127,7 +127,6 @@ const syncIntegrationsHelper = async ({
}) => {
let integrations;
try {
integrations = await Integration.find({
workspace: workspaceId,
isActive: true,
@ -142,7 +141,7 @@ const syncIntegrationsHelper = async ({
workspaceId: integration.workspace.toString(),
environment: integration.environment
});
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
if (!integrationAuth) throw new Error('Failed to find integration auth');
@ -316,7 +315,7 @@ const setIntegrationAuthAccessHelper = async ({
}: {
integrationAuthId: string;
accessToken: string;
accessExpiresAt: Date;
accessExpiresAt: Date | undefined;
}) => {
let integrationAuth;
try {

@ -7,6 +7,7 @@ import { Membership, Key } from '../models';
* @param {Object} obj
* @param {String} obj.userId - id of user to validate
* @param {String} obj.workspaceId - id of workspace
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/
const validateMembership = async ({
userId,

@ -1,6 +1,42 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { MembershipOrg, Workspace, Membership, Key } from '../models';
/**
* Validate that user with id [userId] is a member of organization with id [organizationId]
* and has at least one of the roles in [acceptedRoles]
*
*/
const validateMembership = async ({
userId,
organizationId,
acceptedRoles
}: {
userId: string;
organizationId: string;
acceptedRoles: string[];
}) => {
let membership;
try {
membership = await MembershipOrg.findOne({
user: new Types.ObjectId(userId),
organization: new Types.ObjectId(organizationId)
});
if (!membership) throw new Error('Failed to find organization membership');
if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate organization membership role');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate organization membership');
}
return membership;
}
/**
* Return organization membership matching criteria specified in
* query [queryObj]
@ -84,6 +120,8 @@ const deleteMembershipOrg = async ({
_id: membershipOrgId
});
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
// delete keys associated with organization membership
if (deletedMembershipOrg?.user) {
// case: organization membership had a registered user
@ -117,4 +155,9 @@ const deleteMembershipOrg = async ({
return deletedMembershipOrg;
};
export { findMembershipOrg, addMembershipsOrg, deleteMembershipOrg };
export {
validateMembership,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg
};

@ -1,38 +1,43 @@
import rateLimit from 'express-rate-limit';
// 300 requests per 15 minutes
// 120 requests per minute
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 450,
windowMs: 60 * 1000,
max: 240,
standardHeaders: true,
legacyHeaders: false,
skip: (request) => {
return request.path === '/healthcheck' || request.path === '/api/status'
},
keyGenerator: (req, res) => {
return req.clientIp
}
});
// 5 requests per hour
const signupLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
// 10 requests per minute
const authLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
}
});
// 10 requests per hour
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 25,
standardHeaders: true,
legacyHeaders: false
});
// 5 requests per hour
const passwordLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
}
});
export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter };
export {
apiLimiter,
authLimiter,
passwordLimiter
};

@ -8,6 +8,7 @@ import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
DatabaseService.initDatabase(MONGO_URL);
@ -23,3 +24,5 @@ if (NODE_ENV !== 'test') {
environment: NODE_ENV
});
}
createTestUserForDevelopment()

@ -7,9 +7,13 @@ import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
/**
@ -29,10 +33,11 @@ const getApps = async ({
}) => {
interface App {
name: string;
siteId?: string;
appId?: string;
owner?: string;
}
let apps: App[]; // TODO: add type and define payloads for apps
let apps: App[];
try {
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
@ -48,13 +53,21 @@ const getApps = async ({
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
integrationAuth,
accessToken
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
integrationAuth,
accessToken
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken
});
break;
@ -69,7 +82,7 @@ const getApps = async ({
};
/**
* Return list of names of apps for Heroku integration
* Return list of apps for Heroku integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Heroku API
* @returns {Object[]} apps - names of Heroku apps
@ -141,17 +154,15 @@ const getAppsVercel = async ({
};
/**
* Return list of names of sites for Netlify integration
* Return list of sites for Netlify integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Netlify API
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsNetlify = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
@ -166,7 +177,7 @@ const getAppsNetlify = async ({
apps = res.map((a: any) => ({
name: a.name,
siteId: a.site_id
appId: a.site_id
}));
} catch (err) {
Sentry.setUser(null);
@ -178,17 +189,15 @@ const getAppsNetlify = async ({
};
/**
* Return list of names of repositories for Github integration
* Return list of repositories for Github integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Netlify API
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsGithub = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
@ -199,13 +208,16 @@ const getAppsGithub = async ({
const repos = (await octokit.request(
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
{}
{
per_page: 100
}
)).data;
apps = repos
.filter((a:any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name
name: a.name,
owner: a.owner.login
})
);
} catch (err) {
@ -217,4 +229,94 @@ const getAppsGithub = async ({
return apps;
};
/**
* Return list of services for Render integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Render API
* @returns {Object[]} apps - names and ids of Render services
* @returns {String} apps.name - name of Render service
* @returns {String} apps.appId - id of Render service
*/
const getAppsRender = async ({
accessToken
}: {
accessToken: string;
}) => {
let apps: any;
try {
const res = (
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
).data;
apps = res
.map((a: any) => ({
name: a.service.name,
appId: a.service.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Render services');
}
return apps;
}
/**
* Return list of apps for Fly.io integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Fly.io API
* @returns {Object[]} apps - names and ids of Fly.io apps
* @returns {String} apps.name - name of Fly.io apps
*/
const getAppsFlyio = async ({
accessToken
}: {
accessToken: string;
}) => {
let apps;
try {
const query = `
query($role: String) {
apps(type: "container", first: 400, role: $role) {
nodes {
id
name
hostname
}
}
}
`;
const res = (await axios({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
headers: {
'Authorization': 'Bearer ' + accessToken
},
data: {
query,
variables: {
role: null
}
}
})).data.data.apps.nodes;
apps = res
.map((a: any) => ({
name: a.name
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Fly.io apps');
}
return apps;
}
export { getApps };

@ -1,6 +1,11 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { IIntegrationAuth, IntegrationAuth, Integration } from '../models';
import {
IIntegrationAuth,
IntegrationAuth,
Integration,
Bot,
BotKey
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
@ -15,6 +20,7 @@ const revokeAccess = async ({
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let deletedIntegrationAuth;
try {
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
@ -28,7 +34,7 @@ const revokeAccess = async ({
break;
}
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id
});
@ -42,6 +48,8 @@ const revokeAccess = async ({
Sentry.captureException(err);
throw new Error('Failed to delete integration authorization');
}
return deletedIntegrationAuth;
};
export { revokeAccess };

@ -1,4 +1,4 @@
import axios, { AxiosError } from 'axios';
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
// import * as sodium from 'libsodium-wrappers';
@ -10,9 +10,13 @@ import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
import { access, appendFile } from 'fs';
@ -21,8 +25,6 @@ import { access, appendFile } from 'fs';
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.app - app in integration
* @param {Object} obj.target - (optional) target (environment) in integration
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for integration
*/
@ -69,6 +71,20 @@ const syncSecrets = async ({
accessToken
});
break;
case INTEGRATION_RENDER:
await syncSecretsRender({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
secrets,
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
@ -78,10 +94,11 @@ const syncSecrets = async ({
};
/**
* Sync/push [secrets] to Heroku [app]
* Sync/push [secrets] to Heroku app named [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Heroku integration
*/
const syncSecretsHeroku = async ({
integration,
@ -129,7 +146,7 @@ const syncSecretsHeroku = async ({
};
/**
* Sync/push [secrets] to Heroku [app]
* Sync/push [secrets] to Vercel project named [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
@ -174,7 +191,7 @@ const syncSecretsVercel = async ({
))
.data
.envs
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
.map(async (secret: VercelSecret) => (await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
@ -201,7 +218,7 @@ const syncSecretsVercel = async ({
key: key,
value: secrets[key],
type: 'encrypted',
target: [integration.target]
target: [integration.targetEnvironment]
});
}
});
@ -216,7 +233,7 @@ const syncSecretsVercel = async ({
key: key,
value: secrets[key],
type: 'encrypted',
target: [integration.target]
target: [integration.targetEnvironment]
});
}
} else {
@ -226,7 +243,7 @@ const syncSecretsVercel = async ({
key: key,
value: res[key].value,
type: 'encrypted',
target: [integration.target],
target: [integration.targetEnvironment],
});
}
});
@ -287,11 +304,12 @@ const syncSecretsVercel = async ({
}
/**
* Sync/push [secrets] to Netlify site [app]
* Sync/push [secrets] to Netlify site with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {Object} obj.accessToken - access token for Netlify integration
*/
const syncSecretsNetlify = async ({
integration,
@ -323,7 +341,7 @@ const syncSecretsNetlify = async ({
const getParams = new URLSearchParams({
context_name: 'all', // integration.context or all
site_id: integration.siteId
site_id: integration.appId
});
const res = (await axios.get(
@ -354,7 +372,7 @@ const syncSecretsNetlify = async ({
key,
values: [{
value: secrets[key],
context: integration.context
context: integration.targetEnvironment
}]
});
} else {
@ -365,15 +383,15 @@ const syncSecretsNetlify = async ({
[value.context]: value
}), {});
if (integration.context in contexts) {
if (integration.targetEnvironment in contexts) {
// case: Netlify secret value exists in integration context
if (secrets[key] !== contexts[integration.context].value) {
if (secrets[key] !== contexts[integration.targetEnvironment].value) {
// case: Infisical and Netlify secret values are different
// -> update Netlify secret context and value
updateSecrets.push({
key,
values: [{
context: integration.context,
context: integration.targetEnvironment,
value: secrets[key]
}]
});
@ -384,7 +402,7 @@ const syncSecretsNetlify = async ({
updateSecrets.push({
key,
values: [{
context: integration.context,
context: integration.targetEnvironment,
value: secrets[key]
}]
});
@ -402,7 +420,7 @@ const syncSecretsNetlify = async ({
const numberOfValues = res[key].values.length;
res[key].values.forEach((value: NetlifyValue) => {
if (value.context === integration.context) {
if (value.context === integration.targetEnvironment) {
if (numberOfValues <= 1) {
// case: Netlify secret value has less than 1 context -> delete secret
deleteSecrets.push(key);
@ -412,7 +430,7 @@ const syncSecretsNetlify = async ({
key,
values: [{
id: value.id,
context: integration.context,
context: integration.targetEnvironment,
value: value.value
}]
});
@ -423,7 +441,7 @@ const syncSecretsNetlify = async ({
});
const syncParams = new URLSearchParams({
site_id: integration.siteId
site_id: integration.appId
});
if (newSecrets.length > 0) {
@ -492,11 +510,12 @@ const syncSecretsNetlify = async ({
}
/**
* Sync/push [secrets] to GitHub [repo]
* Sync/push [secrets] to GitHub repo with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for GitHub integration
*/
const syncSecretsGitHub = async ({
integration,
@ -530,21 +549,20 @@ const syncSecretsGitHub = async ({
auth: accessToken
});
const user = (await octokit.request('GET /user', {})).data;
// const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets/public-key',
{
owner: user.login,
owner: integration.owner,
repo: integration.app
}
)).data;
// // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
const encryptedSecrets: GitHubSecretRes = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets',
{
owner: user.login,
owner: integration.owner,
repo: integration.app
}
))
@ -560,7 +578,7 @@ const syncSecretsGitHub = async ({
await octokit.request(
'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: user.login,
owner: integration.owner,
repo: integration.app,
secret_name: key
}
@ -590,7 +608,7 @@ const syncSecretsGitHub = async ({
await octokit.request(
'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: user.login,
owner: integration.owner,
repo: integration.app,
secret_name: key,
encrypted_value: encryptedSecret,
@ -606,4 +624,175 @@ const syncSecretsGitHub = async ({
}
};
/**
* Sync/push [secrets] to Render service with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Render integration
*/
const syncSecretsRender = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
await axios.put(
`${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`,
Object.keys(secrets).map((key) => ({
key,
value: secrets[key]
})),
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Render');
}
}
/**
* Sync/push [secrets] to Fly.io app
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Render integration
*/
const syncSecretsFlyio = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
// set secrets
const SetSecrets = `
mutation($input: SetSecretsInput!) {
setSecrets(input: $input) {
release {
id
version
reason
description
user {
id
email
name
}
evaluationId
createdAt
}
}
}
`;
await axios({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
headers: {
'Authorization': 'Bearer ' + accessToken
},
data: {
query: SetSecrets,
variables: {
input: {
appId: integration.app,
secrets: Object.entries(secrets).map(([key, value]) => ({ key, value }))
}
}
}
});
// get secrets
interface FlyioSecret {
name: string;
digest: string;
createdAt: string;
}
const GetSecrets = `query ($appName: String!) {
app(name: $appName) {
secrets {
name
digest
createdAt
}
}
}`;
const getSecretsRes = (await axios({
method: 'post',
url: INTEGRATION_FLYIO_API_URL,
headers: {
'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json'
},
data: {
query: GetSecrets,
variables: {
appName: integration.app
}
}
})).data.data.app.secrets;
const deleteSecretsKeys = getSecretsRes
.filter((secret: FlyioSecret) => !(secret.name in secrets))
.map((secret: FlyioSecret) => secret.name);
// unset (delete) secrets
const DeleteSecrets = `mutation($input: UnsetSecretsInput!) {
unsetSecrets(input: $input) {
release {
id
version
reason
description
user {
id
email
name
}
evaluationId
createdAt
}
}
}`;
await axios({
method: 'post',
url: INTEGRATION_FLYIO_API_URL,
headers: {
'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json'
},
data: {
query: DeleteSecrets,
variables: {
input: {
appId: integration.app,
keys: deleteSecretsKeys
}
}
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Fly.io');
}
}
export { syncSecrets };

@ -2,6 +2,8 @@ import requireAuth from './requireAuth';
import requireBotAuth from './requireBotAuth';
import requireSignupAuth from './requireSignupAuth';
import requireWorkspaceAuth from './requireWorkspaceAuth';
import requireMembershipAuth from './requireMembershipAuth';
import requireMembershipOrgAuth from './requireMembershipOrgAuth';
import requireOrganizationAuth from './requireOrganizationAuth';
import requireIntegrationAuth from './requireIntegrationAuth';
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
@ -16,6 +18,8 @@ export {
requireBotAuth,
requireSignupAuth,
requireWorkspaceAuth,
requireMembershipAuth,
requireMembershipOrgAuth,
requireOrganizationAuth,
requireIntegrationAuth,
requireIntegrationAuthorizationAuth,

@ -7,7 +7,6 @@ import {
getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth';
import { BadRequestError } from '../utils/errors';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -31,37 +30,28 @@ const requireAuth = ({
acceptedAuthModes: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if (AUTH_TOKEN_TYPE === null)
return next(BadRequestError({ message: `Missing Authorization Header in the request header.` }))
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer')
return next(BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }))
if (AUTH_TOKEN_VALUE === null)
return next(BadRequestError({ message: 'Missing Authorization Body in the request header' }))
// validate auth token against
const authMode = validateAuthMode({
authTokenValue: AUTH_TOKEN_VALUE,
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authTokenType, authTokenValue } = validateAuthMode({
headers: req.headers,
acceptedAuthModes
});
if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
// attach auth payloads
switch (authMode) {
switch (authTokenType) {
case 'serviceToken':
req.serviceTokenData = await getAuthSTDPayload({
authTokenValue: AUTH_TOKEN_VALUE
authTokenValue
});
break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({
authTokenValue: AUTH_TOKEN_VALUE
authTokenValue
});
break;
default:
req.user = await getAuthUserPayload({
authTokenValue: AUTH_TOKEN_VALUE
authTokenValue
});
break;
}

@ -1,10 +1,12 @@
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth } from '../models';
import { IntegrationAuth, IWorkspace } from '../models';
import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
/**
* Validate if user on request is a member of workspace with proper roles associated
* with the integration authorization on request params.
@ -14,17 +16,20 @@ import { UnauthorizedRequestError } from '../utils/errors';
*/
const requireIntegrationAuthorizationAuth = ({
acceptedRoles,
attachAccessToken = true
attachAccessToken = true,
location = 'params'
}: {
acceptedRoles: string[];
attachAccessToken?: boolean;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { integrationAuthId } = req.params;
const { integrationAuthId } = req[location];
const integrationAuth = await IntegrationAuth.findOne({
_id: integrationAuthId
}).select(
})
.populate<{ workspace: IWorkspace }>('workspace')
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
@ -34,7 +39,7 @@ const requireIntegrationAuthorizationAuth = ({
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integrationAuth.workspace.toString(),
workspaceId: integrationAuth.workspace._id.toString(),
acceptedRoles
});

@ -0,0 +1,59 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
Membership,
} from '../models';
import { validateMembership } from '../helpers/membership';
type req = 'params' | 'body' | 'query';
/**
* Validate membership with id [membershipId] and that user with id
* [req.user._id] can modify that membership.
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
*/
const requireMembershipAuth = ({
acceptedRoles,
location = 'params'
}: {
acceptedRoles: string[];
location?: req;
}) => {
return async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const { membershipId } = req[location];
const membership = await Membership.findById(membershipId);
if (!membership) throw new Error('Failed to find target membership');
const userMembership = await Membership.findOne({
workspace: membership.workspace
});
if (!userMembership) throw new Error('Failed to validate own membership')
const targetMembership = await validateMembership({
userId: req.user._id.toString(),
workspaceId: membership.workspace.toString(),
acceptedRoles
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate workspace membership'
}));
}
}
}
export default requireMembershipAuth;

@ -0,0 +1,49 @@
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
MembershipOrg
} from '../models';
import { validateMembership } from '../helpers/membershipOrg';
type req = 'params' | 'body' | 'query';
/**
* Validate (organization) membership id [membershipId] and that user with id
* [req.user._id] can modify that membership.
* @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted organization roles
* @param {String[]} obj.location - location of [membershipId] on request (e.g. params, body) for parsing
*/
const requireMembershipOrgAuth = ({
acceptedRoles,
location = 'params'
}: {
acceptedRoles: string[];
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { membershipId } = req[location];
const membershipOrg = await MembershipOrg.findById(membershipId);
if (!membershipOrg) throw new Error('Failed to find target organization membership');
const targetMembership = await validateMembership({
userId: req.user._id.toString(),
organizationId: membershipOrg.organization.toString(),
acceptedRoles
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate organization membership'
}));
}
}
}
export default requireMembershipOrgAuth;

@ -17,10 +17,10 @@ const requireServiceTokenDataAuth = ({
const serviceTokenData = await ServiceTokenData
.findById(req[location].serviceTokenDataId)
.select('+encryptedKey +iv +tag');
.select('+encryptedKey +iv +tag').populate('user');
if (!serviceTokenData) {
return next(AccountNotFoundError({message: 'Failed to locate service token data'}));
return next(AccountNotFoundError({ message: 'Failed to locate service token data' }));
}
if (req.user) {
@ -31,9 +31,9 @@ const requireServiceTokenDataAuth = ({
acceptedRoles
});
}
req.serviceTokenData = serviceTokenData;
next();
}
}

@ -3,7 +3,9 @@ import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
} from '../variables';
export interface IIntegration {
@ -12,19 +14,19 @@ export interface IIntegration {
environment: string;
isActive: boolean;
app: string;
target: string;
context: string;
siteId: string;
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
owner: string;
targetEnvironment: string;
appId: string;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
integrationAuth: Types.ObjectId;
}
const integrationSchema = new Schema<IIntegration>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
environment: {
type: String,
@ -39,18 +41,18 @@ const integrationSchema = new Schema<IIntegration>(
type: String,
default: null
},
target: {
// vercel-specific target (environment)
appId: { // (new)
// id of app in provider
type: String,
default: null
},
context: {
// netlify-specific context (deploy)
targetEnvironment: { // (new)
// target environment
type: String,
default: null
},
siteId: {
// netlify-specific site (app) id
owner: {
// github-specific repo owner-login
type: String,
default: null
},
@ -60,7 +62,9 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
],
required: true
},

@ -9,7 +9,7 @@ import {
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
teamId: string;
accountId: string;
refreshCiphertext?: string;
@ -24,8 +24,9 @@ export interface IIntegrationAuth {
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
required: true
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
integration: {
type: String,

@ -29,19 +29,19 @@ const workspaceSchema = new Schema<IWorkspace>({
],
default: [
{
name: "development",
name: "Development",
slug: "dev"
},
{
name: "test",
name: "Test",
slug: "test"
},
{
name: "staging",
name: "Staging",
slug: "staging"
},
{
name: "production",
name: "Production",
slug: "prod"
}
],

@ -3,13 +3,13 @@ const router = express.Router();
import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware';
import { authController } from '../../controllers/v1';
import { loginLimiter } from '../../helpers/rateLimiter';
import { authLimiter } from '../../helpers/rateLimiter';
router.post('/token', validateRequest, authController.getNewToken);
router.post(
'/login1',
loginLimiter,
authLimiter,
body('email').exists().trim().notEmpty(),
body('clientPublicKey').exists().trim().notEmpty(),
validateRequest,
@ -18,7 +18,7 @@ router.post(
router.post(
'/login2',
loginLimiter,
authLimiter,
body('email').exists().trim().notEmpty(),
body('clientProof').exists().trim().notEmpty(),
validateRequest,
@ -27,11 +27,13 @@ router.post(
router.post(
'/logout',
authLimiter,
requireAuth({
acceptedAuthModes: ['jwt']
}),
authController.logout
);
router.post(
'/checkAuth',
requireAuth({

@ -3,12 +3,27 @@ const router = express.Router();
import {
requireAuth,
requireIntegrationAuth,
requireIntegrationAuthorizationAuth,
validateRequest
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('integrationAuthId').exists().trim(),
validateRequest,
integrationController.createIntegration
);
router.patch(
'/:integrationId',
requireAuth({
@ -18,12 +33,12 @@ router.patch(
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationId').exists().trim(),
body('isActive').exists().isBoolean(),
body('app').exists().trim(),
body('environment').exists().trim(),
body('isActive').exists().isBoolean(),
body('target').exists(),
body('context').exists(),
body('siteId').exists(),
body('appId').exists(),
body('targetEnvironment').exists(),
body('owner').exists(),
validateRequest,
integrationController.updateIntegration
);

@ -34,6 +34,22 @@ router.post(
integrationAuthController.oAuthExchange
);
router.post(
'/access-token',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('workspaceId').exists().trim().notEmpty(),
body('accessToken').exists().trim().notEmpty(),
body('integration').exists().trim().notEmpty(),
validateRequest,
integrationAuthController.saveIntegrationAccessToken
);
router.get(
'/:integrationAuthId/apps',
requireAuth({

@ -4,7 +4,9 @@ import { body, param } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware';
import { membershipController } from '../../controllers/v1';
router.get( // used for CLI (deprecate)
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
router.get( // used for old CLI (deprecate)
'/:workspaceId/connect',
requireAuth({
acceptedAuthModes: ['jwt']

@ -9,7 +9,7 @@ import {
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { organizationController } from '../../controllers/v1';
router.get(
router.get( // deprecated (moved to api/v2/users/me/organizations)
'/',
requireAuth({
acceptedAuthModes: ['jwt']
@ -41,7 +41,7 @@ router.get(
organizationController.getOrganization
);
router.get(
router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships)
'/:organizationId/users',
requireAuth({
acceptedAuthModes: ['jwt']
@ -56,7 +56,7 @@ router.get(
);
router.get(
'/:organizationId/my-workspaces',
'/:organizationId/my-workspaces', // deprecated (moved to api/v2/organizations/:organizationId/workspaces)
requireAuth({
acceptedAuthModes: ['jwt']
}),

@ -3,11 +3,11 @@ const router = express.Router();
import { body } from 'express-validator';
import { requireSignupAuth, validateRequest } from '../../middleware';
import { signupController } from '../../controllers/v1';
import { signupLimiter } from '../../helpers/rateLimiter';
import { authLimiter } from '../../helpers/rateLimiter';
router.post(
'/email/signup',
signupLimiter,
authLimiter,
body('email').exists().trim().notEmpty().isEmail(),
validateRequest,
signupController.beginEmailSignup
@ -15,7 +15,7 @@ router.post(
router.post(
'/email/verify',
signupLimiter,
authLimiter,
body('email').exists().trim().notEmpty().isEmail(),
body('code').exists().trim().notEmpty(),
validateRequest,
@ -24,7 +24,7 @@ router.post(
router.post(
'/complete-account/signup',
signupLimiter,
authLimiter,
requireSignupAuth,
body('email').exists().trim().notEmpty().isEmail(),
body('firstName').exists().trim().notEmpty(),
@ -42,7 +42,7 @@ router.post(
router.post(
'/complete-account/invite',
signupLimiter,
authLimiter,
requireSignupAuth,
body('email').exists().trim().notEmpty().isEmail(),
body('firstName').exists().trim().notEmpty(),

@ -1,14 +1,18 @@
import secret from './secret'; // stop-supporting
import secrets from './secrets';
import users from './users';
import organizations from './organizations';
import workspace from './workspace';
import secret from './secret'; // deprecated
import secrets from './secrets';
import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData';
import environment from "./environment"
export {
users,
organizations,
workspace,
secret,
secrets,
workspace,
serviceTokenData,
apiKeyData,
environment

@ -0,0 +1,80 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireOrganizationAuth,
requireMembershipOrgAuth,
validateRequest
} from '../../middleware';
import { body, param, query } from 'express-validator';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { organizationsController } from '../../controllers/v2';
// TODO: /POST to create membership
router.get(
'/:organizationId/memberships',
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationMemberships
);
router.patch(
'/:organizationId/memberships/:membershipId',
param('organizationId').exists().trim(),
param('membershipId').exists().trim(),
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN]
}),
organizationsController.updateOrganizationMembership
);
router.delete(
'/:organizationId/memberships/:membershipId',
param('organizationId').exists().trim(),
param('membershipId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN]
}),
organizationsController.deleteOrganizationMembership
);
router.get(
'/:organizationId/workspaces',
param('organizationId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED]
}),
organizationsController.getOrganizationWorkspaces
);
export default router;

@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
import { secretController } from '../../controllers/v2';
// note to devs: stop supporting
// note to devs: stop supporting these routes [deprecated]
const router = express.Router();

@ -61,7 +61,7 @@ router.post(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -76,7 +76,7 @@ router.get(
query('environment').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -115,7 +115,7 @@ router.patch(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -143,7 +143,7 @@ router.delete(
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]

@ -0,0 +1,24 @@
import express from 'express';
const router = express.Router();
import {
requireAuth
} from '../../middleware';
import { usersController } from '../../controllers/v2';
router.get(
'/me',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
usersController.getMe
);
router.get(
'/me/organizations',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
usersController.getMyOrganizations
);
export default router;

@ -3,6 +3,7 @@ const router = express.Router();
import { body, param, query } from 'express-validator';
import {
requireAuth,
requireMembershipAuth,
requireWorkspaceAuth,
validateRequest
} from '../../middleware';
@ -44,7 +45,7 @@ router.get(
router.get(
'/:workspaceId/encrypted-key',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
@ -67,4 +68,54 @@ router.get(
workspaceController.getWorkspaceServiceTokenData
);
// TODO: /POST to create membership and re-route inviting user to workspace there
router.get( // new - TODO: rewire dashboard to this route
'/:workspaceId/memberships',
param('workspaceId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
workspaceController.getWorkspaceMemberships
);
router.patch( // TODO - rewire dashboard to this route
'/:workspaceId/memberships/:membershipId',
param('workspaceId').exists().trim(),
param('membershipId').exists().trim(),
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
}),
requireMembershipAuth({
acceptedRoles: [ADMIN]
}),
workspaceController.updateWorkspaceMembership
);
router.delete( // TODO - rewire dashboard to this route
'/:workspaceId/memberships/:membershipId',
param('workspaceId').exists().trim(),
param('membershipId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
}),
requireMembershipAuth({
acceptedRoles: [ADMIN]
}),
workspaceController.deleteWorkspaceMembership
);
export default router;

@ -122,7 +122,7 @@ class IntegrationService {
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessToken - access token
* @param {String} obj.accessExpiresAt - expiration date of access token
* @param {Date} obj.accessExpiresAt - expiration date of access token
* @returns {IntegrationAuth} - updated integration auth
*/
static async setIntegrationAuthAccess({
@ -132,7 +132,7 @@ class IntegrationService {
}: {
integrationAuthId: string;
accessToken: string;
accessExpiresAt: Date;
accessExpiresAt: Date | undefined;
}) {
return await setIntegrationAuthAccessHelper({
integrationAuthId,

@ -5,9 +5,11 @@ import { ISecret } from '../../models';
declare global {
namespace Express {
interface Request {
clientIp: any;
user: any;
workspace: any;
membership: any;
targetMembership: any;
organization: any;
membershipOrg: any;
integration: any;

@ -0,0 +1,140 @@
/************************************************************************************************
*
* Attention: The credentials below are only for development purposes, it should never be used for production
*
************************************************************************************************/
import { NODE_ENV } from "../config"
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
import { Types } from 'mongoose';
export const createTestUserForDevelopment = async () => {
if (NODE_ENV === "development") {
const testUserEmail = "test@localhost.local"
const testUserPassword = "testInfisical1"
const testUserId = "63cefa6ec8d3175601cfa980"
const testWorkspaceId = "63cefb15c8d3175601cfa989"
const testOrgId = "63cefb15c8d3175601cfa985"
const testMembershipId = "63cefb159185d9aa3ef0cf35"
const testMembershipOrgId = "63cefb159185d9aa3ef0cf31"
const testWorkspaceKeyId = "63cf48f0225e6955acec5eff"
const testUser = {
_id: testUserId,
email: testUserEmail,
refreshVersion: 0,
encryptedPrivateKey: 'ITMdDXtLoxib4+53U/qzvIV/T/UalRwimogFCXv/UsulzEoiKM+aK2aqOb0=',
firstName: 'Jake',
iv: '9fp0dZHI+UuHeKkWMDvD6w==',
lastName: 'Moni',
publicKey: 'cf44BhkybbBfsE0fZHe2jvqtCj6KLXvSq4hVjV0svzk=',
salt: 'd8099dc70958090346910fb9639262b83cf526fc9b4555a171b36a9e1bcd0240',
tag: 'bQ/UTghqcQHRoSMpLQD33g==',
verifier: '12271fcd50937ca4512e1e3166adaf9d9fc7a5cd0e4c4cb3eda89f35572ede4d9eef23f64aef9220367abff9437b0b6fa55792c442f177201d87051cf77dadade254ff667170440327355fb7d6ac4745d4db302f4843632c2ed5919ebdcff343287a4cd552255d9e3ce81177edefe089617b7616683901475d393405f554634b9bf9230c041ac85624f37a60401be20b78044932580ae0868323be3749fbf856df1518153ba375fec628275f0c445f237446ea4aa7f12c1aa1d6b5fd74b7f2e88d062845a19819ec63f2d2ed9e9f37c055149649461d997d2ae1482f53b04f9de7493efbb9686fb19b2d559b9aa2b502c22dec83f9fc43290dfea89a1dc6f03580b3642b3824513853e81a441be9a0b2fde2231bac60f3287872617a36884697805eeea673cf1a351697834484ada0f282e4745015c9c2928d61e6d092f1b9c3a27eda8413175d23bb2edae62f82ccaf52bf5a6a90344a766c7e4ebf65dae9ae90b2ad4ae65dbf16e3a6948e429771cc50307ae86d454f71a746939ed061f080dd3ae369c1a0739819aca17af46a085bac1f2a5d936d198e7951a8ac3bb38b893665fe7312835abd3f61811f81efa2a8761af5070085f9b6adcca80bf9b0d81899c3d41487fba90728bb24eceb98bd69770360a232624133700ceb4d153f2ad702e0a5b7dfaf97d20bc8aa71dc8c20024a58c06a8fecdad18cb5a2f89c51eaf7'
}
const testWorkspaceKey = {
_id: new Types.ObjectId(testWorkspaceKeyId),
workspace: testWorkspaceId,
encryptedKey: '96ZIRSU21CjVzIQ4Yp994FGWQvDdyK3gq+z+NCaJLK0ByTlvUePmf+AYGFJjkAdz',
nonce: '1jhCGqg9Wx3n0OtVxbDgiYYGq4S3EdgO',
sender: '63cefa6ec8d3175601cfa980',
receiver: '63cefa6ec8d3175601cfa980',
}
const testWorkspace = {
_id: new Types.ObjectId(testWorkspaceId),
name: 'Example Project',
organization: testOrgId,
environments: [
{
_id: '63cefb15c8d3175601cfa98a',
name: 'Development',
slug: 'dev'
},
{
_id: '63cefb15c8d3175601cfa98b',
name: 'Test',
slug: 'test'
},
{
_id: '63cefb15c8d3175601cfa98c',
name: 'Staging',
slug: 'staging'
},
{
_id: '63cefb15c8d3175601cfa98d',
name: 'Production',
slug: 'prod'
}
],
}
const testOrg = {
_id: testOrgId,
name: 'Jake\'s organization'
}
const testMembershipOrg = {
_id: testMembershipOrgId,
organization: testOrgId,
role: 'owner',
status: 'accepted',
user: testUserId,
}
const testMembership = {
_id: testMembershipId,
role: 'admin',
user: testUserId,
workspace: testWorkspaceId
}
try {
// create user if not exist
const userInDB = await User.findById(testUserId)
if (!userInDB) {
await User.create(testUser)
}
// create org if not exist
const orgInDB = await Organization.findById(testOrgId)
if (!orgInDB) {
await Organization.create(testOrg)
}
// create membership org if not exist
const membershipOrgInDB = await MembershipOrg.findById(testMembershipOrgId)
if (!membershipOrgInDB) {
await MembershipOrg.create(testMembershipOrg)
}
// create membership
const membershipInDB = await Membership.findById(testMembershipId)
if (!membershipInDB) {
await Membership.create(testMembership)
}
// create workspace if not exist
const workspaceInDB = await Workspace.findById(testWorkspaceId)
if (!workspaceInDB) {
await Workspace.create(testWorkspace)
}
// create workspace key if not exist
const workspaceKeyInDB = await Key.findById(testWorkspaceKeyId)
if (!workspaceKeyInDB) {
await Key.create(testWorkspaceKey)
}
/* eslint-disable no-console */
console.info(`DEVELOPMENT MODE DETECTED: You may login with test user with email: ${testUserEmail} and password: ${testUserPassword}`)
/* eslint-enable no-console */
} catch (e) {
/* eslint-disable no-console */
console.error(`Unable to create test user while booting up [err=${e}]`)
/* eslint-enable no-console */
}
}
}

@ -0,0 +1,15 @@
const CLI_USER_AGENT_NAME = "cli"
const K8_OPERATOR_AGENT_NAME = "k8-operator"
export const getChannelFromUserAgent = function (userAgent: string | undefined) {
if (userAgent == undefined) {
return "other"
} else if (userAgent == CLI_USER_AGENT_NAME) {
return "cli"
} else if (userAgent == K8_OPERATOR_AGENT_NAME) {
return "k8-operator"
} else if (userAgent.toLowerCase().includes('mozilla')) {
return "web"
} else {
return "other"
}
}

@ -10,6 +10,8 @@ import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU_TOKEN_URL,
@ -19,6 +21,8 @@ import {
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS
} from './integration';
import {
@ -56,6 +60,8 @@ export {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU_TOKEN_URL,
@ -65,6 +71,8 @@ export {
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_ADD_SECRETS,

@ -10,11 +10,15 @@ const INTEGRATION_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify';
const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_SET = new Set([
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
]);
// integration types
@ -32,23 +36,25 @@ const INTEGRATION_GITHUB_TOKEN_URL =
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_RENDER_API_URL = 'https://api.render.com';
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
const INTEGRATION_OPTIONS = [
{
name: 'Heroku',
slug: 'heroku',
image: 'Heroku',
image: 'Heroku.png',
isAvailable: true,
type: 'oauth2',
type: 'oauth',
clientId: CLIENT_ID_HEROKU,
docsLink: ''
},
{
name: 'Vercel',
slug: 'vercel',
image: 'Vercel',
image: 'Vercel.png',
isAvailable: true,
type: 'vercel',
type: 'oauth',
clientId: '',
clientSlug: CLIENT_SLUG_VERCEL,
docsLink: ''
@ -56,26 +62,43 @@ const INTEGRATION_OPTIONS = [
{
name: 'Netlify',
slug: 'netlify',
image: 'Netlify',
isAvailable: false,
type: 'oauth2',
image: 'Netlify.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_NETLIFY,
docsLink: ''
},
{
name: 'GitHub',
slug: 'github',
image: 'GitHub',
isAvailable: false,
type: 'oauth2',
image: 'GitHub.png',
isAvailable: true,
type: 'oauth',
clientId: CLIENT_ID_GITHUB,
docsLink: ''
},
{
name: 'Render',
slug: 'render',
image: 'Render.png',
isAvailable: true,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Fly.io',
slug: 'flyio',
image: 'Flyio.svg',
isAvailable: false,
type: 'pat',
clientId: '',
docsLink: ''
},
{
name: 'Google Cloud Platform',
slug: 'gcp',
image: 'Google Cloud Platform',
image: 'Google Cloud Platform.png',
isAvailable: false,
type: '',
clientId: '',
@ -84,7 +107,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Amazon Web Services',
slug: 'aws',
image: 'Amazon Web Services',
image: 'Amazon Web Services.png',
isAvailable: false,
type: '',
clientId: '',
@ -93,7 +116,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Microsoft Azure',
slug: 'azure',
image: 'Microsoft Azure',
image: 'Microsoft Azure.png',
isAvailable: false,
type: '',
clientId: '',
@ -102,7 +125,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Travis CI',
slug: 'travisci',
image: 'Travis CI',
image: 'Travis CI.png',
isAvailable: false,
type: '',
clientId: '',
@ -111,7 +134,7 @@ const INTEGRATION_OPTIONS = [
{
name: 'Circle CI',
slug: 'circleci',
image: 'Circle CI',
image: 'Circle CI.png',
isAvailable: false,
type: '',
clientId: '',
@ -124,6 +147,8 @@ export {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_HEROKU_TOKEN_URL,
@ -133,5 +158,7 @@ export {
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_OPTIONS
};

@ -1,22 +0,0 @@
// 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);

212
backend/swagger/index.ts Normal file

@ -0,0 +1,212 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
const fs = require('fs').promises;
const yaml = require('js-yaml');
/**
* Generates OpenAPI specs for all Infisical API endpoints:
* - spec.json in /backend for api-serving
* - spec.yaml in /docs for API reference
*/
const generateOpenAPISpec = async () => {
const doc = {
info: {
title: 'Infisical API',
description: 'List of all available APIs that can be consumed',
},
host: ['https://infisical.com'],
servers: [
{
url: 'https://infisical.com',
description: 'Production server'
},
{
url: 'http://localhost:8080',
description: 'Local server'
}
],
securityDefinitions: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: "This security definition uses the HTTP 'bearer' scheme, which allows the client to authenticate using a JSON Web Token (JWT) that is passed in the Authorization header of the request."
},
apiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'X-API-Key',
description: 'This security definition uses an API key, which is passed in the header of the request as the value of the "X-API-Key" header. The client must provide a valid key in order to access the API.'
}
},
definitions: {
CurrentUser: {
_id: '',
email: 'johndoe@gmail.com',
firstName: 'John',
lastName: 'Doe',
publicKey: 'johns_nacl_public_key',
encryptedPrivateKey: 'johns_enc_nacl_private_key',
iv: 'iv_of_enc_nacl_private_key',
tag: 'tag_of_enc_nacl_private_key',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
Membership: {
user: {
_id: '',
email: 'johndoe@gmail.com',
firstName: 'John',
lastName: 'Doe',
publicKey: 'johns_nacl_public_key',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
workspace: '',
role: 'admin'
},
MembershipOrg: {
user: {
_id: '',
email: 'johndoe@gmail.com',
firstName: 'John',
lastName: 'Doe',
publicKey: 'johns_nacl_public_key',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
organization: '',
role: 'owner',
status: 'accepted'
},
Organization: {
_id: '',
name: 'Acme Corp.',
customerId: ''
},
Project: {
name: 'My Project',
organization: '',
environments: [{
name: 'development',
slug: 'dev'
}]
},
ProjectKey: {
encryptedkey: '',
nonce: '',
sender: {
publicKey: 'senders_nacl_public_key'
},
receiver: '',
workspace: ''
},
CreateSecret: {
type: 'shared',
secretKeyCiphertext: '',
secretKeyIV: '',
secretKeyTag: '',
secretValueCiphertext: '',
secretValueIV: '',
secretValueTag: '',
secretCommentCiphertext: '',
secretCommentIV: '',
secretCommentTag: ''
},
UpdateSecret: {
id: '',
secretKeyCiphertext: '',
secretKeyIV: '',
secretKeyTag: '',
secretValueCiphertext: '',
secretValueIV: '',
secretValueTag: '',
secretCommentCiphertext: '',
secretCommentIV: '',
secretCommentTag: ''
},
Secret: {
_id: '',
version: 1,
workspace : '',
type: 'shared',
user: null,
secretKeyCiphertext: '',
secretKeyIV: '',
secretKeyTag: '',
secretValueCiphertext: '',
secretValueIV: '',
secretValueTag: '',
secretCommentCiphertext: '',
secretCommentIV: '',
secretCommentTag: '',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
Log: {
_id: '',
user: {
_id: '',
email: 'johndoe@gmail.com',
firstName: 'John',
lastName: 'Doe'
},
workspace: '',
actionNames: [
'addSecrets'
],
actions: [
{
name: 'addSecrets',
user: '',
workspace: '',
payload: [
{
oldSecretVersion: '',
newSecretVersion: ''
}
]
}
],
channel: 'cli',
ipAddress: '192.168.0.1',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
SecretSnapshot: {
workspace: '',
version: 1,
secretVersions: [
{
_id: ''
}
]
},
SecretVersion: {
_id: '',
secret: '',
version: 1,
workspace: '',
type: 'shared',
user: '',
environment: 'dev',
isDeleted: '',
secretKeyCiphertext: '',
secretKeyIV: '',
secretKeyTag: '',
secretValueCiphertext: '',
secretValueIV: '',
secretValueTag: '',
}
}
};
const outputJSONFile = '../spec.json';
const outputYAMLFile = '../docs/spec.yaml';
const endpointsFiles = ['../src/app.ts'];
const spec = await swaggerAutogen(outputJSONFile, endpointsFiles, doc);
await fs.writeFile(outputYAMLFile, yaml.dump(spec.data));
}
generateOpenAPISpec();

@ -7,18 +7,21 @@ import (
"github.com/go-resty/resty/v2"
)
const USER_AGENT = "cli"
func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchModifySecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch-modify/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Patch(endpoint)
if err != nil {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
if response.IsError() {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
@ -26,17 +29,18 @@ func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request B
}
func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchCreateSecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch-create/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
endpoint := fmt.Sprintf("%v/v2/secrets/", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Post(endpoint)
if err != nil {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
if response.IsError() {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
@ -44,17 +48,18 @@ func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request B
}
func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchDeleteSecretsBySecretIdsRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch/workspace/%v/environment/%v", config.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)
response, err := httpClient.
R().
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Delete(endpoint)
if err != nil {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
if response.IsError() {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
@ -67,13 +72,14 @@ func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncrypted
response, err := httpClient.
R().
SetResult(&result).
SetHeader("User-Agent", USER_AGENT).
Get(endpoint)
if err != nil {
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
if response.IsError() {
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
}
@ -85,13 +91,14 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
response, err := httpClient.
R().
SetResult(&tokenDetailsResponse).
SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v2/service-token", config.INFISICAL_URL))
if err != nil {
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
if response.IsError() {
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
}
@ -103,14 +110,16 @@ func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Req
response, err := httpClient.
R().
SetResult(&secretsResponse).
SetQueryParam("environment", request.EnvironmentName).
Get(fmt.Sprintf("%v/v2/secret/workspace/%v", config.INFISICAL_URL, request.WorkspaceId))
SetHeader("User-Agent", USER_AGENT).
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId).
Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL))
if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
if response.IsError() {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
}
@ -122,13 +131,14 @@ func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesR
response, err := httpClient.
R().
SetResult(&workSpacesResponse).
SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v1/workspace", config.INFISICAL_URL))
if err != nil {
return GetWorkSpacesResponse{}, err
}
if response.StatusCode() > 299 {
if response.IsError() {
return GetWorkSpacesResponse{}, fmt.Errorf("CallGetAllWorkSpacesUserBelongsTo: Unsuccessful response: [response=%v]", response)
}

@ -142,19 +142,19 @@ type Secret struct {
SecretCommentTag string `json:"secretCommentTag,omitempty"`
SecretCommentHash string `json:"secretCommentHash,omitempty"`
Type string `json:"type,omitempty"`
ID string `json:"_id,omitempty"`
ID string `json:"id,omitempty"`
}
type BatchCreateSecretsByWorkspaceAndEnvRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchModifySecretsByWorkspaceAndEnvRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchDeleteSecretsBySecretIdsRequest struct {
@ -195,41 +195,49 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
}
type GetEncryptedSecretsV2Request struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
}
type GetEncryptedSecretsV2Response []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretKeyHash string `json:"secretKeyHash"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretValueHash string `json:"secretValueHash"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
SecretCommentHash string `json:"secretCommentHash"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
type GetEncryptedSecretsV2Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
} `json:"secrets"`
}
type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"`
Name string `json:"name"`
Workspace string `json:"workspace"`
Environment string `json:"environment"`
User string `json:"user"`
EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"`
Tag string `json:"tag"`
ID string `json:"_id"`
Name string `json:"name"`
Workspace string `json:"workspace"`
Environment string `json:"environment"`
User struct {
ID string `json:"_id"`
Email string `json:"email"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
} `json:"user"`
ExpiresAt time.Time `json:"expiresAt"`
EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"`
Tag string `json:"tag"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"`
}

@ -51,11 +51,22 @@ var exportCmd = &cobra.Command{
util.HandleError(err)
}
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(envName)
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
if secretOverriding {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
} else {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
}
var output string
if shouldExpandSecrets {
substitutions := util.SubstituteSecrets(secrets)
@ -79,6 +90,7 @@ func init() {
exportCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
}
// Format according to the format flag

@ -6,8 +6,10 @@ package cmd
import (
"os"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/spf13/cobra"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/util"
)
var rootCmd = &cobra.Command{
@ -15,7 +17,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.2",
Version: util.CLI_VERSION,
}
// Execute adds all child commands to the root command and sets flags appropriately.
@ -30,7 +32,17 @@ func Execute() {
func init() {
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging")
rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend")
// rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
// }
rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]")
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.CheckForUpdate()
}
// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
// this is used to allow overrides of the default value
if !rootCmd.Flag("domain").Changed {
if envInfisicalBackendUrl, ok := os.LookupEnv("INFISICAL_API_URL"); ok {
config.INFISICAL_URL = envInfisicalBackendUrl
}
}
}

@ -77,12 +77,14 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
}
if shouldExpandSecrets {
secrets = util.SubstituteSecrets(secrets)
if secretOverriding {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
} else {
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
}
if secretOverriding {
secrets = util.OverrideWithPersonalSecrets(secrets)
if shouldExpandSecrets {
secrets = util.SubstituteSecrets(secrets)
}
secretsByKey := getSecretsByKeys(secrets)
@ -164,7 +166,10 @@ func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []
if runtime.GOOS == "windows" {
shell = [2]string{"cmd", "/C"}
} else {
shell[0] = os.Getenv("SHELL")
currentShell := os.Getenv("SHELL")
if currentShell != "" {
shell[0] = currentShell
}
}
cmd := exec.Command(shell[0], shell[1], fullCommand)

@ -205,9 +205,9 @@ var secretsSetCmd = &cobra.Command{
if len(secretsToCreate) > 0 {
batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName,
Secrets: secretsToCreate,
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
Secrets: secretsToCreate,
}
err = api.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest)
@ -219,9 +219,9 @@ var secretsSetCmd = &cobra.Command{
if len(secretsToModify) > 0 {
batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName,
Secrets: secretsToModify,
WorkspaceId: workspaceFile.WorkspaceId,
Environment: environmentName,
Secrets: secretsToModify,
}
err = api.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest)

@ -1,3 +1,3 @@
package config
var INFISICAL_URL = "http://localhost:8080/api"
var INFISICAL_URL string

@ -0,0 +1,42 @@
package util
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func CheckForUpdate() {
latestVersion, err := getLatestTag("infisical", "infisical")
if err != nil {
// do nothing and continue
return
}
if latestVersion != CLI_VERSION {
PrintWarning(fmt.Sprintf("Please update your CLI. You are running version %s but the latest version is %s", CLI_VERSION, latestVersion))
}
}
func getLatestTag(repoOwner string, repoName string) (string, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/tags", repoOwner, repoName)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var tags []struct {
Name string `json:"name"`
}
json.Unmarshal(body, &tags)
return tags[0].Name, nil
}

@ -3,6 +3,7 @@ package util
const (
CONFIG_FILE_NAME = "infisical-config.json"
CONFIG_FOLDER_NAME = ".infisical"
INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api"
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
SECRET_TYPE_PERSONAL = "personal"
@ -10,4 +11,5 @@ const (
KEYRING_SERVICE_NAME = "infisical"
PERSONAL_SECRET_TYPE_NAME = "personal"
SHARED_SECRET_TYPE_NAME = "shared"
CLI_VERSION = "v0.2.7"
)

@ -24,6 +24,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
httpClient := resty.New()
httpClient.SetAuthToken(serviceToken).
SetHeader("Accept", "application/json")
@ -33,8 +34,8 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
}
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
WorkspaceId: serviceTokenDetails.Workspace,
EnvironmentName: serviceTokenDetails.Environment,
WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment,
})
if err != nil {
@ -80,8 +81,8 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
WorkspaceId: workspaceId,
EnvironmentName: environmentName,
WorkspaceId: workspaceId,
Environment: environmentName,
})
if err != nil {
@ -194,39 +195,52 @@ func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.Sing
return expandedSecrets
}
//
// if two secrets with the same name are found, the one that has type `personal` will be in the returned list
func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
personalSecret := make(map[string]models.SingleEnvironmentVariable)
sharedSecret := make(map[string]models.SingleEnvironmentVariable)
func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable {
personalSecrets := make(map[string]models.SingleEnvironmentVariable)
sharedSecrets := make(map[string]models.SingleEnvironmentVariable)
secretsToReturn := []models.SingleEnvironmentVariable{}
secretsToReturnMap := make(map[string]models.SingleEnvironmentVariable)
for _, secret := range secrets {
if secret.Type == PERSONAL_SECRET_TYPE_NAME {
personalSecret[secret.Key] = secret
personalSecrets[secret.Key] = secret
}
if secret.Type == SHARED_SECRET_TYPE_NAME {
sharedSecret[secret.Key] = secret
sharedSecrets[secret.Key] = secret
}
}
for _, secret := range sharedSecret {
personalValue, personalExists := personalSecret[secret.Key]
if personalExists {
secretsToReturn = append(secretsToReturn, personalValue)
} else {
secretsToReturn = append(secretsToReturn, secret)
if secretType == PERSONAL_SECRET_TYPE_NAME {
for _, secret := range secrets {
if personalSecret, exists := personalSecrets[secret.Key]; exists {
secretsToReturnMap[secret.Key] = personalSecret
} else {
if _, exists = secretsToReturnMap[secret.Key]; !exists {
secretsToReturnMap[secret.Key] = secret
}
}
}
} else if secretType == SHARED_SECRET_TYPE_NAME {
for _, secret := range secrets {
if sharedSecret, exists := sharedSecrets[secret.Key]; exists {
secretsToReturnMap[secret.Key] = sharedSecret
} else {
if _, exists := secretsToReturnMap[secret.Key]; !exists {
secretsToReturnMap[secret.Key] = secret
}
}
}
}
for _, secret := range secretsToReturnMap {
secretsToReturn = append(secretsToReturn, secret)
}
return secretsToReturn
}
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2Response) ([]models.SingleEnvironmentVariable, error) {
plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets {
for _, secret := range encryptedSecrets.Secrets {
// Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil {

@ -46,12 +46,12 @@ services:
context: ./frontend
dockerfile: Dockerfile.dev
volumes:
- ./frontend/pages:/app/pages
- ./frontend/src/pages:/app/src/pages
- ./frontend/src/components:/app/src/components
- ./frontend/src/ee:/app/src/ee
- ./frontend/src/locales:/app/src/locales
- ./frontend/src/styles:/app/src/styles
- ./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
environment:
@ -93,7 +93,7 @@ services:
smtp-server:
container_name: infisical-dev-smtp-server
image: mailhog/mailhog
image: lytrax/mailhog:latest # https://github.com/mailhog/MailHog/issues/353#issuecomment-821137362
restart: always
logging:
driver: 'none' # disable saving logs

@ -0,0 +1,4 @@
---
title: "Delete Membership"
openapi: "DELETE /api/v2/organizations/{organizationId}/memberships/{membershipId}"
---

@ -0,0 +1,4 @@
---
title: "Get Memberships"
openapi: "GET /api/v2/organizations/{organizationId}/memberships"
---

@ -0,0 +1,4 @@
---
title: "Update Membership"
openapi: "PATCH /api/v2/organizations/{organizationId}/memberships/{membershipId}"
---

@ -0,0 +1,4 @@
---
title: "Get Projects"
openapi: "GET /api/v2/organizations/{organizationId}/workspaces"
---

@ -2,3 +2,10 @@
title: "Create"
openapi: "POST /api/v2/secrets/"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for creating
secrets](/api-reference/overview/examples/create-secrets).
</Tip>

@ -1,4 +1,11 @@
---
title: "Read"
title: "Retrieve"
openapi: "GET /api/v2/secrets/"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for retrieving
secrets](/api-reference/overview/examples/retrieve-secrets).
</Tip>

@ -0,0 +1,4 @@
---
title: "Roll Back to Version"
openapi: "POST /api/v1/secret/{secretId}/secret-versions/rollback"
---

@ -2,3 +2,10 @@
title: "Update"
openapi: "PATCH /api/v2/secrets/"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for updating
secrets](/api-reference/overview/examples/update-secrets).
</Tip>

@ -0,0 +1,4 @@
---
title: "Get Versions"
openapi: "GET /api/v1/secret/{secretId}/secret-versions"
---

@ -0,0 +1,4 @@
---
title: "Get My User"
openapi: "GET /api/v2/users/me"
---

@ -0,0 +1,4 @@
---
title: "Get My Organizations"
openapi: "GET /api/v2/users/me/organizations"
---

@ -0,0 +1,4 @@
---
title: "Delete Membership"
openapi: "DELETE /api/v2/workspace/{workspaceId}/memberships/{membershipId}"
---

@ -0,0 +1,4 @@
---
title: "Get Logs"
openapi: "GET /api/v1/workspace/{workspaceId}/logs"
---

@ -0,0 +1,4 @@
---
title: "Get Memberships"
openapi: "GET /api/v2/workspace/{workspaceId}/memberships"
---

@ -0,0 +1,4 @@
---
title: "Roll Back to Snapshot"
openapi: "POST /api/v1/workspace/{workspaceId}/secret-snapshots/rollback"
---

@ -0,0 +1,4 @@
---
title: "Get Snapshots"
openapi: "GET /api/v1/workspace/{workspaceId}/secret-snapshots"
---

@ -0,0 +1,4 @@
---
title: "Update Membership"
openapi: "PATCH /api/v2/workspace/{workspaceId}/memberships/{membershipId}"
---

@ -0,0 +1,4 @@
---
title: "Get Key"
openapi: "GET /api/v2/workspace/{workspaceId}/encrypted-key"
---

@ -1,3 +1,15 @@
---
title: "Authentication"
---
To authenticate requests with Infisical, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key in User Settings > API Keys
![API key dashboard](../../images/api-key-dashboard.png)
![API key in personal settings](../../images/api-key-settings.png)
![Adding an API key](../../images/api-key-add.png)
<Info>
It's important to keep your API key secure, as it grants access to your
secrets in Infisical. For added security, set a reasonable expiration time and
rotate your API key on a regular basis.
</Info>

@ -0,0 +1,152 @@
---
title: "Create secrets"
---
In this example, we demonstrate how to add secrets to a project and environment.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
## Flow
1. [Get your (encrypted) private key](/api-reference/endpoints/users/me).
2. Decrypt your (encrypted) private key with your password.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. Encrypt your secret(s) with the project key.
6. [Send (encrypted) secret(s) to the Infical API](/api-reference/endpoints/secrets/create)
## Example
```js
const crypto = require('crypto');
const axios = require('axios');
const ALGORITHM = 'aes-256-gcm';
const BLOCK_SIZE_BYTES = 16;
const encrypt = (
text,
secret
) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
const decrypt = (ciphertext, iv, tag, secret) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
const createSecrets = async () => {
const API_KEY = 'your_api_key';
const PSWD = 'your_pswd';
const WORKSPACE_ID = 'your_workspace_id';
const SECRET_KEY = 'SOME_KEY';
const SECRET_VALUE = 'SOME_VALUE';
// 1. get (encrypted) private key
const user = await axios.get(
'https://api.infisical.com/api/v2/users/me', {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 2. decrypt your (encrypted) private key with your password
const privateKey = decrypt({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
secret: PSWD.slice(0, 32).padStart(32, '0');
});
// 3. get the (encrypted) project key for the project
const encryptedProjectKey = await axios.get(
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 4. decrypt the project key with your private key
const projectKey = nacl.box.open(
util.decodeBase64(encryptedProjectKey),
util.decodeBase64(projectKey.nonce),
util.decodeBase64(projectKey.sender.publicKey),
util.decodeBase64(privateKey)
);
// 5. encrypt your secret(s) with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encrypt(SECRET_KEY, projectKey);
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encrypt(SECRET_VALUE, projectKey);
const secret = {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
}
// 6. Send (encrypted) secret(s) to the Infisical API
await axios.post(
`https://api.infisical.com/api/v2/secrets`,
{
workspaceId: WORKSPACE_ID,
environment: 'dev',
secrets: secret
},
{
headers: {
'X-API-KEY': API_KEY
}
}
);
}
createSecrets();
```
<Info>
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
are ports of NaCl available in every major language.
</Info>
<Tip>
It can be useful to perform steps 1-4 ahead of time and store away your
private key (and even project key) for later use. The Infisical CLI works by
securely storing your private key via your OS keyring.
</Tip>

@ -0,0 +1,34 @@
---
title: "Delete secrets"
---
In this example, we demonstrate how to delete secrets
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
## Example
```js
const deleteSecrets = async () => {
const API_KEY = "your_api_key";
const SECRET_ID = "ID"; // ID of secret to delete
// 6. Send ID(s) of secret(s) to delete to the Infisical API
await axios.delete(
`https://api.infisical.com/api/v2/secrets`,
{
secretIds: SECRET_ID,
},
{
headers: {
"X-API-KEY": API_KEY,
},
}
);
};
deleteSecrets();
```

@ -0,0 +1,142 @@
---
title: "Retrieve secrets"
---
In this example, we demonstrate how to retrieve secrets from a project and environment.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
## Flow
1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me)
2. Decrypt your (encrypted) private key with your password.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. [Get secrets for a project and environment.](/api-reference/endpoints/secrets/read)
6. Decrypt the (encrypted) secrets
## Example
```js
const crypto = require('crypto');
const axios = require('axios');
const ALGORITHM = 'aes-256-gcm';
const BLOCK_SIZE_BYTES = 16;
const encrypt = (
text,
secret
) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
const decrypt = (ciphertext, iv, tag, secret) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
const retrieveSecrets = async () => {
const API_KEY = 'your_api_key';
const PSWD = 'your_pswd';
const WORKSPACE_ID = 'your_workspace_id';
// 1. get (encrypted) private key
const user = await axios.get(
'https://api.infisical.com/api/v2/users/me', {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 2. decrypt your (encrypted) private key with your password
const privateKey = decrypt({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
secret: PSWD.slice(0, 32).padStart(32, '0');
});
// 3. get the (encrypted) project key for the project
const encryptedProjectKey = await axios.get(
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 4. decrypt the project key with your private key
const projectKey = nacl.box.open(
util.decodeBase64(encryptedProjectKey),
util.decodeBase64(projectKey.nonce),
util.decodeBase64(projectKey.sender.publicKey),
util.decodeBase64(privateKey)
);
// 5. get (encrypted) secrets for a project and environment.
const encryptedSecrets = await axios.get(
'https://api.infisical.com/api/v2/secrets', {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 6. decrypt the (encrypted) secrets
const secrets = encryptedSecrets.map((encryptedSecret) => {
const secretKey = decrypt({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag
secret: projectKey
});
const secretValue = decrypt({
ciphertext: encryptedSecret.secretValueCiphertext,
iv: encryptedSecret.secretValueIV,
tag: encryptedSecret.secretValueTag
secret: projectKey
});
return ({
secretKey,
secretValue
});
});
}
retrieveSecrets();
```
<Info>
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
are ports of NaCl available in every major language.
</Info>
<Tip>
It can be useful to perform steps 1-4 ahead of time and store away your
private key (and even project key) for later use. The Infisical CLI works by
securely storing your private key via your OS keyring.
</Tip>

@ -0,0 +1,152 @@
---
title: "Update secrets"
---
In this example, we demonstrate how to update secrets
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
## Flow
1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me)
2. Decrypt your (encrypted) private key with your password.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. Encrypt your secret(s) with the project key.
6. [Send (encrypted) updated secret(s) to the Infical API.](/api-reference/endpoints/secrets/update)
## Example
```js
const crypto = require('crypto');
const axios = require('axios');
const ALGORITHM = 'aes-256-gcm';
const BLOCK_SIZE_BYTES = 16;
const encrypt = (
text,
secret
) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
const decrypt = (ciphertext, iv, tag, secret) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
}
const updateSecrets = async () => {
const API_KEY = 'your_api_key';
const PSWD = 'your_pswd';
const WORKSPACE_ID = 'your_workspace_id';
const SECRET_ID = 'ID' // ID of secret to update
const SECRET_KEY = 'SOME_KEY';
const SECRET_VALUE = 'SOME_VALUE';
// 1. get (encrypted) private key
const user = await axios.get(
'https://api.infisical.com/api/v2/users/me', {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 2. decrypt your (encrypted) private key with your password
const privateKey = decrypt({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
secret: PSWD.slice(0, 32).padStart(32, '0');
});
// 3. get the (encrypted) project key for the project
const encryptedProjectKey = await axios.get(
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 4. decrypt the project key with your private key
const projectKey = nacl.box.open(
util.decodeBase64(encryptedProjectKey),
util.decodeBase64(projectKey.nonce),
util.decodeBase64(projectKey.sender.publicKey),
util.decodeBase64(privateKey)
);
// 5. encrypt your secret(s) with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encrypt(SECRET_KEY, projectKey);
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encrypt(SECRET_VALUE, projectKey);
const secret = {
id: SECRET_ID,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
}
// 6. Send (encrypted) secret(s) to the Infisical API
await axios.patch(
`https://api.infisical.com/api/v2/secrets`,
{
secrets: secret
},
{
headers: {
'X-API-KEY': API_KEY
}
}
);
}
updateSecrets();
```
<Info>
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
are ports of NaCl available in every major language.
</Info>
<Tip>
It can be useful to perform steps 1-4 ahead of time and store away your
private key (and even project key) for later use. The Infisical CLI works by
securely storing your private key via your OS keyring.
</Tip>

@ -1,3 +1,27 @@
---
title: "Introduction"
---
Infisical's REST API provides users an alternative way to programmatically access and manage
secrets via HTTPS requests. This can be useful for automating tasks, such as
rotating credentials, or for integrating secret management into a larger system.
With the REST API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
## Concepts
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview).
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process.
- Each (encrypted) secret belongs to a project and environment.
- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
<Info>
Infisical's system requires that secrets be encrypted/decrypted on the
client-side to maintain E2EE. We strongly recommend you read up on the system
prior to using the Infisical API. The (opt-in) ability to retrieve secrets
back in decrypted format if you choose to share secrets with Infisical is on
our roadmap.
</Info>

@ -0,0 +1,18 @@
---
title: "Usage"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com) or your self-hosted instance.
- Obtain an API Key in your user settings to be included in requests to the Infisical API.
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview).
## Concepts
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process.
- Each (encrypted) secret belongs to a project and environment.
- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.

@ -16,7 +16,7 @@ The Infisical CLI provides a way to inject environment variables from the platfo
brew install infisical/get-cli/infisical
```
## Updates
### Updates
```bash
brew upgrade infisical
@ -34,7 +34,7 @@ The Infisical CLI provides a way to inject environment variables from the platfo
scoop install infisical
```
## Updates
### Updates
```bash
scoop update infisical
@ -99,8 +99,28 @@ The Infisical CLI provides a way to inject environment variables from the platfo
</Tab>
</Tabs>
## Log in to the Infisical CLI
### Log in to the Infisical CLI
```bash
infisical login
```
<Accordion title="Optional: point CLI to self-hosted">
The CLI is set to connect to Infisical Cloud by default, but if you're running your own instance of Infisical, you can direct the CLI to it using one of the methods provided below.
#### Export environment variable
You can point the CLI to the self hosted Infisical instance by exporting the environment variable `INFISICAL_API_URL` in your terminal.
```bash
# Example
export INFISICAL_API_URL="https://your-self-hosted-infisical.com/api"
```
#### Set manually on every command
Another option to point the CLI to your self hosted Infisical instance is to set it via a flag on every command you run.
```bash
# Example
infisical <any-command> --domain="https://your-self-hosted-infisical.com/api"
```
</Accordion>

@ -1,13 +1,12 @@
---
title: 'Developing'
title: 'Local development'
description: 'This guide will help you set up and run Infisical in development mode.'
---
## Clone the repo
```bash
# change location to the path you want Infisical to be installed
cd ~
# navigate location to the path you want Infisical to be installed
# clone the repo and cd to Infisical dir
git clone https://github.com/Infisical/infisical
@ -16,57 +15,71 @@ cd infisical
## Set up environment variables
Start by creating a .env file at the root of the Infisical directory. It's best to start with the provided [`.env.example`](https://github.com/Infisical/infisical/blob/main/.env.example) template containing the necessary envars to fill out your .env file — you only have to modify the SMTP parameters.
Start by creating a .env file at the root of the Infisical directory then copy the contents of the file below into the .env file.
<Accordion title=".env file content">
```env
# Keys
# Required key for platform encryption/decryption ops
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT
# Required secrets to sign JWT tokens
JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
# MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
# to the MongoDB container instance or Mongo Cloud
# Required
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# Optional credentials for MongoDB container instance and Mongo-Express
MONGO_USERNAME=root
MONGO_PASSWORD=example
# Website URL
# Required
SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST='smtp-server'
SMTP_PORT='1025'
SMTP_NAME='local'
SMTP_USERNAME='team@infisical.com'
SMTP_PASSWORD=
```
</Accordion>
<Warning>
The pre-populated environment variable values in the `.env.example` file are meant to be used in development only.
You'll want to fill in your own values in production, especially concerning encryption keys, secrets, and SMTP parameters.
The pre-populated environment variable values above are meant to be used in development only. They should never be used in production.
</Warning>
Refer to the [environment variable list](https://infisical.com/docs/self-hosting/configuration/envars) for guidance on each envar.
View all available [environment variables](https://infisical.com/docs/self-hosting/configuration/envars) and guidance for each.
### Helpful tips for developing with Infisical:
## Starting Infisical for development
<Tip>
Use the `ENCRYPTION_KEY`, JWT-secret envars, `MONGO_URL`, `MONGO_USERNAME`, `MONGO_PASSWORD` provided in the `.env.example` file.
We use use Docker to easily spin up all required services to have Infisical up and running for development. If you are unfamiliar with Docker, don't worry, all you have to do is install [Docker](https://docs.docker.com/get-docker/) for your
machine and run the commands below to start up the development server.
If setting your own values:
- `ENCRYPTION_KEY` should be a [32-byte random hex](https://www.browserling.com/tools/random-hex)
- `MONGO_URL` should take the form: `mongodb://[MONGO_USERNAME]:[MONGO_PASSWORD]@mongo:27017/?authSource=admin`.
</Tip>
<Tip>
Bring and configure your own SMTP server by following our [email configuration guide](https://infisical.com/docs/self-hosting/configuration/email) (we recommend using either SendGrid or Mailgun).
Alternatively, you can use the provided development (Mailhog) SMTP server to send and browse emails sent by the backend on http://localhost:8025; to use this option, set the following `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM_NAME`, `SMTP_USERNAME`, `SMTP_PASSWORD` below.
</Tip>
```
SMTP_HOST=smtp-server
SMTP_PORT=1025
SMTP_FROM_ADDRESS=team@infisical.com
SMTP_FROM_NAME=Infisical
SMTP_USERNAME=team@infisical.com
SMTP_PASSWORD=
```
<Warning>
If using Mailhog, make sure to leave the `SMTP_PASSWORD` blank so the backend can connect to MailHog.
</Warning>
## Docker for development
#### Start local server
```bash
# build and start the services
docker-compose -f docker-compose.dev.yml up --build --force-recreate
```
#### Access local server
Then browse http://localhost:8080
Once all the services have spun up, browse to http://localhost:8080. To sign in, you may use the default credentials listed below.
Email: `test@localhost.local`
Password: `testInfisical1`
#### Shutdown local server
```bash
# To stop environment use Control+C (on Mac) CTRL+C (on Win) or
docker-compose -f docker-compose.dev.yml down
# start services
docker-compose -f docker-compose.dev.yml up
```

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