1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-20 07:38:48 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
7e4ccc35fc Merge pull request from Infisical/patch-6
add owner reference to InfisicalSecret
2023-12-18 19:20:37 -05:00
95f6f2dcea increase chart version 2023-12-18 19:20:17 -05:00
4ac8c004d7 add owner reference 2023-12-18 19:17:25 -05:00
2168 changed files with 152200 additions and 131724 deletions
.env.example.env.migration.example.env.test.example
.github
.gitignore.goreleaser.yaml.infisicalignoreCONTRIBUTING.mdDockerfile.standalone-infisicalMakefileREADME.md
backend
.eslintignore.eslintrc.eslintrc.js.gitignore.prettierrcDockerfileDockerfile.dev
e2e-test
environment.d.tshealthcheck.js
img
jest.config.tsnodemon.jsonpackage-lock.jsonpackage.json
scripts
spec.json
src
@types
bootstrap.ts
config
controllers
data
db
index.tsinstance.tsknexfile.ts
migrations
20231128072457_user.ts20231128092347_user-encryption-key.ts20231129072939_auth-token.ts20231130072734_auth-token-session.ts20231201151432_backup-key.ts20231204092737_organization.ts20231204092747_org-membership.ts20231205151331_incident-contact.ts20231207055643_user-action.ts20231207055701_super-admin.ts20231207105059_api-key.ts20231212110939_project.ts20231212110946_project-membership.ts20231218092441_secret-folder.ts20231218092508_secret-import.ts20231218092517_secret-tag.ts20231218103423_secret.ts20231220052508_secret-version.ts20231222092113_project-bot.ts20231222172455_integration.ts20231225072545_service-token.ts20231225072552_webhook.ts20231228074856_identity.ts20231228074908_identity-universal-auth.ts20231228075011_identity-access-token.ts20231228075023_identity-membership.ts20240101054849_secret-approval-policy.ts20240101104907_secret-approval-request.ts20240102152111_secret-rotation.ts20240104140641_secret-snapshot.ts20240107153439_saml-config.ts20240107163155_org-bot.ts20240108134148_audit-log.ts20240111051011_secret-scanning.ts20240113103743_trusted-ip.ts20240204171758_org-based-auth.ts20240208234120_scim-token.ts20240216154123_ghost_users.ts20240222201806_admin-signup-control.ts20240226094411_instance-id.ts20240307232900_integration-last-used.ts20240311210135_ldap-config.ts20240312162549_temp-roles.ts20240312162556_temp-role-identity.ts20240318164718_dynamic-secret.ts20240326172010_project-user-additional-privilege.ts20240326172011_machine-identity-additional-privilege.ts20240405000045_org-memberships-unique-constraint.ts20240412174842_group.ts20240414192520_drop-role-roleid-project-membership.ts20240417032913_pending-group-addition.ts20240423023203_ldap-config-groups.ts20240424235842_user-search-filter.ts20240429154610_audit-log-index.ts20240503101144_audit-log-stream.ts20240507032811_trusted-saml-ldap-emails.ts20240507162140_access-approval-policy.ts20240507162141_access.ts20240507210655_identity-aws-auth.ts20240514041650_identity-gcp-auth.ts20240514141809_inline-secret-reference-sync.ts20240518142614_kubernetes-auth.ts20240520064127_add-integration-sync-status.ts
schemas
access-approval-policies-approvers.tsaccess-approval-policies.tsaccess-approval-requests-reviewers.tsaccess-approval-requests.tsapi-keys.tsaudit-log-streams.tsaudit-logs.tsauth-token-sessions.tsauth-tokens.tsbackup-private-key.tsdynamic-secret-leases.tsdynamic-secrets.tsgit-app-install-sessions.tsgit-app-org.tsgroup-project-membership-roles.tsgroup-project-memberships.tsgroups.tsidentities.tsidentity-access-tokens.tsidentity-aws-auths.tsidentity-gcp-auths.tsidentity-kubernetes-auths.tsidentity-org-memberships.tsidentity-project-additional-privilege.tsidentity-project-membership-role.tsidentity-project-memberships.tsidentity-ua-client-secrets.tsidentity-universal-auths.tsincident-contacts.tsindex.tsintegration-auths.tsintegrations.tsldap-configs.tsldap-group-maps.tsmodels.tsorg-bots.tsorg-memberships.tsorg-roles.tsorganizations.tsproject-bots.tsproject-environments.tsproject-keys.tsproject-memberships.tsproject-roles.tsproject-user-additional-privilege.tsproject-user-membership-roles.tsprojects.tssaml-configs.tsscim-tokens.tssecret-approval-policies-approvers.tssecret-approval-policies.tssecret-approval-request-secret-tags.tssecret-approval-requests-reviewers.tssecret-approval-requests-secrets.tssecret-approval-requests.tssecret-blind-indexes.tssecret-folder-versions.tssecret-folders.tssecret-imports.tssecret-references.tssecret-rotation-outputs.tssecret-rotations.tssecret-scanning-git-risks.tssecret-snapshot-folders.tssecret-snapshot-secrets.tssecret-snapshots.tssecret-tag-junction.tssecret-tags.tssecret-version-tag-junction.tssecret-versions.tssecrets.tsservice-tokens.tssuper-admin.tstrusted-ips.tsuser-actions.tsuser-aliases.tsuser-encryption-keys.tsuser-group-membership.tsusers.tswebhooks.ts
seed-data.ts
seeds
utils.ts
ee
LICENSE
controllers
helpers
models
routes
secretRotation
services
EEAuditLogService.tsEELicenseService.tsEESecretService.ts
GithubSecretScanning
ProjectRoleService.tsRoleService.tsSecretApprovalService.ts
access-approval-policy
access-approval-request
audit-log-stream
audit-log
dynamic-secret-lease
dynamic-secret
group
identity-project-additional-privilege
index.ts
ldap-config
license
permission
project-user-additional-privilege
saml-config
scim
secret-approval-policy
secret-approval-request
secret-rotation
secret-scanning
secret-snapshot
trusted-ip
validation
events
helpers
index.ts
integrations
interfaces
middleware
services
BotService
SecretService
utils
keystore
lib
main.ts
middleware
models
queue
queues
routes
server
services
BotOrgService.tsBotService.tsDatabaseService.tsEventService.tsFolderService.tsIntegrationService.tsRedisService.tsSecretImportService.tsSecretService.tsTelemetryService.tsTokenService.tsWebhookService.ts
api-key
auth-token
auth
group-project
health.ts
identity-access-token
identity-aws-auth
identity-gcp-auth
identity-kubernetes-auth
identity-project
identity-ua
identity
index.ts
integration-auth
integration
org-membership
org
project-bot
project-env
project-key
project-membership
project-role
project
secret-blind-index
secret-folder
secret-import
secret-tag
secret
service-token
smtp.ts
smtp
super-admin
telemetry
user-alias
user
webhook
templates
types
utils
validation
variables
swagger
test-resources
tests
tsconfig.jsontsup.config.jsvitest.e2e.config.ts
cli
company
docker-compose.dev.ymldocker-compose.prod.ymldocker-compose.yml
docker-swarm
docs
api-reference
changelog
cli
contributing
documentation
images
agent
auth-methods
docker-swarm-secrets-complete.png
guides
integrations
organization-members.png
platform
access-controls
audit-log-streams
dynamic-secrets
groups
identities
ldap
organization
project
scim
secret-rotation/aws-iam
secret-versioning.png
project-token-old-add.pngsdk-flow.png
secret-rotation
self-hosting
applicable-to-all
configuration/email
deployment-options
aws-lightsail
azure-app-services
azure-container-instances
docker-swarm
flyio
gcp-cloud-run
railway
guides/mongo-postgres
reference-architectures
sso
infisical-agent
integrations
internals
mint.json
sdks
self-hosting
spec.yamlstyle.css
frontend
.eslintrc.js
.storybook
Dockerfilepackage-lock.jsonpackage.json
public
scripts
src
components
AddTagPopoverContent
analytics
basic
context/Notifications
dashboard
features
integrations
navigation
notifications
permissions
signup
tags/CreateTagModal
utilities
v2
config
const.ts
context
ee
helpers
hoc
withPermission
withProjectPermission
hooks
api
accessApproval
admin
apiKeys
auditLogStreams
auditLogs
auth
bots
dynamicSecret
dynamicSecretLease
groups
identities
identityProjectAdditionalPrivilege
incidentContacts
index.tsx
integrationAuth
integrations
keys
ldapConfig
organization
projectUserAdditionalPrivilege
roles
scim
secretApproval
secretApprovalRequest
secretFolders
secretImports
secretRotation
secretSnapshots
secrets
serverDetails
serviceTokens
ssoConfig
subscriptions
tags
trustedIps
types.ts
users
webhooks
workspace
index.tsuseTimedReset.tsx
i18n.ts
layouts
AdminLayout
AppLayout
AppLayout.tsx
components/NavBar
lib/fn
pages
404.tsx_app.tsx
admin
api
cli-redirect.tsxdashboard.tsxemail-not-verified.tsxindex.tsx
integrations
login
org
[id]
members
overview
secret-scanning
none
password-reset.tsxpersonal-settings.tsx
project/[id]
allowlist
audit-logs
secrets/v2
requestnewinvite.tsxsecret-scanning.tsx
signup
signupinvite.tsxverify-email.tsx
reactQuery.ts
services
styles
views
IntegrationsPage
Login
Org
Project
SecretApprovalPage
SecretMainPage
SecretOverviewPage
SecretRotationPage
SecretScanning/components
Settings
BillingSettingsPage
OrgSettingsPage
PersonalSettingsPage
ProjectSettingsPage
Signup
admin
DashboardPage
SignUpPage
SignUpPage.tsx
components/DownloadBackupKeys
tsconfig.jsontsconfig.tsbuildinfo
helm-charts
k8-operator
package-lock.jsonpackage.json
sink
standalone-entrypoint.sh

@ -8,17 +8,19 @@ ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
# Postgres creds
POSTGRES_PASSWORD=infisical
POSTGRES_USER=infisical
POSTGRES_DB=infisical
# 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
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# Redis
REDIS_URL=redis://redis:6379
# Optional credentials for MongoDB container instance and Mongo-Express
MONGO_USERNAME=root
MONGO_PASSWORD=example
# Website URL
# Required
SITE_URL=http://localhost:8080

@ -1 +0,0 @@
DB_CONNECTION_URI=

@ -1,4 +0,0 @@
REDIS_URL=redis://localhost:6379
DB_CONNECTION_URI=postgres://infisical:infisical@localhost/infisical?sslmode=disable
AUTH_SECRET=4bnfe4e407b8921c104518903515b218
ENCRYPTION_KEY=4bnfe4e407b8921c104518903515b218

@ -1,6 +1,6 @@
# Description 📣
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Here's how we expect a pull request to be : https://infisical.com/docs/contributing/getting-started/pull-requests -->
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
## Type ✨
@ -19,6 +19,4 @@
---
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/getting-started/code-of-conduct). 📝
<!-- If you have any questions regarding contribution, here's the FAQ : https://infisical.com/docs/contributing/getting-started/faq -->
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/code-of-conduct). 📝

@ -1,190 +0,0 @@
# inspired by https://www.photoroom.com/inside-photoroom/how-we-automated-our-changelog-thanks-to-chatgpt
import os
import requests
import re
from openai import OpenAI
import subprocess
from datetime import datetime
import uuid
# Constants
REPO_OWNER = "infisical"
REPO_NAME = "infisical"
TOKEN = os.environ["GITHUB_TOKEN"]
SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
SLACK_MSG_COLOR = "#36a64f"
headers = {
"Authorization": f"Bearer {TOKEN}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
def set_multiline_output(name, value):
with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
delimiter = uuid.uuid1()
print(f'{name}<<{delimiter}', file=fh)
print(value, file=fh)
print(delimiter, file=fh)
def post_changelog_to_slack(changelog, tag):
slack_payload = {
"text": "Hey team, it's changelog time! :wave:",
"attachments": [
{
"color": SLACK_MSG_COLOR,
"title": f"🗓Infisical Changelog - {tag}",
"text": changelog,
}
],
}
response = requests.post(SLACK_WEBHOOK_URL, json=slack_payload)
if response.status_code != 200:
raise Exception("Failed to post changelog to Slack.")
def find_previous_release_tag(release_tag:str):
previous_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", f"{release_tag}^"]).decode("utf-8").strip()
while not(previous_tag.startswith("infisical/")):
previous_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", f"{previous_tag}^"]).decode("utf-8").strip()
return previous_tag
def get_tag_creation_date(tag_name):
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/git/refs/tags/{tag_name}"
response = requests.get(url, headers=headers)
response.raise_for_status()
commit_sha = response.json()['object']['sha']
commit_url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/commits/{commit_sha}"
commit_response = requests.get(commit_url, headers=headers)
commit_response.raise_for_status()
creation_date = commit_response.json()['commit']['author']['date']
return datetime.strptime(creation_date, '%Y-%m-%dT%H:%M:%SZ')
def fetch_prs_between_tags(previous_tag_date:datetime, release_tag_date:datetime):
# Use GitHub API to fetch PRs merged between the commits
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/pulls?state=closed&merged=true"
response = requests.get(url, headers=headers)
if response.status_code != 200:
raise Exception("Error fetching PRs from GitHub API!")
prs = []
for pr in response.json():
# the idea is as tags happen recently we get last 100 closed PRs and then filter by tag creation date
if pr["merged_at"] and datetime.strptime(pr["merged_at"],'%Y-%m-%dT%H:%M:%SZ') < release_tag_date and datetime.strptime(pr["merged_at"],'%Y-%m-%dT%H:%M:%SZ') > previous_tag_date:
prs.append(pr)
return prs
def extract_commit_details_from_prs(prs):
commit_details = []
for pr in prs:
commit_message = pr["title"]
commit_url = pr["html_url"]
pr_number = pr["number"]
branch_name = pr["head"]["ref"]
issue_numbers = re.findall(r"(www-\d+|web-\d+)", branch_name)
# If no issue numbers are found, add the PR details without issue numbers and URLs
if not issue_numbers:
commit_details.append(
{
"message": commit_message,
"pr_number": pr_number,
"pr_url": commit_url,
"issue_number": None,
"issue_url": None,
}
)
continue
for issue in issue_numbers:
commit_details.append(
{
"message": commit_message,
"pr_number": pr_number,
"pr_url": commit_url,
"issue_number": issue,
}
)
return commit_details
# Function to generate changelog using OpenAI
def generate_changelog_with_openai(commit_details):
commit_messages = []
for details in commit_details:
base_message = f"{details['pr_url']} - {details['message']}"
# Add the issue URL if available
# if details["issue_url"]:
# base_message += f" (Linear Issue: {details['issue_url']})"
commit_messages.append(base_message)
commit_list = "\n".join(commit_messages)
prompt = """
Generate a changelog for Infisical, opensource secretops
The changelog should:
1. Be Informative: Using the provided list of GitHub commits, break them down into categories such as Features, Fixes & Improvements, and Technical Updates. Summarize each commit concisely, ensuring the key points are highlighted.
2. Have a Professional yet Friendly tone: The tone should be balanced, not too corporate or too informal.
3. Celebratory Introduction and Conclusion: Start the changelog with a celebratory note acknowledging the team's hard work and progress. End with a shoutout to the team and wishes for a pleasant weekend.
4. Formatting: you cannot use Markdown formatting, and you can only use emojis for the introductory paragraph or the conclusion paragraph, nowhere else.
5. Links: the syntax to create links is the following: `<http://www.example.com|This message is a link>`.
6. Linear Links: note that the Linear link is optional, include it only if provided.
7. Do not wrap your answer in a codeblock. Just output the text, nothing else
Here's a good example to follow, please try to match the formatting as closely as possible, only changing the content of the changelog and have some liberty with the introduction. Notice the importance of the formatting of a changelog item:
- <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>))
And here's an example of the full changelog:
*Features*
• <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
*Fixes & Improvements*
• <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
*Technical Updates*
• <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
Stay tuned for more exciting updates coming soon!
And here are the commits:
{}
""".format(
commit_list
)
client = OpenAI(api_key=OPENAI_API_KEY)
messages = [{"role": "user", "content": prompt}]
response = client.chat.completions.create(model="gpt-3.5-turbo", messages=messages)
if "error" in response.choices[0].message:
raise Exception("Error generating changelog with OpenAI!")
return response.choices[0].message.content.strip()
if __name__ == "__main__":
try:
# Get the latest and previous release tags
latest_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"]).decode("utf-8").strip()
previous_tag = find_previous_release_tag(latest_tag)
latest_tag_date = get_tag_creation_date(latest_tag)
previous_tag_date = get_tag_creation_date(previous_tag)
prs = fetch_prs_between_tags(previous_tag_date,latest_tag_date)
pr_details = extract_commit_details_from_prs(prs)
# Generate changelog
changelog = generate_changelog_with_openai(pr_details)
post_changelog_to_slack(changelog,latest_tag)
# Print or post changelog to Slack
# set_multiline_output("changelog", changelog)
except Exception as e:
print(str(e))

@ -1,26 +0,0 @@
import os
from datetime import datetime, timedelta
def rename_migrations():
migration_folder = "./backend/src/db/migrations"
with open("added_files.txt", "r") as file:
changed_files = file.readlines()
# Find the latest file among the changed files
latest_timestamp = datetime.now() # utc time
for file_path in changed_files:
file_path = file_path.strip()
# each new file bump by 1s
latest_timestamp = latest_timestamp + timedelta(seconds=1)
new_filename = os.path.join(migration_folder, latest_timestamp.strftime("%Y%m%d%H%M%S") + f"_{file_path.split('_')[1]}")
old_filename = os.path.join(migration_folder, file_path)
os.rename(old_filename, new_filename)
print(f"Renamed {old_filename} to {new_filename}")
if len(changed_files) == 0:
print("No new files added to migration folder")
if __name__ == "__main__":
rename_migrations()

82
.github/values.yaml vendored

@ -1,57 +1,49 @@
## @section Common parameters
##
## @param nameOverride Override release name
##
nameOverride: ""
## @param fullnameOverride Override release fullname
##
fullnameOverride: ""
## @section Infisical backend parameters
## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes
##
infisical:
autoDatabaseSchemaMigration: false
enabled: false
name: infisical
replicaCount: 3
backend:
enabled: true
name: backend
podAnnotations: {}
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/staging_infisical
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-backend-secret
service:
annotations: {}
type: ClusterIP
nodePort: ""
resources:
limits:
memory: 300Mi
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
backendEnvironmentVariables: null
kubeSecretRef: "managed-secret"
## Mongo DB persistence
mongodb:
enabled: false
persistence:
enabled: false
## By default the backend will be connected to a Mongo instance within the cluster
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
mongodbConnection:
externalMongoDBConnectionString: ""
ingress:
## @param ingress.enabled Enable ingress
##
enabled: true
## @param ingress.ingressClassName Ingress class name
##
ingressClassName: nginx
## @param ingress.nginx.enabled Ingress controller
##
# nginx:
# enabled: true
## @param ingress.annotations Ingress annotations
##
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hostName: "gamma.infisical.com"
# annotations:
# kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
hostName: gamma.infisical.com ## <- Replace with your own domain
tls:
- secretName: letsencrypt-prod
hosts:
- gamma.infisical.com
[]
# - secretName: letsencrypt-nginx
# hosts:
# - infisical.local
postgresql:
enabled: false
redis:
mailhog:
enabled: false

@ -3,7 +3,6 @@ on:
push:
tags:
- "infisical/v*.*.*"
- "!infisical/v*.*.*-postgres"
jobs:
backend-image:
@ -41,7 +40,6 @@ jobs:
load: true
context: backend
tags: infisical/infisical:test
platforms: linux/amd64,linux/arm64
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@ -93,7 +91,6 @@ jobs:
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

@ -1,38 +0,0 @@
name: Build patroni
on: [workflow_dispatch]
jobs:
patroni-image:
name: Build patroni
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
repository: 'zalando/patroni'
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 🏗️ Build backend and push to docker hub
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile
tags: |
infisical/patroni:${{ steps.commit.outputs.short }}
infisical/patroni:latest
platforms: linux/amd64,linux/arm64

@ -1,140 +0,0 @@
name: Deployment pipeline
on: [workflow_dispatch]
permissions:
id-token: write
contents: read
jobs:
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 🏗️ Build backend and push to docker hub
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile.standalone-infisical
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }}
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [infisical-image]
environment:
name: Gamma
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
aws-region: us-east-1
role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-core-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true
production-postgres-deployment:
name: Deploy to production
runs-on: ubuntu-latest
needs: [gamma-deployment]
environment:
name: Production
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
aws-region: us-east-1
role-to-assume: arn:aws:iam::381492033652:role/gha-make-prod-deployment
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-core-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
cluster: infisical-core-platform
wait-for-service-stability: true

99
.github/workflows/build-staging-img.yml vendored Normal file

@ -0,0 +1,99 @@
name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch]
jobs:
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: .
file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
- name: 🧪 Test backend image
run: |
./.github/resources/healthcheck.sh infisical-backend-test
- name: ⏻ Shut down backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile.standalone-infisical
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [infisical-image]
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: v3.10.0
- name: Install infisical helm chart
run: |
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
- name: Install kubectl
uses: azure/setup-kubectl@v3
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
- name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma
- name: test kubectl
run: kubectl get ingress
- name: Download helm values to file and upgrade gamma deploy
run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait --install
if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed"
exit 1
else
echo "Helm upgrade was successful"
fi

@ -1,76 +0,0 @@
name: "Check API For Breaking Changes"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/src/server/routes/**"
- "backend/src/ee/routes/**"
jobs:
check-be-api-changes:
name: Check API Changes
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout source
uses: actions/checkout@v3
# - name: Setup Node 20
# uses: actions/setup-node@v3
# with:
# node-version: "20"
# uncomment this when testing locally using nektos/act
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
with:
version: "2.14.2"
- name: 📦Build the latest image
run: docker build --tag infisical-api .
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
- name: Start the server
run: |
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
env:
REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
JWT_AUTH_SECRET: something-random
- uses: actions/setup-go@v5
with:
go-version: '1.21.5'
- name: Wait for container to be stable and check logs
run: |
SECONDS=0
HEALTHY=0
while [ $SECONDS -lt 60 ]; do
if docker ps | grep infisical-api | grep -q healthy; then
echo "Container is healthy."
HEALTHY=1
break
fi
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
docker logs infisical-api
sleep 2
SECONDS=$((SECONDS+2))
done
if [ $HEALTHY -ne 1 ]; then
echo "Container did not become healthy in time"
exit 1
fi
- name: Install openapi-diff
run: go install github.com/tufin/oasdiff@latest
- name: Running OpenAPI Spec diff action
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker stop infisical-api
docker remove infisical-api

@ -0,0 +1,43 @@
name: "Check Backend Pull Request"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
jobs:
check-be-pr:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Setup Node 16
uses: actions/setup-node@v3
with:
node-version: "16"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: 📦 Install dependencies
run: npm ci --only-production
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
# - name: 📁 Upload test results
# uses: actions/upload-artifact@v3
# if: always()
# with:
# name: be-test-results
# path: |
# ./backend/reports
# ./backend/coverage
- name: 🏗️ Run build
run: npm run build
working-directory: backend

@ -1,35 +0,0 @@
name: "Check Backend PR types and lint"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
jobs:
check-be-pr:
name: Check TS and Lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
run: npm install
working-directory: backend
- name: Run type check
run: npm run type:check
working-directory: backend
- name: Run lint check
run: npm run lint
working-directory: backend

@ -1,4 +1,4 @@
name: Check Frontend Type and Lint check
name: Check Frontend Pull Request
on:
pull_request:
@ -10,8 +10,8 @@ on:
- "frontend/.eslintrc.js"
jobs:
check-fe-ts-lint:
name: Check Frontend Type and Lint check
check-fe-pr:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
@ -25,11 +25,12 @@ jobs:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: 📦 Install dependencies
run: npm install
run: npm ci --only-production --ignore-scripts
working-directory: frontend
- name: 🏗️ Run Type check
run: npm run type:check
working-directory: frontend
- name: 🏗️ Run Link check
run: npm run lint:fix
# -
# name: 🧪 Run tests
# run: npm run test:ci
# working-directory: frontend
- name: 🏗️ Run build
run: npm run build
working-directory: frontend

@ -1,34 +0,0 @@
name: Generate Changelog
permissions:
contents: write
on:
workflow_dispatch:
push:
tags:
- "infisical/v*.*.*-postgres"
jobs:
generate_changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12.0"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests openai
- name: Generate Changelog and Post to Slack
id: gen-changelog
run: python .github/resources/changelog-generator.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

@ -2,17 +2,12 @@ name: Release standalone docker image
on:
push:
tags:
- "infisical/v*.*.*-postgres"
- "infisical/v*.*.*"
jobs:
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-standalone:
name: Build infisical standalone image postgres
name: Build infisical standalone image
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: Extract version from tag
id: extract_version
@ -24,6 +19,27 @@ jobs:
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- uses: paulhatch/semantic-version@v5.0.2
id: version
with:
# The prefix to use to identify tags
tag_prefix: "infisical-standalone/v"
# A string which, if present in a git commit, indicates that a change represents a
# major (breaking) change, supports regular expressions wrapped with '/'
major_pattern: "(MAJOR)"
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
minor_pattern: "(MINOR)"
# A string to determine the format of the version output
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
# Optional path to check for changes. If any changes are detected in the path the
# 'changed' output will true. Enter multiple paths separated by spaces.
change_path: "backend,frontend"
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
enable_prerelease_mode: true
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
@ -52,7 +68,7 @@ jobs:
push: true
context: .
tags: |
infisical/infisical:latest-postgres
infisical/infisical:latest
infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64

58
.github/workflows/release_build.yml vendored Normal file

@ -0,0 +1,58 @@
name: Build and release CLI
on:
push:
# run only against tags
tags:
- "infisical-cli/v*.*.*"
permissions:
contents: write
# packages: write
# issues: write
jobs:
goreleaser:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: git fetch --force --tags
- run: echo "Ref name ${{github.ref_name}}"
- uses: actions/setup-go@v3
with:
go-version: ">=1.19.3"
cache: true
cache-dependency-path: cli/go.sum
- name: libssl1.1 => libssl1.0-dev for OSXCross
run: |
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
sudo apt update && apt-cache policy libssl1.0-dev
sudo apt-get install libssl1.0-dev
- name: OSXCross for CGO Support
run: |
mkdir ../../osxcross
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser-pro
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith
run: sh cli/upload_to_cloudsmith.sh
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}

@ -1,72 +0,0 @@
name: Build and release CLI
on:
workflow_dispatch:
push:
# run only against tags
tags:
- "infisical-cli/v*.*.*"
permissions:
contents: write
# packages: write
# issues: write
jobs:
cli-integration-tests:
name: Run tests before deployment
uses: ./.github/workflows/run-cli-tests.yml
secrets:
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
goreleaser:
runs-on: ubuntu-20.04
needs: [cli-integration-tests]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- run: git fetch --force --tags
- run: echo "Ref name ${{github.ref_name}}"
- uses: actions/setup-go@v3
with:
go-version: ">=1.19.3"
cache: true
cache-dependency-path: cli/go.sum
- name: libssl1.1 => libssl1.0-dev for OSXCross
run: |
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
sudo apt update && apt-cache policy libssl1.0-dev
sudo apt-get install libssl1.0-dev
- name: OSXCross for CGO Support
run: |
mkdir ../../osxcross
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser-pro
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith
run: sh cli/upload_to_cloudsmith.sh
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}

@ -1,47 +0,0 @@
name: "Run backend tests"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
workflow_call:
jobs:
check-be-pr:
name: Run integration test
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
with:
version: "2.14.2"
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
run: npm install
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test
run: npm run test:e2e
working-directory: backend
env:
REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
AUTH_SECRET: something-random
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down

@ -1,47 +0,0 @@
name: Go CLI Tests
on:
pull_request:
types: [opened, synchronize]
paths:
- "cli/**"
workflow_dispatch:
workflow_call:
secrets:
CLI_TESTS_UA_CLIENT_ID:
required: true
CLI_TESTS_UA_CLIENT_SECRET:
required: true
CLI_TESTS_SERVICE_TOKEN:
required: true
CLI_TESTS_PROJECT_ID:
required: true
CLI_TESTS_ENV_SLUG:
required: true
jobs:
test:
defaults:
run:
working-directory: ./cli
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.21.x"
- name: Install dependencies
run: go get .
- name: Test with the Go CLI
env:
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
run: go test -v -count=1 ./test

@ -1,59 +0,0 @@
name: Rename Migrations
on:
pull_request:
types: [closed]
paths:
- 'backend/src/db/migrations/**'
jobs:
rename:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get list of newly added files in migration folder
run: |
git diff --name-status HEAD^ HEAD backend/src/db/migrations | grep '^A' | cut -f2 | xargs -n1 basename > added_files.txt
if [ ! -s added_files.txt ]; then
echo "No new files added. Skipping"
echo "SKIP_RENAME=true" >> $GITHUB_ENV
fi
- name: Script to rename migrations
if: env.SKIP_RENAME != 'true'
run: python .github/resources/rename_migration_files.py
- name: Commit and push changes
if: env.SKIP_RENAME != 'true'
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git add ./backend/src/db/migrations
rm added_files.txt
git commit -m "chore: renamed new migration files to latest timestamp (gh-action)"
- name: Get PR details
id: pr_details
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
PR_MERGER=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | jq -r '.merged_by.login')
echo "PR Number: $PR_NUMBER"
echo "PR Merger: $PR_MERGER"
echo "pr_merger=$PR_MERGER" >> $GITHUB_OUTPUT
- name: Create Pull Request
if: env.SKIP_RENAME != 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: renamed new migration files to latest UTC (gh-action)'
title: 'GH Action: rename new migration file timestamp'
branch-suffix: timestamp
reviewers: ${{ steps.pr_details.outputs.pr_merger }}

11
.gitignore vendored

@ -1,12 +1,11 @@
# backend
node_modules
.env
.env.test
.env.dev
.env.gamma
.env.prod
.env.infisical
.env.migration
*~
*.swp
*.swo
@ -59,13 +58,7 @@ yarn-error.log*
# Infisical init
.infisical.json
.infisicalignore
# Editor specific
.vscode/*
frontend-build
*.tgz
cli/infisical-merge
cli/test/infisical-merge
frontend-build

@ -108,7 +108,7 @@ brews:
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
- name: "infisical@{{.Version}}"
- name: 'infisical@{{.Version}}'
tap:
owner: Infisical
name: homebrew-get-cli
@ -186,38 +186,12 @@ aurs:
# man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
dockers:
- dockerfile: docker/alpine
goos: linux
goarch: amd64
use: buildx
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- "infisical/cli:latest-amd64"
build_flag_templates:
- "--pull"
- "--platform=linux/amd64"
- dockerfile: docker/alpine
goos: linux
goarch: amd64
use: buildx
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- "infisical/cli:latest-arm64"
build_flag_templates:
- "--pull"
- "--platform=linux/arm64"
docker_manifests:
- name_template: "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
image_templates:
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- name_template: "infisical/cli:latest"
image_templates:
- "infisical/cli:latest-amd64"
- "infisical/cli:latest-arm64"
# dockers:
# - dockerfile: cli/docker/Dockerfile
# goos: linux
# goarch: amd64
# ids:
# - infisical
# image_templates:
# - "infisical/cli:{{ .Version }}"
# - "infisical/cli:latest"

@ -1,7 +1 @@
.github/resources/docker-compose.be-test.yml:generic-api-key:16
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451

@ -2,6 +2,6 @@
Thanks for taking the time to contribute! 😃 🚀
Please refer to our [Contributing Guide](https://infisical.com/docs/contributing/getting-started/overview) for instructions on how to contribute.
Please refer to our [Contributing Guide](https://infisical.com/docs/contributing/overview) for instructions on how to contribute.
We also have some 🔥amazing🔥 merch for our contributors. Please reach out to tony@infisical.com for more info 👀

@ -1,9 +1,8 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG SAML_ORG_SLUG=saml-org-slug-default
FROM node:20-alpine AS base
FROM node:16-alpine AS base
FROM base AS frontend-dependencies
@ -36,8 +35,6 @@ ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG SAML_ORG_SLUG
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
# Build
RUN npm run build
@ -76,7 +73,6 @@ RUN npm ci --only-production
COPY /backend .
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
RUN npm i -D tsconfig-paths
RUN npm run build
# Production stage
@ -103,9 +99,6 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ARG SAML_ORG_SLUG
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
WORKDIR /
@ -113,20 +106,23 @@ COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build
ENV PORT 8080
ENV HOST=0.0.0.0
ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
WORKDIR /backend
ENV TELEMETRY_ENABLED true
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 8080
EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]

@ -5,13 +5,13 @@ push:
docker-compose -f docker-compose.yml push
up-dev:
docker compose -f docker-compose.dev.yml up --build
docker-compose -f docker-compose.dev.yml up --build
up-dev-ldap:
docker compose -f docker-compose.dev.yml --profile ldap up --build
i-dev:
infisical run -- docker-compose -f docker-compose.dev.yml up --build
up-prod:
docker-compose -f docker-compose.prod.yml up --build
docker-compose -f docker-compose.yml up --build
down:
docker compose -f docker-compose.dev.yml down
docker-compose down

@ -10,8 +10,7 @@
<a href="https://infisical.com/">Infisical Cloud</a> |
<a href="https://infisical.com/docs/self-hosting/overview">Self-Hosting</a> |
<a href="https://infisical.com/docs/documentation/getting-started/introduction">Docs</a> |
<a href="https://www.infisical.com">Website</a> |
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
<a href="https://www.infisical.com">Website</a>
</h4>
<p align="center">
@ -34,7 +33,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-6.95M-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@ -54,19 +53,17 @@ We're on a mission to make secret management more accessible to everyone, not ju
## Features
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
- **[Infisical Kubernetes operator](https://infisical.com/docs/documentation/getting-started/kubernetes)** to managed secrets in k8s, automatically reload deployments, and more.
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
- **[Self-hosting and on-prem](https://infisical.com/docs/self-hosting/overview)** to get complete control over your data.
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
- **[Simple on-premise deployments](https://infisical.com/docs/self-hosting/overview)** to AWS, Digital Ocean, and more.
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.)
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like GitHub, Vercel, Netlify, and more
- [**Automatic Kubernetes deployment secret reloads**](https://infisical.com/docs/documentation/getting-started/kubernetes)
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery]()** to version every secret and project state
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project
- **Role-based Access Controls** per environment
- [**Simple on-premise deployments** to AWS, Digital Ocean, and more](https://infisical.com/docs/self-hosting/overview)
- [**Secret Scanning and Leak Prevention**](https://infisical.com/docs/cli/scanning-overview)
And much more.
@ -76,7 +73,7 @@ Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/int
| Use Infisical Cloud | Deploy Infisical on premise |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <a href="https://infisical.com/docs/self-hosting/deployment-options/aws-ec2"><img src=".github/images/deploy-to-aws.png" width="150" width="300" /></a> <a href="https://infisical.com/docs/self-hosting/deployment-options/digital-ocean-marketplace" alt="Deploy to DigitalOcean"> <img width="217" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/> </a> <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
### Run Infisical locally
@ -85,13 +82,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
Linux/macOS:
```console
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.prod.yml up
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.yml up
```
Windows Command Prompt:
```console
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.prod.yml up
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.yml up
```
Create an account at `http://localhost:80`
@ -118,9 +115,9 @@ Lean about Infisical's code scanning feature [here](https://infisical.com/docs/c
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo):
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://cal.com/vmatsiiako/infisical-demo):
<a href="[https://infisical.cal.com/vlad/infisical-demo](https://infisical.cal.com/vlad/infisical-demo)"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
<a href="https://cal.com/vmatsiiako/infisical-demo"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
## Security
@ -132,7 +129,7 @@ Note that this security address should be used only for undisclosed vulnerabilit
## Contributing
Whether it's big or small, we love contributions. Check out our guide to see how to [get started](https://infisical.com/docs/contributing/getting-started).
Whether it's big or small, we love contributions. Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview).
Not sure where to get started? You can:

@ -1,3 +1,2 @@
vitest-environment-infisical.ts
vitest.config.ts
vitest.e2e.config.ts
node_modules
built

41
backend/.eslintrc Normal file

@ -0,0 +1,41 @@
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"unused-imports"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off",
"no-console": 2,
"quotes": [
"error",
"double",
{
"avoidEscape": true
}
],
"comma-dangle": [
"error",
"only-multiline"
],
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"sort-imports": 1
}
}

@ -1,74 +0,0 @@
/* eslint-env node */
module.exports = {
env: {
es6: true,
node: true
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"airbnb-base",
"airbnb-typescript/base",
"plugin:prettier/recommended",
"prettier"
],
plugins: ["@typescript-eslint", "simple-import-sort", "import"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
sourceType: "module",
tsconfigRootDir: __dirname
},
root: true,
overrides: [
{
files: ["./e2e-test/**/*", "./src/db/migrations/**/*"],
rules: {
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off"
}
}
],
rules: {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off",
"no-void": "off",
"consistent-return": "off", // my style
"import/order": "off", // for simple-import-order
"import/prefer-default-export": "off", // why
"no-restricted-syntax": "off",
// importing rules
"simple-import-sort/exports": "error",
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"simple-import-sort/imports": [
"warn",
{
groups: [
// Side effect imports.
["^\\u0000"],
// Node.js builtins prefixed with `node:`.
["^node:"],
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
["^@?\\w"],
["^@app"],
["@lib"],
["@server"],
// Absolute imports and other imports such as Vue-style `@/foo`.
// Anything not matched in another group.
["^"],
// Relative imports.
// Anything that starts with a dot.
["^\\."]
]
}
]
}
};

1
backend/.gitignore vendored

@ -1 +0,0 @@
dist

@ -1,6 +1,6 @@
{
"singleQuote": false,
"printWidth": 120,
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 2,
"semi": true

@ -1,5 +1,5 @@
# Build stage
FROM node:20-alpine AS build
FROM node:16-alpine AS build
WORKDIR /app
@ -10,7 +10,7 @@ COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
FROM node:16-alpine
WORKDIR /app
@ -28,8 +28,6 @@ RUN apk add --no-cache bash curl && curl -1sLf \
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
ENV HOST=0.0.0.0
EXPOSE 4000
CMD ["npm", "start"]
CMD ["node", "build/index.js"]

@ -1,18 +0,0 @@
FROM node:20-alpine
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1 && apk add --no-cache git
WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm install
COPY . .
ENV HOST=0.0.0.0
CMD ["npm", "run", "dev:docker"]

@ -1,30 +0,0 @@
import { TKeyStoreFactory } from "@app/keystore/keystore";
export const mockKeyStore = (): TKeyStoreFactory => {
const store: Record<string, string | number | Buffer> = {};
return {
setItem: async (key, value) => {
store[key] = value;
return "OK";
},
setItemWithExpiry: async (key, value) => {
store[key] = value;
return "OK";
},
deleteItem: async (key) => {
delete store[key];
return 1;
},
getItem: async (key) => {
const value = store[key];
if (typeof value === "string") {
return value;
}
return null;
},
incrementBy: async () => {
return 1;
}
};
};

@ -1,26 +0,0 @@
import { TQueueServiceFactory } from "@app/queue";
export const mockQueue = (): TQueueServiceFactory => {
const queues: Record<string, unknown> = {};
const workers: Record<string, unknown> = {};
const job: Record<string, unknown> = {};
const events: Record<string, unknown> = {};
return {
queue: async (name, jobData) => {
job[name] = jobData;
},
shutdown: async () => undefined,
stopRepeatableJob: async () => true,
start: (name, jobFn) => {
queues[name] = jobFn;
workers[name] = jobFn;
},
listen: (name, event) => {
events[name] = event;
},
clearQueue: async () => {},
stopJobById: async () => {},
stopRepeatableJobByJobId: async () => true
};
};

@ -1,10 +0,0 @@
import { TSmtpSendMail, TSmtpService } from "@app/services/smtp/smtp-service";
export const mockSmtpServer = (): TSmtpService => {
const storage: TSmtpSendMail[] = [];
return {
sendMail: async (data) => {
storage.push(data);
}
};
};

@ -1,71 +0,0 @@
import { OrgMembershipRole } from "@app/db/schemas";
import { seedData1 } from "@app/db/seed-data";
export const createIdentity = async (name: string, role: string) => {
const createIdentityRes = await testServer.inject({
method: "POST",
url: "/api/v1/identities",
body: {
name,
role,
organizationId: seedData1.organization.id
},
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(createIdentityRes.statusCode).toBe(200);
return createIdentityRes.json().identity;
};
export const deleteIdentity = async (id: string) => {
const deleteIdentityRes = await testServer.inject({
method: "DELETE",
url: `/api/v1/identities/${id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(deleteIdentityRes.statusCode).toBe(200);
return deleteIdentityRes.json().identity;
};
describe("Identity v1", async () => {
test("Create identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull();
await deleteIdentity(newIdentity.id);
});
test("Update identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull();
const updatedIdentity = await testServer.inject({
method: "PATCH",
url: `/api/v1/identities/${newIdentity.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: "updated-mac-1",
role: OrgMembershipRole.Member
}
});
expect(updatedIdentity.statusCode).toBe(200);
expect(updatedIdentity.json().identity.name).toBe("updated-mac-1");
await deleteIdentity(newIdentity.id);
});
test("Delete Identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
const deletedIdentity = await deleteIdentity(newIdentity.id);
expect(deletedIdentity.name).toBe("mac1");
});
});

@ -1,46 +0,0 @@
import jsrp from "jsrp";
import { seedData1 } from "@app/db/seed-data";
describe("Login V1 Router", async () => {
// eslint-disable-next-line
const client = new jsrp.client();
await new Promise((resolve) => {
client.init({ username: seedData1.email, password: seedData1.password }, () => resolve(null));
});
let clientProof: string;
test("Login first phase", async () => {
const res = await testServer.inject({
method: "POST",
url: "/api/v3/auth/login1",
body: {
email: "test@localhost.local",
clientPublicKey: client.getPublicKey()
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("serverPublicKey");
expect(payload).toHaveProperty("salt");
client.setSalt(payload.salt);
client.setServerPublicKey(payload.serverPublicKey);
clientProof = client.getProof(); // called M1
});
test("Login second phase", async () => {
const res = await testServer.inject({
method: "POST",
url: "/api/v3/auth/login2",
body: {
email: seedData1.email,
clientProof
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("mfaEnabled");
expect(payload).toHaveProperty("token");
expect(payload.mfaEnabled).toBeFalsy();
});
});

@ -1,19 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
describe("Org V1 Router", async () => {
test("GET Org list", async () => {
const res = await testServer.inject({
method: "GET",
url: "/api/v1/organization",
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("organizations");
expect(payload).toEqual({
organizations: [expect.objectContaining({ name: seedData1.organization.name })]
});
});
});

@ -1,132 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project";
const createProjectEnvironment = async (name: string, slug: string) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name,
slug
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
return payload.environment;
};
const deleteProjectEnvironment = async (envId: string) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/workspace/${seedData1.project.id}/environments/${envId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
return payload.environment;
};
describe("Project Environment Router", async () => {
test("Get default environments", async () => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/workspace/${seedData1.project.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("workspace");
// check for default environments
expect(payload).toEqual({
workspace: expect.objectContaining({
name: seedData1.project.name,
id: seedData1.project.id,
slug: seedData1.project.slug,
environments: expect.arrayContaining([
expect.objectContaining(DEFAULT_PROJECT_ENVS[0]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[1]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[2])
])
})
});
// ensure only two default environments exist
expect(payload.workspace.environments.length).toBe(3);
});
const mockProjectEnv = { name: "temp", slug: "temp" }; // id will be filled in create op
test("Create environment", async () => {
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
expect(newEnvironment).toEqual(
expect.objectContaining({
id: expect.any(String),
name: mockProjectEnv.name,
slug: mockProjectEnv.slug,
projectId: seedData1.project.id,
position: DEFAULT_PROJECT_ENVS.length + 1,
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
);
await deleteProjectEnvironment(newEnvironment.id);
});
test("Update environment", async () => {
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
const updatedName = { name: "temp#2", slug: "temp2" };
const res = await testServer.inject({
method: "PATCH",
url: `/api/v1/workspace/${seedData1.project.id}/environments/${newEnvironment.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: updatedName.name,
slug: updatedName.slug,
position: 1
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual(
expect.objectContaining({
id: newEnvironment.id,
name: updatedName.name,
slug: updatedName.slug,
projectId: seedData1.project.id,
position: 1,
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
);
await deleteProjectEnvironment(newEnvironment.id);
});
test("Delete environment", async () => {
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
const deletedProjectEnvironment = await deleteProjectEnvironment(newEnvironment.id);
expect(deletedProjectEnvironment).toEqual(
expect.objectContaining({
id: deletedProjectEnvironment.id,
name: mockProjectEnv.name,
slug: mockProjectEnv.slug,
position: 4,
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
);
});
});

@ -1,165 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
const createFolder = async (dto: { path: string; name: string }) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: dto.name,
path: dto.path
}
});
expect(res.statusCode).toBe(200);
return res.json().folder;
};
const deleteFolder = async (dto: { path: string; id: string }) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${dto.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: dto.path
}
});
expect(res.statusCode).toBe(200);
return res.json().folder;
};
describe("Secret Folder Router", async () => {
test.each([
{ name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name });
// check for default environments
expect(createdFolder).toEqual(
expect.objectContaining({
name,
id: expect.any(String)
})
);
await deleteFolder({ path, id: createdFolder.id });
});
test.each([
{
path: "/",
expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
length: 3
}
},
{ path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } }
])("Get folders $path", async ({ path, expected }) => {
const newFolders = await Promise.all(expected.folders.map(({ name }) => createFolder({ name, path })));
const res = await testServer.inject({
method: "GET",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folders");
expect(payload.folders.length >= expected.folders.length).toBeTruthy();
expect(payload).toEqual({
folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
});
await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id })));
});
test("Update a deep folder", async () => {
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
expect(newFolder).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "folder-updated"
})
);
const resUpdatedFolders = await testServer.inject({
method: "GET",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/level1/level2"
}
});
expect(resUpdatedFolders.statusCode).toBe(200);
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders[0].name).toEqual("folder-updated");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
test("Delete a deep folder", async () => {
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${newFolder.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folder");
expect(payload.folder).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "folder-updated"
})
);
const resUpdatedFolders = await testServer.inject({
method: "GET",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/level1/level2"
}
});
expect(resUpdatedFolders.statusCode).toBe(200);
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0);
});
});

@ -1,206 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
const createSecretImport = async (importPath: string, importEnv: string) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/",
import: {
environment: importEnv,
path: importPath
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
const deleteSecretImport = async (id: string) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/secret-imports/${id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
describe("Secret Import Router", async () => {
test.each([
{ importEnv: "prod", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
// check for default environments
const payload = await createSecretImport(importPath, importEnv);
expect(payload).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
id: expect.any(String)
})
})
);
await deleteSecretImport(payload.id);
});
test("Get secret imports", async () => {
const createdImport1 = await createSecretImport("/", "prod");
const createdImport2 = await createSecretImport("/", "staging");
const res = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImports");
expect(payload.secretImports.length).toBe(2);
expect(payload.secretImports).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
id: expect.any(String)
})
})
])
);
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
});
test("Update secret import position", async () => {
const prodImportDetails = { path: "/", envSlug: "prod" };
const stagingImportDetails = { path: "/", envSlug: "staging" };
const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug);
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
const updateImportRes = await testServer.inject({
method: "PATCH",
url: `/api/v1/secret-imports/${createdImport1.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/",
import: {
position: 2
}
}
});
expect(updateImportRes.statusCode).toBe(200);
const payload = JSON.parse(updateImportRes.payload);
expect(payload).toHaveProperty("secretImport");
// check for default environments
expect(payload.secretImport).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
position: 2,
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.stringMatching(prodImportDetails.envSlug),
id: expect.any(String)
})
})
);
const secretImportsListRes = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(secretImportsListRes.statusCode).toBe(200);
const secretImportList = JSON.parse(secretImportsListRes.payload);
expect(secretImportList).toHaveProperty("secretImports");
expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id);
expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id);
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
});
test("Delete secret import position", async () => {
const createdImport1 = await createSecretImport("/", "prod");
const createdImport2 = await createSecretImport("/", "staging");
const deletedImport = await deleteSecretImport(createdImport1.id);
// check for default environments
expect(deletedImport).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
id: expect.any(String)
})
})
);
const secretImportsListRes = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(secretImportsListRes.statusCode).toBe(200);
const secretImportList = JSON.parse(secretImportsListRes.payload);
expect(secretImportList).toHaveProperty("secretImports");
expect(secretImportList.secretImports.length).toEqual(1);
expect(secretImportList.secretImports[0].position).toEqual(1);
await deleteSecretImport(createdImport2.id);
});
});

@ -1,9 +0,0 @@
describe("Status V1 Router", async () => {
test("Simple check", async () => {
const res = await testServer.inject({
method: "GET",
url: "/api/status"
});
expect(res.statusCode).toBe(200);
});
});

@ -1,579 +0,0 @@
import crypto from "node:crypto";
import { SecretType, TSecrets } from "@app/db/schemas";
import { decryptSecret, encryptSecret, getUserPrivateKey, seedData1 } from "@app/db/seed-data";
import { decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
const createServiceToken = async (
scopes: { environment: string; secretPath: string }[],
permissions: ("read" | "write")[]
) => {
const projectKeyRes = await testServer.inject({
method: "GET",
url: `/api/v2/workspace/${seedData1.project.id}/encrypted-key`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
const projectKeyEnc = JSON.parse(projectKeyRes.payload);
const userInfoRes = await testServer.inject({
method: "GET",
url: "/api/v2/users/me",
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
const { user: userInfo } = JSON.parse(userInfoRes.payload);
const privateKey = await getUserPrivateKey(seedData1.password, userInfo);
const projectKey = decryptAsymmetric({
ciphertext: projectKeyEnc.encryptedKey,
nonce: projectKeyEnc.nonce,
publicKey: projectKeyEnc.sender.publicKey,
privateKey
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8(projectKey, randomBytes);
const serviceTokenRes = await testServer.inject({
method: "POST",
url: "/api/v2/service-token",
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: "test-token",
workspaceId: seedData1.project.id,
scopes,
encryptedKey: ciphertext,
iv,
tag,
permissions,
expiresIn: null
}
});
expect(serviceTokenRes.statusCode).toBe(200);
const serviceTokenInfo = serviceTokenRes.json();
expect(serviceTokenInfo).toHaveProperty("serviceToken");
expect(serviceTokenInfo).toHaveProperty("serviceTokenData");
return `${serviceTokenInfo.serviceToken}.${randomBytes}`;
};
const deleteServiceToken = async () => {
const serviceTokenListRes = await testServer.inject({
method: "GET",
url: `/api/v1/workspace/${seedData1.project.id}/service-token-data`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(serviceTokenListRes.statusCode).toBe(200);
const serviceTokens = JSON.parse(serviceTokenListRes.payload).serviceTokenData as { name: string; id: string }[];
expect(serviceTokens.length).toBeGreaterThan(0);
const serviceTokenInfo = serviceTokens.find(({ name }) => name === "test-token");
expect(serviceTokenInfo).toBeDefined();
const deleteTokenRes = await testServer.inject({
method: "DELETE",
url: `/api/v2/service-token/${serviceTokenInfo?.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(deleteTokenRes.statusCode).toBe(200);
};
const createSecret = async (dto: {
projectKey: string;
path: string;
key: string;
value: string;
comment: string;
type?: SecretType;
token: string;
}) => {
const createSecretReqBody = {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: dto.type || SecretType.Shared,
secretPath: dto.path,
...encryptSecret(dto.projectKey, dto.key, dto.value, dto.comment)
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/${dto.key}`,
headers: {
authorization: `Bearer ${dto.token}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
const createdSecretPayload = JSON.parse(createSecRes.payload);
expect(createdSecretPayload).toHaveProperty("secret");
return createdSecretPayload.secret;
};
const deleteSecret = async (dto: { path: string; key: string; token: string }) => {
const deleteSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/${dto.key}`,
headers: {
authorization: `Bearer ${dto.token}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: dto.path
}
});
expect(deleteSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
return updatedSecretPayload.secret;
};
describe("Service token secret ops", async () => {
let serviceToken = "";
let projectKey = "";
let folderId = "";
beforeAll(async () => {
serviceToken = await createServiceToken(
[{ secretPath: "/**", environment: seedData1.environment.slug }],
["read", "write"]
);
// this is ensure cli service token decryptiong working fine
const serviceTokenInfoRes = await testServer.inject({
method: "GET",
url: "/api/v2/service-token",
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(serviceTokenInfoRes.statusCode).toBe(200);
const serviceTokenInfo = serviceTokenInfoRes.json();
const serviceTokenParts = serviceToken.split(".");
projectKey = decryptSymmetric128BitHexKeyUTF8({
key: serviceTokenParts[3],
tag: serviceTokenInfo.tag,
ciphertext: serviceTokenInfo.encryptedKey,
iv: serviceTokenInfo.iv
});
// create a deep folder
const folderCreate = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder",
path: "/nested1/nested2"
}
});
expect(folderCreate.statusCode).toBe(200);
folderId = folderCreate.json().folder.id;
});
afterAll(async () => {
await deleteServiceToken();
// create a deep folder
const deleteFolder = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${folderId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/nested1/nested2"
}
});
expect(deleteFolder.statusCode).toBe(200);
});
const testSecrets = [
{
path: "/",
secret: {
key: "ST-SEC",
value: "something-secret",
comment: "some comment"
}
},
{
path: "/nested1/nested2/folder",
secret: {
key: "NESTED-ST-SEC",
value: "something-secret",
comment: "some comment"
}
}
];
const getSecrets = async (environment: string, secretPath = "/") => {
const res = await testServer.inject({
method: "GET",
url: `/api/v3/secrets`,
headers: {
authorization: `Bearer ${serviceToken}`
},
query: {
secretPath,
environment,
workspaceId: seedData1.project.id
}
});
const secrets: TSecrets[] = JSON.parse(res.payload).secrets || [];
return secrets.map((el) => ({ ...decryptSecret(projectKey, el), type: el.type }));
};
test.each(testSecrets)("Create secret in path $path", async ({ secret, path }) => {
const createdSecret = await createSecret({ projectKey, path, ...secret, token: serviceToken });
const decryptedSecret = decryptSecret(projectKey, createdSecret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual(secret.value);
expect(decryptedSecret.comment).toEqual(secret.comment);
expect(decryptedSecret.version).toEqual(1);
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: secret.key,
value: secret.value,
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Get secret by name in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const getSecByNameRes = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${serviceToken}`
},
query: {
secretPath: path,
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug
}
});
expect(getSecByNameRes.statusCode).toBe(200);
const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload);
expect(getSecretByNamePayload).toHaveProperty("secret");
const decryptedSecret = decryptSecret(projectKey, getSecretByNamePayload.secret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual(secret.value);
expect(decryptedSecret.comment).toEqual(secret.comment);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Update secret in path $path", async ({ path, secret }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const updateSecretReqBody = {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: SecretType.Shared,
secretPath: path,
...encryptSecret(projectKey, secret.key, "new-value", secret.comment)
};
const updateSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/${secret.key}`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: updateSecretReqBody
});
expect(updateSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
const decryptedSecret = decryptSecret(projectKey, updatedSecretPayload.secret);
expect(decryptedSecret.key).toEqual(secret.key);
expect(decryptedSecret.value).toEqual("new-value");
expect(decryptedSecret.comment).toEqual(secret.comment);
// list secret should have updated value
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: secret.key,
value: "new-value",
type: SecretType.Shared
})
])
);
await deleteSecret({ path, key: secret.key, token: serviceToken });
});
test.each(testSecrets)("Delete secret in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, path, ...secret, token: serviceToken });
const deletedSecret = await deleteSecret({ path, key: secret.key, token: serviceToken });
const decryptedSecret = decryptSecret(projectKey, deletedSecret);
expect(decryptedSecret.key).toEqual(secret.key);
// shared secret deletion should delete personal ones also
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
key: secret.key,
type: SecretType.Shared
})
])
);
});
test.each(testSecrets)("Bulk create secrets in path $path", async ({ secret, path }) => {
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment)
}))
}
});
expect(createSharedSecRes.statusCode).toBe(200);
const createSharedSecPayload = JSON.parse(createSharedSecRes.payload);
expect(createSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.key}-${i + 1}`,
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
deleteSecret({ path, token: serviceToken, key: `BULK-${secret.key}-${i + 1}` })
)
);
});
test.each(testSecrets)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => {
await createSecret({ projectKey, ...secret, key: `BULK-${secret.key}-1`, path, token: serviceToken });
const createSharedSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, secret.value, secret.comment)
}))
}
});
expect(createSharedSecRes.statusCode).toBe(400);
await deleteSecret({ path, key: `BULK-${secret.key}-1`, token: serviceToken });
});
test.each(testSecrets)("Bulk update secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path })
)
);
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`,
...encryptSecret(projectKey, `BULK-${secret.key}-${i + 1}`, "update-value", secret.comment)
}))
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.key}-${i + 1}`,
value: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}`, token: serviceToken })
)
);
});
test.each(testSecrets)("Bulk delete secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) =>
createSecret({ projectKey, token: serviceToken, ...secret, key: `BULK-${secret.key}-${i + 1}`, path })
)
);
const deletedSharedSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/batch`,
headers: {
authorization: `Bearer ${serviceToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: path,
secrets: Array.from(Array(5)).map((_e, i) => ({
secretName: `BULK-${secret.key}-${i + 1}`
}))
}
});
expect(deletedSharedSecRes.statusCode).toBe(200);
const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload);
expect(deletedSecretPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.not.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
key: `BULK-${secret.value}-${i + 1}`,
type: SecretType.Shared
})
)
)
);
});
});
describe("Service token fail cases", async () => {
test("Unauthorized secret path access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: "/nested/deep"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(401);
expect(fetchSecrets.json().error).toBe("PermissionDenied");
await deleteServiceToken();
});
test("Unauthorized secret environment access", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read", "write"]
);
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: "prod",
secretPath: "/"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(401);
expect(fetchSecrets.json().error).toBe("PermissionDenied");
await deleteServiceToken();
});
test("Unauthorized write operation", async () => {
const serviceToken = await createServiceToken(
[{ secretPath: "/", environment: seedData1.environment.slug }],
["read"]
);
const writeSecrets = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/NEW`,
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
type: SecretType.Shared,
secretPath: "/",
// doesn't matter project key because this will fail before that due to read only access
...encryptSecret(crypto.randomBytes(16).toString("hex"), "NEW", "value", "")
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(writeSecrets.statusCode).toBe(401);
expect(writeSecrets.json().error).toBe("PermissionDenied");
// but read access should still work fine
const fetchSecrets = await testServer.inject({
method: "GET",
url: "/api/v3/secrets",
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
secretPath: "/"
},
headers: {
authorization: `Bearer ${serviceToken}`
}
});
expect(fetchSecrets.statusCode).toBe(200);
await deleteServiceToken();
});
});

File diff suppressed because it is too large Load Diff

@ -1,82 +0,0 @@
// eslint-disable-next-line
import "ts-node/register";
import dotenv from "dotenv";
import jwt from "jsonwebtoken";
import knex from "knex";
import path from "path";
import { seedData1 } from "@app/db/seed-data";
import { initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger";
import { main } from "@app/server/app";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { mockQueue } from "./mocks/queue";
import { mockSmtpServer } from "./mocks/smtp";
import { mockKeyStore } from "./mocks/keystore";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default {
name: "knex-env",
transformMode: "ssr",
async setup() {
const logger = await initLogger();
const cfg = initEnvConfig(logger);
const db = knex({
client: "pg",
connection: cfg.DB_CONNECTION_URI,
migrations: {
directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts",
tableName: "infisical_migrations"
},
seeds: {
directory: path.join(__dirname, "../src/db/seeds"),
extension: "ts"
}
});
try {
await db.migrate.latest();
await db.seed.run();
const smtp = mockSmtpServer();
const queue = mockQueue();
const keyStore = mockKeyStore();
const server = await main({ db, smtp, logger, queue, keyStore });
// @ts-expect-error type
globalThis.testServer = server;
// @ts-expect-error type
globalThis.jwtAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: seedData1.id,
tokenVersionId: seedData1.token.id,
authMethod: AuthMethod.EMAIL,
organizationId: seedData1.organization.id,
accessVersion: 1
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
);
} catch (error) {
console.log("[TEST] Error setting up environment", error);
await db.destroy();
throw error;
}
// custom setup
return {
async teardown() {
// @ts-expect-error type
await globalThis.testServer.close();
// @ts-expect-error type
delete globalThis.testServer;
// @ts-expect-error type
delete globalThis.jwtToken;
// called after all tests with this env have been run
await db.migrate.rollback({}, true);
await db.destroy();
}
};
}
};

46
backend/environment.d.ts vendored Normal file

@ -0,0 +1,46 @@
export {};
declare global {
namespace NodeJS {
interface ProcessEnv {
PORT: string;
ENCRYPTION_KEY: string;
SALT_ROUNDS: string;
JWT_AUTH_LIFETIME: string;
JWT_AUTH_SECRET: string;
JWT_REFRESH_LIFETIME: string;
JWT_REFRESH_SECRET: string;
JWT_SERVICE_SECRET: string;
JWT_SIGNUP_LIFETIME: string;
JWT_SIGNUP_SECRET: string;
MONGO_URL: string;
NODE_ENV: "development" | "staging" | "testing" | "production";
VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: string;
CLIENT_ID_HEROKU: string;
CLIENT_ID_VERCEL: string;
CLIENT_ID_NETLIFY: string;
CLIENT_ID_GITHUB: string;
CLIENT_ID_GITLAB: string;
CLIENT_SECRET_HEROKU: string;
CLIENT_SECRET_VERCEL: string;
CLIENT_SECRET_NETLIFY: string;
CLIENT_SECRET_GITHUB: string;
CLIENT_SECRET_GITLAB: string;
CLIENT_SLUG_VERCEL: string;
POSTHOG_HOST: string;
POSTHOG_PROJECT_API_KEY: string;
SENTRY_DSN: string;
SITE_URL: string;
SMTP_HOST: string;
SMTP_SECURE: string;
SMTP_PORT: string;
SMTP_USERNAME: string;
SMTP_PASSWORD: string;
SMTP_FROM_ADDRESS: string;
SMTP_FROM_NAME: string;
TELEMETRY_ENABLED: string;
LICENSE_KEY: string;
}
}
}

24
backend/healthcheck.js Normal file

@ -0,0 +1,24 @@
const http = require('http');
const PORT = process.env.PORT || 4000;
const options = {
host: 'localhost',
port: PORT,
timeout: 2000,
path: '/healthcheck'
};
const healthCheck = http.request(options, (res) => {
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
} else {
process.exit(1);
}
});
healthCheck.on('error', function (err) {
console.error(`HEALTH CHECK ERROR: ${err}`);
process.exit(1);
});
healthCheck.end();

BIN
backend/img/dashboard.png Normal file

Binary file not shown.

After

(image error) Size: 493 KiB

9
backend/jest.config.ts Normal file

@ -0,0 +1,9 @@
export default {
preset: "ts-jest",
testEnvironment: "node",
collectCoverageFrom: ["src/*.{js,ts}", "!**/node_modules/**"],
modulePaths: ["<rootDir>/src"],
testMatch: ["<rootDir>/tests/**/*.test.ts"],
setupFiles: ["<rootDir>/test-resources/env-vars.js"],
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"],
};

@ -1,6 +1,6 @@
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "tsx ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine"
}
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "ts-node ./src/index.ts"
}

34994
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,137 +1,148 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "./dist/main.mjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon",
"build": "tsup",
"start": "node dist/main.mjs",
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'",
"test:e2e": "vitest run -c vitest.e2e.config.ts",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
"generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
"migration:new": "tsx ./scripts/create-migration.ts",
"migration:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2",
"@types/jsonwebtoken": "^9.0.5",
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
"nodemon": "^3.0.2",
"pino-pretty": "^10.2.3",
"prompt-sync": "^4.2.0",
"rimraf": "^5.0.5",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.8",
"tsconfig-paths": "^4.2.0",
"tsup": "^8.0.1",
"tsx": "^4.4.0",
"typescript": "^5.3.2",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.2.2"
},
"dependencies": {
"@aws-sdk/client-iam": "^3.525.0",
"@aws-sdk/client-secrets-manager": "^3.504.0",
"@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^8.5.0",
"@fastify/etag": "^5.1.0",
"@fastify/formbody": "^7.4.0",
"@fastify/helmet": "^11.1.1",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.77.0",
"@sentry/tracing": "^7.48.0",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "^2.2.1",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-sdk": "^2.1553.0",
"axios": "^1.6.7",
"axios-retry": "^4.0.0",
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
"fastify-plugin": "^4.5.1",
"google-auth-library": "^9.9.0",
"googleapis": "^137.1.0",
"handlebars": "^4.7.8",
"argon2": "^0.30.3",
"aws-sdk": "^2.1364.0",
"axios": "^1.6.0",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.4.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.2.0",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-async-errors": "^3.1.1",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.2.1",
"ioredis": "^5.3.2",
"jmespath": "^0.16.0",
"jsonwebtoken": "^9.0.2",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"knex": "^3.0.1",
"ldapjs": "^3.0.7",
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"ms": "^2.1.3",
"mysql2": "^3.9.7",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"ora": "^7.0.1",
"oracledb": "^6.4.0",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^7.4.1",
"mysql2": "^3.6.2",
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"ora": "^5.4.1",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1",
"pg": "^8.11.3",
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"posthog-node": "^3.6.2",
"probot": "^13.0.0",
"smee-client": "^2.0.0",
"pino": "^8.16.1",
"pino-http": "^8.5.1",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
"query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2",
"swagger-ui-express": "^4.6.2",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"uuid": "^9.0.1",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.4"
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"zod": "^3.22.3"
},
"overrides": {
"rate-limit-mongo": {
"mongodb": "5.8.0"
}
},
"name": "infisical-api",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node build/index.js",
"dev": "nodemon index.js",
"swagger-autogen": "node ./swagger/index.ts",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Infisical/infisical-api.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/Infisical/infisical-api/issues"
},
"homepage": "https://github.com/Infisical/infisical-api#readme",
"description": "",
"devDependencies": {
"@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4",
"@swc/core": "^1.3.99",
"@swc/helpers": "^0.5.3",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/bull": "^4.10.0",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jest": "^29.5.0",
"@types/jmespath": "^0.15.1",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12",
"@types/pg": "^8.10.7",
"@types/picomatch": "^2.3.0",
"@types/pino": "^7.0.5",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.40.1",
"cross-env": "^7.0.3",
"eslint": "^8.26.0",
"eslint-plugin-unused-imports": "^2.0.0",
"install": "^0.13.0",
"jest": "^29.3.1",
"jest-junit": "^15.0.0",
"nodemon": "^2.0.19",
"npm": "^8.19.3",
"pino-pretty": "^10.2.3",
"regenerator-runtime": "^0.14.0",
"smee-client": "^1.2.3",
"supertest": "^6.3.3",
"swagger-autogen": "^2.23.5",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1"
},
"jest-junit": {
"outputDirectory": "reports",
"outputName": "jest-junit.xml",
"ancestorSeparator": " ",
"uniqueOutputName": "false",
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
}
}

@ -1,127 +0,0 @@
/* eslint-disable */
import { mkdirSync, writeFileSync } from "fs";
import path from "path";
import promptSync from "prompt-sync";
const prompt = promptSync({
sigint: true
});
console.log(`
Component List
--------------
1. Service component
2. DAL component
3. Router component
`);
const componentType = parseInt(prompt("Select a component: "), 10);
if (componentType === 1) {
const componentName = prompt("Enter service name: ");
const dir = path.join(__dirname, `../src/services/${componentName}`);
const pascalCase = componentName
.split("-")
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
.join("");
const camelCase = componentName
.split("-")
.map((el, index) => (index === 0 ? el : `${el[0].toUpperCase()}${el.slice(1)}`))
.join("");
const dalTypeName = `T${pascalCase}DALFactory`;
const dalName = `${camelCase}DALFactory`;
const serviceTypeName = `T${pascalCase}ServiceFactory`;
const serviceName = `${camelCase}ServiceFactory`;
mkdirSync(dir);
writeFileSync(
path.join(dir, `${componentName}-dal.ts`),
`import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
export type ${dalTypeName} = ReturnType<typeof ${dalName}>;
export const ${dalName} = (db: TDbClient) => {
return { };
};
`
);
writeFileSync(
path.join(dir, `${componentName}-service.ts`),
`import { ${dalTypeName} } from "./${componentName}-dal";
type ${serviceTypeName}Dep = {
${camelCase}DAL: ${dalTypeName};
};
export type ${serviceTypeName} = ReturnType<typeof ${serviceName}>;
export const ${serviceName} = ({ ${camelCase}DAL }: ${serviceTypeName}Dep) => {
return {};
};
`
);
writeFileSync(path.join(dir, `${componentName}-types.ts`), "");
} else if (componentType === 2) {
const componentName = prompt("Enter service name: ");
const componentPath = prompt("Path wrt service folder: ");
const pascalCase = componentName
.split("-")
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
.join("");
const camelCase = componentName
.split("-")
.map((el, index) => (index === 0 ? el : `${el[0].toUpperCase()}${el.slice(1)}`))
.join("");
const dalTypeName = `T${pascalCase}DALFactory`;
const dalName = `${camelCase}DALFactory`;
writeFileSync(
path.join(__dirname, "../src/services", componentPath, `${componentName}-dal.ts`),
`import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
export type ${dalTypeName} = ReturnType<typeof ${dalName}>;
export const ${dalName} = (db: TDbClient) => {
return { };
};
`
);
} else if (componentType === 3) {
const name = prompt("Enter router name: ");
const version = prompt("Version number: ");
const pascalCase = name
.split("-")
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
.join("");
writeFileSync(
path.join(__dirname, `../src/server/routes/v${Number(version)}/${name}-router.ts`),
`import { z } from "zod";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { readLimit } from "@app/server/config/rateLimiter";
export const register${pascalCase}Router = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({}),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {}
});
};
`
);
}

@ -1,16 +0,0 @@
/* eslint-disable */
import { execSync } from "child_process";
import path from "path";
import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true });
const migrationName = prompt("Enter name for migration: ");
// Remove spaces from migration name and replace with hyphens
const formattedMigrationName = migrationName.replace(/\s+/g, "-");
execSync(
`npx knex migrate:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${formattedMigrationName}`,
{ stdio: "inherit" }
);

@ -1,16 +0,0 @@
/* eslint-disable */
import { execSync } from "child_process";
import { readdirSync } from "fs";
import path from "path";
import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true });
const migrationName = prompt("Enter name for seedfile: ");
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seeds")).length || 1;
execSync(
`npx knex seed:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${
fileCounter + 1
}-${migrationName}`,
{ stdio: "inherit" }
);

@ -1,148 +0,0 @@
/* eslint-disable */
import dotenv from "dotenv";
import path from "path";
import knex from "knex";
import { writeFileSync } from "fs";
dotenv.config({
path: path.join(__dirname, "../../.env.migration")
});
const db = knex({
client: "pg",
connection: process.env.DB_CONNECTION_URI
});
const getZodPrimitiveType = (type: string) => {
switch (type) {
case "uuid":
return "z.string().uuid()";
case "character varying":
return "z.string()";
case "ARRAY":
return "z.string().array()";
case "boolean":
return "z.boolean()";
case "jsonb":
return "z.unknown()";
case "json":
return "z.unknown()";
case "timestamp with time zone":
return "z.date()";
case "integer":
return "z.number()";
case "bigint":
return "z.coerce.number()";
case "text":
return "z.string()";
default:
throw new Error(`Invalid type: ${type}`);
}
};
const getZodDefaultValue = (type: unknown, value: string | number | boolean | Object) => {
if (!value || value === "null") return;
switch (type) {
case "uuid":
return `.default("00000000-0000-0000-0000-000000000000")`;
case "character varying": {
if (value === "gen_random_uuid()") return;
if (typeof value === "string" && value.includes("::")) {
return `.default(${value.split("::")[0]})`;
}
return `.default(${value})`;
}
case "ARRAY":
return `.default(${value})`;
case "boolean":
return `.default(${value})`;
case "jsonb":
return "z.string()";
case "json":
return "z.string()";
case "timestamp with time zone": {
if (value === "CURRENT_TIMESTAMP") return;
return "z.string().datetime()";
}
case "integer": {
if ((value as string).includes("nextval")) return;
return `.default(${value})`;
}
case "bigint": {
if ((value as string).includes("nextval")) return;
return `.default(${parseInt((value as string).split("::")[0].slice(1, -1), 10)})`;
}
case "text":
if (typeof value === "string" && value.includes("::")) {
return `.default(${value.split("::")[0]})`;
}
return `.default(${value})`;
default:
throw new Error(`Invalid type: ${type}`);
}
};
const main = async () => {
const tables = (
await db("information_schema.tables")
.whereRaw("table_schema = current_schema()")
.select<{ tableName: string }[]>("table_name as tableName")
.orderBy("table_name")
).filter((el) => !el.tableName.includes("_migrations"));
for (let i = 0; i < tables.length; i += 1) {
const { tableName } = tables[i];
const columns = await db(tableName).columnInfo();
const columnNames = Object.keys(columns);
let schema = "";
for (let colNum = 0; colNum < columnNames.length; colNum++) {
const columnName = columnNames[colNum];
const colInfo = columns[columnName];
let ztype = getZodPrimitiveType(colInfo.type);
// don't put optional on id
if (colInfo.defaultValue && columnName !== "id") {
const { defaultValue } = colInfo;
const zSchema = getZodDefaultValue(colInfo.type, defaultValue);
if (zSchema) {
ztype = ztype.concat(zSchema);
}
}
if (colInfo.nullable) {
ztype = ztype.concat(".nullable().optional()");
}
schema = schema.concat(
`${!schema ? "\n" : ""} ${columnName}: ${ztype}${colNum === columnNames.length - 1 ? "" : ","}\n`
);
}
const dashcase = tableName.split("_").join("-");
const pascalCase = tableName
.split("_")
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
// the insert and update are changed to zod input type to use default cases
writeFileSync(
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
`// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ${pascalCase}Schema = z.object({${schema}});
export type T${pascalCase} = z.infer<typeof ${pascalCase}Schema>;
export type T${pascalCase}Insert = Omit<z.input<typeof ${pascalCase}Schema>, TImmutableDBKeys>;
export type T${pascalCase}Update = Partial<Omit<z.input<typeof ${pascalCase}Schema>, TImmutableDBKeys>>;
`
);
}
process.exit(0);
};
main();

8046
backend/spec.json Normal file

File diff suppressed because it is too large Load Diff

@ -1,18 +0,0 @@
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { Logger } from "pino";
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
declare global {
type FastifyZodProvider = FastifyInstance<
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
Readonly<Logger>,
ZodTypeProvider
>;
// used only for testing
const testServer: FastifyZodProvider;
const jwtAuthToken: string;
}

@ -1,151 +0,0 @@
import "fastify";
import { TUsers } from "@app/db/schemas";
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
declare module "fastify" {
interface FastifyRequest {
realIp: string;
// used for mfa session authentication
mfa: {
userId: string;
orgId?: string;
user: TUsers;
};
// identity injection. depending on which kinda of token the information is filled in auth
auth: TAuthMode;
permission: {
authMethod: ActorAuthMethod;
type: ActorType;
id: string;
orgId: string;
};
// passport data
passportUser: {
isUserCompleted: string;
providerAuthToken: string;
};
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
}
interface FastifyInstance {
services: {
login: TAuthLoginFactory;
password: TAuthPasswordFactory;
signup: TAuthSignupFactory;
authToken: TAuthTokenServiceFactory;
permission: TPermissionServiceFactory;
org: TOrgServiceFactory;
orgRole: TOrgRoleServiceFactory;
superAdmin: TSuperAdminServiceFactory;
user: TUserServiceFactory;
group: TGroupServiceFactory;
groupProject: TGroupProjectServiceFactory;
apiKey: TApiKeyServiceFactory;
project: TProjectServiceFactory;
projectMembership: TProjectMembershipServiceFactory;
projectEnv: TProjectEnvServiceFactory;
projectKey: TProjectKeyServiceFactory;
projectRole: TProjectRoleServiceFactory;
secret: TSecretServiceFactory;
secretTag: TSecretTagServiceFactory;
secretImport: TSecretImportServiceFactory;
projectBot: TProjectBotServiceFactory;
folder: TSecretFolderServiceFactory;
integration: TIntegrationServiceFactory;
integrationAuth: TIntegrationAuthServiceFactory;
webhook: TWebhookServiceFactory;
serviceToken: TServiceTokenServiceFactory;
identity: TIdentityServiceFactory;
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory;
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
secretRotation: TSecretRotationServiceFactory;
snapshot: TSecretSnapshotServiceFactory;
saml: TSamlConfigServiceFactory;
scim: TScimServiceFactory;
ldap: TLdapConfigServiceFactory;
auditLog: TAuditLogServiceFactory;
auditLogStream: TAuditLogStreamServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory;
secretBlindIndex: TSecretBlindIndexServiceFactory;
telemetry: TTelemetryServiceFactory;
dynamicSecret: TDynamicSecretServiceFactory;
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
store: {
user: Pick<TUserDALFactory, "findById">;
};
}
}

@ -1,506 +0,0 @@
import { Knex } from "knex";
import {
TableName,
TAccessApprovalPolicies,
TAccessApprovalPoliciesApprovers,
TAccessApprovalPoliciesApproversInsert,
TAccessApprovalPoliciesApproversUpdate,
TAccessApprovalPoliciesInsert,
TAccessApprovalPoliciesUpdate,
TAccessApprovalRequests,
TAccessApprovalRequestsInsert,
TAccessApprovalRequestsReviewers,
TAccessApprovalRequestsReviewersInsert,
TAccessApprovalRequestsReviewersUpdate,
TAccessApprovalRequestsUpdate,
TApiKeys,
TApiKeysInsert,
TApiKeysUpdate,
TAuditLogs,
TAuditLogsInsert,
TAuditLogStreams,
TAuditLogStreamsInsert,
TAuditLogStreamsUpdate,
TAuditLogsUpdate,
TAuthTokens,
TAuthTokenSessions,
TAuthTokenSessionsInsert,
TAuthTokenSessionsUpdate,
TAuthTokensInsert,
TAuthTokensUpdate,
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate,
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate,
TDynamicSecrets,
TDynamicSecretsInsert,
TDynamicSecretsUpdate,
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,
TGitAppInstallSessionsUpdate,
TGitAppOrg,
TGitAppOrgInsert,
TGitAppOrgUpdate,
TGroupProjectMembershipRoles,
TGroupProjectMembershipRolesInsert,
TGroupProjectMembershipRolesUpdate,
TGroupProjectMemberships,
TGroupProjectMembershipsInsert,
TGroupProjectMembershipsUpdate,
TGroups,
TGroupsInsert,
TGroupsUpdate,
TIdentities,
TIdentitiesInsert,
TIdentitiesUpdate,
TIdentityAccessTokens,
TIdentityAccessTokensInsert,
TIdentityAccessTokensUpdate,
TIdentityAwsAuths,
TIdentityAwsAuthsInsert,
TIdentityAwsAuthsUpdate,
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate,
TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate,
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
TIdentityProjectAdditionalPrivilege,
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate,
TIdentityProjectMembershipRole,
TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate,
TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate,
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
TIdentityUaClientSecretsUpdate,
TIdentityUniversalAuths,
TIdentityUniversalAuthsInsert,
TIdentityUniversalAuthsUpdate,
TIncidentContacts,
TIncidentContactsInsert,
TIncidentContactsUpdate,
TIntegrationAuths,
TIntegrationAuthsInsert,
TIntegrationAuthsUpdate,
TIntegrations,
TIntegrationsInsert,
TIntegrationsUpdate,
TLdapConfigs,
TLdapConfigsInsert,
TLdapConfigsUpdate,
TLdapGroupMaps,
TLdapGroupMapsInsert,
TLdapGroupMapsUpdate,
TOrganizations,
TOrganizationsInsert,
TOrganizationsUpdate,
TOrgBots,
TOrgBotsInsert,
TOrgBotsUpdate,
TOrgMemberships,
TOrgMembershipsInsert,
TOrgMembershipsUpdate,
TOrgRoles,
TOrgRolesInsert,
TOrgRolesUpdate,
TProjectBots,
TProjectBotsInsert,
TProjectBotsUpdate,
TProjectEnvironments,
TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate,
TProjectKeys,
TProjectKeysInsert,
TProjectKeysUpdate,
TProjectMemberships,
TProjectMembershipsInsert,
TProjectMembershipsUpdate,
TProjectRoles,
TProjectRolesInsert,
TProjectRolesUpdate,
TProjects,
TProjectsInsert,
TProjectsUpdate,
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate,
TProjectUserMembershipRoles,
TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate,
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
TScimTokens,
TScimTokensInsert,
TScimTokensUpdate,
TSecretApprovalPolicies,
TSecretApprovalPoliciesApprovers,
TSecretApprovalPoliciesApproversInsert,
TSecretApprovalPoliciesApproversUpdate,
TSecretApprovalPoliciesInsert,
TSecretApprovalPoliciesUpdate,
TSecretApprovalRequests,
TSecretApprovalRequestSecretTags,
TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate,
TSecretApprovalRequestsInsert,
TSecretApprovalRequestsReviewers,
TSecretApprovalRequestsReviewersInsert,
TSecretApprovalRequestsReviewersUpdate,
TSecretApprovalRequestsSecrets,
TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsUpdate,
TSecretApprovalRequestsUpdate,
TSecretBlindIndexes,
TSecretBlindIndexesInsert,
TSecretBlindIndexesUpdate,
TSecretFolders,
TSecretFoldersInsert,
TSecretFoldersUpdate,
TSecretFolderVersions,
TSecretFolderVersionsInsert,
TSecretFolderVersionsUpdate,
TSecretImports,
TSecretImportsInsert,
TSecretImportsUpdate,
TSecretRotationOutputs,
TSecretRotationOutputsInsert,
TSecretRotationOutputsUpdate,
TSecretRotations,
TSecretRotationsInsert,
TSecretRotationsUpdate,
TSecrets,
TSecretScanningGitRisks,
TSecretScanningGitRisksInsert,
TSecretScanningGitRisksUpdate,
TSecretsInsert,
TSecretSnapshotFolders,
TSecretSnapshotFoldersInsert,
TSecretSnapshotFoldersUpdate,
TSecretSnapshots,
TSecretSnapshotSecrets,
TSecretSnapshotSecretsInsert,
TSecretSnapshotSecretsUpdate,
TSecretSnapshotsInsert,
TSecretSnapshotsUpdate,
TSecretsUpdate,
TSecretTagJunction,
TSecretTagJunctionInsert,
TSecretTagJunctionUpdate,
TSecretTags,
TSecretTagsInsert,
TSecretTagsUpdate,
TSecretVersions,
TSecretVersionsInsert,
TSecretVersionsUpdate,
TSecretVersionTagJunction,
TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate,
TServiceTokens,
TServiceTokensInsert,
TServiceTokensUpdate,
TSuperAdmin,
TSuperAdminInsert,
TSuperAdminUpdate,
TTrustedIps,
TTrustedIpsInsert,
TTrustedIpsUpdate,
TUserActions,
TUserActionsInsert,
TUserActionsUpdate,
TUserAliases,
TUserAliasesInsert,
TUserAliasesUpdate,
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate,
TUserGroupMembership,
TUserGroupMembershipInsert,
TUserGroupMembershipUpdate,
TUsers,
TUsersInsert,
TUsersUpdate,
TWebhooks,
TWebhooksInsert,
TWebhooksUpdate
} from "@app/db/schemas";
import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
declare module "knex/types/tables" {
interface Tables {
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
[TableName.UserGroupMembership]: Knex.CompositeTableType<
TUserGroupMembership,
TUserGroupMembershipInsert,
TUserGroupMembershipUpdate
>;
[TableName.GroupProjectMembership]: Knex.CompositeTableType<
TGroupProjectMemberships,
TGroupProjectMembershipsInsert,
TGroupProjectMembershipsUpdate
>;
[TableName.GroupProjectMembershipRole]: Knex.CompositeTableType<
TGroupProjectMembershipRoles,
TGroupProjectMembershipRolesInsert,
TGroupProjectMembershipRolesUpdate
>;
[TableName.UserAliases]: Knex.CompositeTableType<TUserAliases, TUserAliasesInsert, TUserAliasesUpdate>;
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate
>;
[TableName.AuthTokens]: Knex.CompositeTableType<TAuthTokens, TAuthTokensInsert, TAuthTokensUpdate>;
[TableName.AuthTokenSession]: Knex.CompositeTableType<
TAuthTokenSessions,
TAuthTokenSessionsInsert,
TAuthTokenSessionsUpdate
>;
[TableName.BackupPrivateKey]: Knex.CompositeTableType<
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate
>;
[TableName.Organization]: Knex.CompositeTableType<TOrganizations, TOrganizationsInsert, TOrganizationsUpdate>;
[TableName.OrgMembership]: Knex.CompositeTableType<TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate>;
[TableName.OrgRoles]: Knex.CompositeTableType<TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate>;
[TableName.IncidentContact]: Knex.CompositeTableType<
TIncidentContacts,
TIncidentContactsInsert,
TIncidentContactsUpdate
>;
[TableName.UserAction]: Knex.CompositeTableType<TUserActions, TUserActionsInsert, TUserActionsUpdate>;
[TableName.SuperAdmin]: Knex.CompositeTableType<TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate>;
[TableName.ApiKey]: Knex.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
[TableName.Project]: Knex.CompositeTableType<TProjects, TProjectsInsert, TProjectsUpdate>;
[TableName.ProjectMembership]: Knex.CompositeTableType<
TProjectMemberships,
TProjectMembershipsInsert,
TProjectMembershipsUpdate
>;
[TableName.Environment]: Knex.CompositeTableType<
TProjectEnvironments,
TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate
>;
[TableName.ProjectBot]: Knex.CompositeTableType<TProjectBots, TProjectBotsInsert, TProjectBotsUpdate>;
[TableName.ProjectUserMembershipRole]: Knex.CompositeTableType<
TProjectUserMembershipRoles,
TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate
>;
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
[TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate
>;
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
[TableName.SecretReference]: Knex.CompositeTableType<
TSecretReferences,
TSecretReferencesInsert,
TSecretReferencesUpdate
>;
[TableName.SecretBlindIndex]: Knex.CompositeTableType<
TSecretBlindIndexes,
TSecretBlindIndexesInsert,
TSecretBlindIndexesUpdate
>;
[TableName.SecretVersion]: Knex.CompositeTableType<TSecretVersions, TSecretVersionsInsert, TSecretVersionsUpdate>;
[TableName.SecretFolder]: Knex.CompositeTableType<TSecretFolders, TSecretFoldersInsert, TSecretFoldersUpdate>;
[TableName.SecretFolderVersion]: Knex.CompositeTableType<
TSecretFolderVersions,
TSecretFolderVersionsInsert,
TSecretFolderVersionsUpdate
>;
[TableName.SecretTag]: Knex.CompositeTableType<TSecretTags, TSecretTagsInsert, TSecretTagsUpdate>;
[TableName.SecretImport]: Knex.CompositeTableType<TSecretImports, TSecretImportsInsert, TSecretImportsUpdate>;
[TableName.Integration]: Knex.CompositeTableType<TIntegrations, TIntegrationsInsert, TIntegrationsUpdate>;
[TableName.Webhook]: Knex.CompositeTableType<TWebhooks, TWebhooksInsert, TWebhooksUpdate>;
[TableName.ServiceToken]: Knex.CompositeTableType<TServiceTokens, TServiceTokensInsert, TServiceTokensUpdate>;
[TableName.IntegrationAuth]: Knex.CompositeTableType<
TIntegrationAuths,
TIntegrationAuthsInsert,
TIntegrationAuthsUpdate
>;
[TableName.Identity]: Knex.CompositeTableType<TIdentities, TIdentitiesInsert, TIdentitiesUpdate>;
[TableName.IdentityUniversalAuth]: Knex.CompositeTableType<
TIdentityUniversalAuths,
TIdentityUniversalAuthsInsert,
TIdentityUniversalAuthsUpdate
>;
[TableName.IdentityKubernetesAuth]: Knex.CompositeTableType<
TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate
>;
[TableName.IdentityGcpAuth]: Knex.CompositeTableType<
TIdentityGcpAuths,
TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate
>;
[TableName.IdentityAwsAuth]: Knex.CompositeTableType<
TIdentityAwsAuths,
TIdentityAwsAuthsInsert,
TIdentityAwsAuthsUpdate
>;
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
TIdentityUaClientSecretsUpdate
>;
[TableName.IdentityAccessToken]: Knex.CompositeTableType<
TIdentityAccessTokens,
TIdentityAccessTokensInsert,
TIdentityAccessTokensUpdate
>;
[TableName.IdentityOrgMembership]: Knex.CompositeTableType<
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate
>;
[TableName.IdentityProjectMembership]: Knex.CompositeTableType<
TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate
>;
[TableName.IdentityProjectMembershipRole]: Knex.CompositeTableType<
TIdentityProjectMembershipRole,
TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate
>;
[TableName.IdentityProjectAdditionalPrivilege]: Knex.CompositeTableType<
TIdentityProjectAdditionalPrivilege,
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate
>;
[TableName.AccessApprovalPolicy]: Knex.CompositeTableType<
TAccessApprovalPolicies,
TAccessApprovalPoliciesInsert,
TAccessApprovalPoliciesUpdate
>;
[TableName.AccessApprovalPolicyApprover]: Knex.CompositeTableType<
TAccessApprovalPoliciesApprovers,
TAccessApprovalPoliciesApproversInsert,
TAccessApprovalPoliciesApproversUpdate
>;
[TableName.AccessApprovalRequest]: Knex.CompositeTableType<
TAccessApprovalRequests,
TAccessApprovalRequestsInsert,
TAccessApprovalRequestsUpdate
>;
[TableName.AccessApprovalRequestReviewer]: Knex.CompositeTableType<
TAccessApprovalRequestsReviewers,
TAccessApprovalRequestsReviewersInsert,
TAccessApprovalRequestsReviewersUpdate
>;
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,
TSecretApprovalPoliciesInsert,
TSecretApprovalPoliciesUpdate
>;
[TableName.SecretApprovalPolicyApprover]: Knex.CompositeTableType<
TSecretApprovalPoliciesApprovers,
TSecretApprovalPoliciesApproversInsert,
TSecretApprovalPoliciesApproversUpdate
>;
[TableName.SecretApprovalRequest]: Knex.CompositeTableType<
TSecretApprovalRequests,
TSecretApprovalRequestsInsert,
TSecretApprovalRequestsUpdate
>;
[TableName.SecretApprovalRequestReviewer]: Knex.CompositeTableType<
TSecretApprovalRequestsReviewers,
TSecretApprovalRequestsReviewersInsert,
TSecretApprovalRequestsReviewersUpdate
>;
[TableName.SecretApprovalRequestSecret]: Knex.CompositeTableType<
TSecretApprovalRequestsSecrets,
TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsUpdate
>;
[TableName.SecretApprovalRequestSecretTag]: Knex.CompositeTableType<
TSecretApprovalRequestSecretTags,
TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate
>;
[TableName.SecretRotation]: Knex.CompositeTableType<
TSecretRotations,
TSecretRotationsInsert,
TSecretRotationsUpdate
>;
[TableName.SecretRotationOutput]: Knex.CompositeTableType<
TSecretRotationOutputs,
TSecretRotationOutputsInsert,
TSecretRotationOutputsUpdate
>;
[TableName.Snapshot]: Knex.CompositeTableType<TSecretSnapshots, TSecretSnapshotsInsert, TSecretSnapshotsUpdate>;
[TableName.SnapshotSecret]: Knex.CompositeTableType<
TSecretSnapshotSecrets,
TSecretSnapshotSecretsInsert,
TSecretSnapshotSecretsUpdate
>;
[TableName.SnapshotFolder]: Knex.CompositeTableType<
TSecretSnapshotFolders,
TSecretSnapshotFoldersInsert,
TSecretSnapshotFoldersUpdate
>;
[TableName.DynamicSecret]: Knex.CompositeTableType<TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate>;
[TableName.DynamicSecretLease]: Knex.CompositeTableType<
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate
>;
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
[TableName.AuditLogStream]: Knex.CompositeTableType<
TAuditLogStreams,
TAuditLogStreamsInsert,
TAuditLogStreamsUpdate
>;
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,
TGitAppInstallSessionsUpdate
>;
[TableName.GitAppOrg]: Knex.CompositeTableType<TGitAppOrg, TGitAppOrgInsert, TGitAppOrgUpdate>;
[TableName.SecretScanningGitRisk]: Knex.CompositeTableType<
TSecretScanningGitRisks,
TSecretScanningGitRisksInsert,
TSecretScanningGitRisksUpdate
>;
[TableName.TrustedIps]: Knex.CompositeTableType<TTrustedIps, TTrustedIpsInsert, TTrustedIpsUpdate>;
// Junction tables
[TableName.JnSecretTag]: Knex.CompositeTableType<
TSecretTagJunction,
TSecretTagJunctionInsert,
TSecretTagJunctionUpdate
>;
[TableName.SecretVersionTag]: Knex.CompositeTableType<
TSecretVersionTagJunction,
TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate
>;
}
}

@ -1 +0,0 @@
declare module "passport-gitlab2";

43
backend/src/bootstrap.ts Normal file

@ -0,0 +1,43 @@
import ora from "ora";
import nodemailer from "nodemailer";
import { getSmtpHost, getSmtpPort } from "./config";
import { logger } from "./utils/logging";
import mongoose from "mongoose";
import { redisClient } from "./services/RedisService";
type BootstrapOpt = {
transporter: nodemailer.Transporter;
};
export const bootstrap = async ({ transporter }: BootstrapOpt) => {
const spinner = ora().start();
spinner.info("Checking configurations...");
spinner.info("Testing smtp connection");
await transporter
.verify()
.then(async () => {
spinner.succeed("SMTP successfully connected");
})
.catch(async (err) => {
spinner.fail(`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
logger.error(err);
});
spinner.info("Testing mongodb connection");
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
spinner.fail("Mongo DB - Failed to connect");
} else {
spinner.succeed("Mongodb successfully connected");
}
spinner.info("Testing redis connection");
const redisPing = await redisClient?.ping();
if (!redisPing) {
spinner.fail("Redis - Failed to connect");
} else {
spinner.succeed("Redis successfully connected");
}
spinner.stop();
};

173
backend/src/config/index.ts Normal file

@ -0,0 +1,173 @@
import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!
});
export const getPort = async () => (await client.getSecret("PORT")).secretValue || 4000;
export const getEncryptionKey = async () => {
const secretValue = (await client.getSecret("ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
};
export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
};
export const getInviteOnlySignup = async () =>
(await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true";
export const getSaltRounds = async () =>
parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10;
export const getAuthSecret = async () =>
(await client.getSecret("JWT_AUTH_SECRET")).secretValue ??
(await client.getSecret("AUTH_SECRET")).secretValue;
export const getJwtAuthLifetime = async () =>
(await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
export const getJwtMfaLifetime = async () =>
(await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtRefreshLifetime = async () =>
(await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
export const getJwtServiceSecret = async () =>
(await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
export const getJwtSignupLifetime = async () =>
(await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
export const getJwtProviderAuthLifetime = async () =>
(await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
export const getNodeEnv = async () =>
(await client.getSecret("NODE_ENV")).secretValue || "production";
export const getVerboseErrorOutput = async () =>
(await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
export const getLokiHost = async () => (await client.getSecret("LOKI_HOST")).secretValue;
export const getClientIdAzure = async () => (await client.getSecret("CLIENT_ID_AZURE")).secretValue;
export const getClientIdHeroku = async () =>
(await client.getSecret("CLIENT_ID_HEROKU")).secretValue;
export const getClientIdVercel = async () =>
(await client.getSecret("CLIENT_ID_VERCEL")).secretValue;
export const getClientIdNetlify = async () =>
(await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
export const getClientIdGitHub = async () =>
(await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientIdGitLab = async () =>
(await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdBitBucket = async () =>
(await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientIdGCPSecretManager = async () =>
(await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue;
export const getClientSecretAzure = async () =>
(await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () =>
(await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSecretVercel = async () =>
(await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
export const getClientSecretNetlify = async () =>
(await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
export const getClientSecretGitHub = async () =>
(await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () =>
(await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretBitBucket = async () =>
(await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSecretGCPSecretManager = async () =>
(await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
export const getClientSlugVercel = async () =>
(await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getClientIdGoogleLogin = async () =>
(await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
export const getClientSecretGoogleLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () =>
(await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getClientIdGitLabLogin = async () =>
(await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
export const getClientSecretGitLabLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
export const getUrlGitLabLogin = async () =>
(await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
export const getAwsCloudWatchLog = async () => {
const logGroupName =
(await client.getSecret("AWS_CLOUDWATCH_LOG_GROUP_NAME")).secretValue || "infisical-log-stream";
const region = (await client.getSecret("AWS_CLOUDWATCH_LOG_REGION")).secretValue;
const accessKeyId = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID")).secretValue;
const accessKeySecret = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET"))
.secretValue;
const interval = parseInt(
(await client.getSecret("AWS_CLOUDWATCH_LOG_INTERVAL")).secretValue || 1000,
10
);
if (!region || !accessKeyId || !accessKeySecret) return;
return { logGroupName, region, accessKeySecret, accessKeyId, interval };
};
export const getPostHogHost = async () =>
(await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () =>
(await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue ||
"phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;
export const getSiteURL = async () => (await client.getSecret("SITE_URL")).secretValue;
export const getSmtpHost = async () => (await client.getSecret("SMTP_HOST")).secretValue;
export const getSmtpSecure = async () =>
(await client.getSecret("SMTP_SECURE")).secretValue === "true" || false;
export const getSmtpPort = async () =>
parseInt((await client.getSecret("SMTP_PORT")).secretValue) || 587;
export const getSmtpUsername = async () => (await client.getSecret("SMTP_USERNAME")).secretValue;
export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWORD")).secretValue;
export const getSmtpFromAddress = async () =>
(await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
export const getSmtpFromName = async () =>
(await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
export const getSecretScanningWebhookProxy = async () =>
(await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
export const getSecretScanningWebhookSecret = async () =>
(await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
export const getSecretScanningGitAppId = async () =>
(await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
export const getSecretScanningPrivateKey = async () =>
(await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
export const getRedisUrl = async () => (await client.getSecret("REDIS_URL")).secretValue;
export const getIsInfisicalCloud = async () =>
(await client.getSecret("INFISICAL_CLOUD")).secretValue === "true";
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
};
export const getLicenseServerKey = async () => {
const secretValue = (await client.getSecret("LICENSE_SERVER_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
};
export const getLicenseServerUrl = async () =>
(await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com";
export const getTelemetryEnabled = async () =>
(await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true;
export const getLoopsApiKey = async () => (await client.getSecret("LOOPS_API_KEY")).secretValue;
export const getSmtpConfigured = async () =>
(await client.getSecret("SMTP_HOST")).secretValue == "" ||
(await client.getSecret("SMTP_HOST")).secretValue == undefined
? false
: true;
export const getHttpsEnabled = async () => {
if ((await getNodeEnv()) != "production") {
// no https for anything other than prod
return false;
}
if (
(await client.getSecret("HTTPS_ENABLED")).secretValue == undefined ||
(await client.getSecret("HTTPS_ENABLED")).secretValue == ""
) {
// default when no value present
return true;
}
return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true;
};

@ -0,0 +1,124 @@
import axios from "axios";
import axiosRetry from "axios-retry";
import {
getLicenseKeyAuthToken,
getLicenseServerKeyAuthToken,
setLicenseKeyAuthToken,
setLicenseServerKeyAuthToken,
} from "./storage";
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl,
} from "./index";
// should have JWT to interact with the license server
export const licenseServerKeyRequest = axios.create();
export const licenseKeyRequest = axios.create();
export const standardRequest = axios.create();
// add retry functionality to the axios instance
axiosRetry(standardRequest, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
retryCondition: (error) => {
// only retry if the error is a network error or a 5xx server error
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
},
});
export const refreshLicenseServerKeyToken = async () => {
const licenseServerKey = await getLicenseServerKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
{
headers: {
"X-API-KEY": licenseServerKey,
},
}
);
setLicenseServerKeyAuthToken(token);
return token;
}
export const refreshLicenseKeyToken = async () => {
const licenseKey = await getLicenseKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-login`, {},
{
headers: {
"X-API-KEY": licenseKey,
},
}
);
setLicenseKeyAuthToken(token);
return token;
}
licenseServerKeyRequest.interceptors.request.use((config) => {
const token = getLicenseServerKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseServerKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseServerKeyToken();
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseServerKeyRequest(originalRequest);
}
return Promise.reject(err);
});
licenseKeyRequest.interceptors.request.use((config) => {
const token = getLicenseKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseKeyToken();
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseKeyRequest(originalRequest);
}
return Promise.reject(err);
});

@ -0,0 +1,24 @@
import { IServerConfig, ServerConfig } from "../models/serverConfig";
let serverConfig: IServerConfig;
export const serverConfigInit = async () => {
const cfg = await ServerConfig.findOne({});
if (!cfg) {
const cfg = new ServerConfig();
await cfg.save();
serverConfig = cfg;
} else {
serverConfig = cfg;
}
return serverConfig;
};
export const getServerConfig = () => serverConfig;
export const updateServerConfig = async (data: Partial<IServerConfig>) => {
const cfg = await ServerConfig.findByIdAndUpdate(serverConfig._id, data, { new: true });
if (!cfg) throw new Error("Failed to update server config");
serverConfig = cfg;
return serverConfig;
};

@ -0,0 +1,30 @@
const MemoryLicenseServerKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken,
};
};
const MemoryLicenseKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken,
};
};
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;

@ -0,0 +1,100 @@
import { Request, Response } from "express";
import { getHttpsEnabled } from "../../config";
import { getServerConfig, updateServerConfig as setServerConfig } from "../../config/serverConfig";
import { initializeDefaultOrg, issueAuthTokens } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
import { User } from "../../models";
import { TelemetryService } from "../../services";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/admin";
export const getServerConfigInfo = (_req: Request, res: Response) => {
const config = getServerConfig();
return res.send({ config });
};
export const updateServerConfig = async (req: Request, res: Response) => {
const {
body: { allowSignUp }
} = await validateRequest(reqValidator.UpdateServerConfigV1, req);
const config = await setServerConfig({ allowSignUp });
return res.send({ config });
};
export const adminSignUp = async (req: Request, res: Response) => {
const cfg = getServerConfig();
if (cfg.initialized) throw UnauthorizedRequestError({ message: "Admin has been created" });
const {
body: {
email,
publicKey,
salt,
lastName,
verifier,
firstName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.SignupV1, req);
let user = await User.findOne({ email });
if (user) throw BadRequestError({ message: "User already exist" });
user = new User({
email,
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
superAdmin: true
});
await user.save();
await initializeDefaultOrg({ organizationName: "Admin Org", user });
await setServerConfig({ initialized: true });
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
const token = tokens.token;
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "admin initialization",
properties: {
email: user.email,
lastName,
firstName
}
});
}
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
return res.status(200).send({
message: "Successfully set up admin account",
user,
token
});
};

@ -0,0 +1,277 @@
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp");
import {
LoginSRPDetail,
TokenVersion,
User
} from "../../models";
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import { AuthTokenType } from "../../variables";
import {
BadRequestError,
UnauthorizedRequestError
} from "../../utils/errors";
import {
getAuthSecret,
getHttpsEnabled,
getJwtAuthLifetime,
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface AuthnJwtPayload extends jwt.JwtPayload {
authTokenType: AuthTokenType;
}
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
refreshVersion?: number;
}
export interface IdentityAccessTokenJwtPayload extends jwt.JwtPayload {
_id: string;
clientSecretId: string;
identityAccessTokenId: string;
authTokenType: string;
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const {
body: { email, clientPublicKey }
} = await validateRequest(reqValidator.Login1V1, req);
const user = await User.findOne({
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
const {
body: { email, clientProof }
} = await validateRequest(reqValidator.Login2V1, req);
const user = await User.findOne({
email
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
if (!user) throw new Error("Failed to find user");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetailFromDB) {
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// issue tokens
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
});
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
}
);
};
/**
* Log out user
* @param req
* @param res
* @returns
*/
export const logout = async (req: Request, res: Response) => {
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId);
}
// clear httpOnly cookie
res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean
});
return res.status(200).send({
message: "Successfully logged out."
});
};
export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany(
{
user: req.user._id
},
{
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({
message: "Successfully revoked all sessions."
});
};
/**
* Return user is authenticated
* @param req
* @param res
* @returns
*/
export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({
message: "Authenticated"
});
};
/**
* Return new JWT access token by first validating the refresh token
* @param req
* @param res
* @returns
*/
export const getNewToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.jid;
if (!refreshToken)
throw BadRequestError({
message: "Failed to find refresh token in request cookies"
});
const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getAuthSecret());
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw UnauthorizedRequestError();
const user = await User.findOne({
_id: decodedToken.userId
}).select("+publicKey +refreshVersion +accessVersion");
if (!user) throw new Error("Failed to authenticate unfound user");
if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account");
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion)
throw UnauthorizedRequestError({
message: "Failed to validate refresh token"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
throw BadRequestError({
message: "Failed to validate refresh token"
});
const token = createToken({
payload: {
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
token
});
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
};

@ -0,0 +1,135 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Bot, BotKey } from "../../models";
import { createBot } from "../../helpers/bot";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/bot";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
interface BotKey {
encryptedKey: string;
nonce: string;
}
/**
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
* then create and return a new bot.
* @param req
* @param res
* @returns
*/
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
let bot = await Bot.findOne({
workspace: workspaceId
});
if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it
bot = await createBot({
name: "Infisical Bot",
workspaceId: new Types.ObjectId(workspaceId)
});
}
return res.status(200).send({
bot
});
};
/**
* Return bot with id [req.bot._id] with active state set to [isActive].
* @param req
* @param res
* @returns
*/
export const setBotActiveState = async (req: Request, res: Response) => {
const {
body: { botKey, isActive },
params: { botId }
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
const bot = await Bot.findById(botId);
if (!bot) {
throw BadRequestError({ message: "Bot not found" });
}
const userId = req.user._id;
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: bot.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
if (isActive) {
// bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({
message: "Failed to set bot state to active - missing bot key"
});
}
await BotKey.findOneAndUpdate(
{
workspace: bot.workspace
},
{
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: userId,
bot: bot._id,
workspace: bot.workspace
},
{
upsert: true,
new: true
}
);
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: bot._id
});
}
const updatedBot = await Bot.findOneAndUpdate(
{
_id: bot._id
},
{
isActive
},
{
new: true
}
);
if (!updatedBot) throw new Error("Failed to update bot active state");
return res.status(200).send({
bot
});
};

@ -0,0 +1,43 @@
import * as authController from "./authController";
import * as universalAuthController from "./universalAuthController";
import * as botController from "./botController";
import * as integrationAuthController from "./integrationAuthController";
import * as integrationController from "./integrationController";
import * as keyController from "./keyController";
import * as membershipController from "./membershipController";
import * as membershipOrgController from "./membershipOrgController";
import * as organizationController from "./organizationController";
import * as passwordController from "./passwordController";
import * as secretController from "./secretController";
import * as serviceTokenController from "./serviceTokenController";
import * as signupController from "./signupController";
import * as userActionController from "./userActionController";
import * as userController from "./userController";
import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
import * as adminController from "./adminController";
export {
authController,
universalAuthController,
botController,
integrationAuthController,
integrationController,
keyController,
membershipController,
membershipOrgController,
organizationController,
passwordController,
secretController,
serviceTokenController,
signupController,
userActionController,
userController,
workspaceController,
secretScanningController,
webhookController,
secretImpsController,
adminController
};

File diff suppressed because it is too large Load Diff

@ -0,0 +1,307 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
import { EventService } from "../../services";
import { eventStartIntegration } from "../../events";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/integration";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Create/initialize an (empty) integration for integration authorization
* @param req
* @param res
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
const {
body: {
isActive,
sourceEnvironment,
secretPath,
app,
path,
appId,
owner,
region,
scope,
targetService,
targetServiceId,
integrationAuthId,
targetEnvironment,
targetEnvironmentId,
metadata
}
} = await validateRequest(reqValidator.CreateIntegrationV1, req);
const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>("workspace")
.select(
"+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
);
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace._id
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Folder path doesn't exist"
});
}
}
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
const integration = await new Integration({
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
scope,
secretPath,
integration: integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId),
metadata
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({
integration
});
};
/**
* Change environment or name of integration with id [integrationId]
* @param req
* @param res
* @returns
*/
export const updateIntegration = async (req: Request, res: Response) => {
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
const {
body: {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
secretPath
},
params: { integrationId }
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integration.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: integration.workspace,
environment
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist"
});
}
}
const updatedIntegration = await Integration.findOneAndUpdate(
{
_id: integration._id
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
secretPath
},
{
new: true
}
);
if (updatedIntegration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: updatedIntegration.workspace,
environment
})
});
}
return res.status(200).send({
integration: updatedIntegration
});
};
/**
* Delete integration with id [integrationId]
* @param req
* @param res
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
const {
params: { integrationId }
} = await validateRequest(reqValidator.DeleteIntegrationV1, req);
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integration.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Integrations
);
const deletedIntegration = await Integration.findOneAndDelete({
_id: integrationId
});
if (!deletedIntegration) throw new Error("Failed to find integration");
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath: integration.secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({
integration
});
};
// Will trigger sync for all integrations within the given env and workspace id
export const manualSync = async (req: Request, res: Response) => {
const {
body: { workspaceId, environment }
} = await validateRequest(reqValidator.ManualSyncV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
syncSecretsToActiveIntegrationsQueue({
workspaceId,
environment
});
res.status(200).send();
};

@ -0,0 +1,101 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { Key } from "../../models";
import { findMembership } from "../../helpers/membership";
import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/key";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
* id [key.userId]
* @param req
* @param res
* @returns
*/
export const uploadKey = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { key }
} = await validateRequest(reqValidator.UploadKeyV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
// validate membership of receiver
const receiverMembership = await findMembership({
user: key.userId,
workspace: workspaceId
});
if (!receiverMembership) {
throw new Error("Failed receiver membership validation for workspace");
}
await new Key({
encryptedKey: key.encryptedKey,
nonce: key.nonce,
sender: req.user._id,
receiver: key.userId,
workspace: workspaceId
}).save();
return res.status(200).send({
message: "Successfully uploaded key to workspace"
});
};
/**
* Return latest (encrypted) copy of workspace key for user
* @param req
* @param res
* @returns
*/
export const getLatestKey = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetLatestKeyV1, req);
// get latest key
const latestKey = await Key.find({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.limit(1)
.populate("sender", "+publicKey");
const resObj: any = {};
if (latestKey.length > 0) {
resObj["latestKey"] = latestKey[0];
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_WORKSPACE_KEY,
metadata: {
keyId: latestKey[0]._id.toString()
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
}
return res.status(200).send(resObj);
};

@ -0,0 +1,286 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
import { EventType, Role } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
import { getSiteURL } from "../../config";
import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/membership";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
/**
* Check that user is a member of workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const validateMembership = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
// validate membership
const membership = await findMembership({
user: req.user._id,
workspace: workspaceId
});
if (!membership) {
throw new Error("Failed to validate membership");
}
return res.status(200).send({
message: "Workspace membership confirmed"
});
};
/**
* Delete membership with id [membershipId]
* @param req
* @param res
* @returns
*/
export const deleteMembership = async (req: Request, res: Response) => {
const {
params: { membershipId }
} = await validateRequest(reqValidator.DeleteMembershipV1, req);
// check if membership to delete exists
const membershipToDelete = await Membership.findOne({
_id: membershipId
}).populate<{ user: IUser }>("user");
if (!membershipToDelete) {
throw new Error("Failed to delete workspace membership that doesn't exist");
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: membershipToDelete.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
// delete workspace membership
const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.REMOVE_WORKSPACE_MEMBER,
metadata: {
userId: membershipToDelete.user._id.toString(),
email: membershipToDelete.user.email
}
},
{
workspaceId: membershipToDelete.workspace
}
);
return res.status(200).send({
deletedMembership
});
};
/**
* Change and return workspace membership role
* @param req
* @param res
* @returns
*/
export const changeMembershipRole = async (req: Request, res: Response) => {
const {
body: { role },
params: { membershipId }
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
// validate target membership
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
"user"
);
if (!membershipToChangeRole) {
throw new Error("Failed to find membership to change role");
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: membershipToChangeRole.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) {
const wsRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: membershipToChangeRole.workspace
});
if (!wsRole) throw BadRequestError({ message: "Role not found" });
const plan = await EELicenseService.getPlan(wsRole.organization);
if (!plan.rbac) return res.status(400).send({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: wsRole
});
return res.status(200).send({
membership
});
}
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
metadata: {
userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email,
oldRole: membershipToChangeRole.role,
newRole: role
}
},
{
workspaceId: membershipToChangeRole.workspace
}
);
return res.status(200).send({
membership
});
};
/**
* Add user with email [email] to workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { email }
} = await validateRequest(InviteUserToWorkspaceV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
const invitee = await User.findOne({
email
}).select("+publicKey");
if (!invitee || !invitee?.publicKey) throw new Error("Failed to validate invitee");
// validate invitee's workspace membership - ensure member isn't
// already a member of the workspace
const inviteeMembership = await Membership.findOne({
user: invitee._id,
workspace: workspaceId
}).populate<{ user: IUser }>("user");
if (inviteeMembership) throw new Error("Failed to add existing member of workspace");
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw new Error("Failed to find workspace");
// validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: workspace.organization,
status: ACCEPTED
});
if (!membershipOrg) throw new Error("Failed to validate invitee's organization membership");
// get latest key
const latestKey = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate("sender", "+publicKey");
// create new workspace membership
await new Membership({
user: invitee._id,
workspace: workspaceId,
role: MEMBER
}).save();
await sendMail({
template: "workspaceInvitation.handlebars",
subjectLine: "Infisical workspace invitation",
recipients: [invitee.email],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: workspace.name,
callback_url: (await getSiteURL()) + "/login"
}
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.ADD_WORKSPACE_MEMBER,
metadata: {
userId: invitee._id.toString(),
email: invitee.email
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
invitee,
latestKey
});
};

@ -0,0 +1,292 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { MembershipOrg, Organization, User } from "../../models";
import { SSOConfig } from "../../ee/models";
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
import { createToken } from "../../helpers/auth";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { EELicenseService } from "../../ee/services";
import { ACCEPTED, AuthTokenType, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
import * as reqValidator from "../../validation/membershipOrg";
import {
getAuthSecret,
getJwtSignupLifetime,
getSiteURL,
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Delete organization membership with id [membershipOrgId] from organization
* @param req
* @param res
* @returns
*/
export const deleteMembershipOrg = async (req: Request, _res: Response) => {
const {
params: { membershipOrgId }
} = await validateRequest(reqValidator.DelOrgMembershipv1, req);
// check if organization membership to delete exists
const membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId
}).populate("user");
if (!membershipOrgToDelete) {
throw new Error("Failed to delete organization membership that doesn't exist");
}
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: membershipOrgToDelete.organization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
// delete organization membership
await deleteMemberFromOrg({
membershipOrgId: membershipOrgToDelete._id.toString()
});
await updateSubscriptionOrgQuantity({
organizationId: membershipOrgToDelete.organization.toString()
});
return membershipOrgToDelete;
};
/**
* Change and return organization membership role
* @param req
* @param res
* @returns
*/
export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// change role for (target) organization membership with id
// [membershipOrgId]
let membershipToChangeRole;
return res.status(200).send({
membershipOrg: membershipToChangeRole
});
};
/**
* Organization invitation step 1: Send email invitation to user with email [email]
* for organization with id [organizationId] containing magic link
* @param req
* @param res
* @returns
*/
export const inviteUserToOrganization = async (req: Request, res: Response) => {
let inviteeMembershipOrg, completeInviteLink;
const {
body: { inviteeEmail, organizationId }
} = await validateRequest(reqValidator.InviteUserToOrgv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Member
);
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
const ssoConfig = await SSOConfig.findOne({
organization: new Types.ObjectId(organizationId)
});
if (ssoConfig && ssoConfig.isActive) {
// case: SAML SSO is enabled for the organization
return res.status(400).send({
message: "Failed to invite member due to SAML SSO configured for organization"
});
}
if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed
if (plan.membersUsed >= plan.memberLimit) {
// case: number of members used exceeds the number of members allowed
return res.status(400).send({
message:
"Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
}
const invitee = await User.findOne({
email: inviteeEmail
}).select("+publicKey");
if (invitee) {
// case: invitee is an existing user
inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: organizationId
});
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
throw new Error("Failed to invite an existing member of the organization");
}
if (!inviteeMembershipOrg) {
await new MembershipOrg({
user: invitee,
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
} else {
// check if invitee has been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail,
organization: organizationId
});
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
}
const organization = await Organization.findOne({ _id: organizationId });
if (organization) {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
organizationId: organization._id
});
await sendMail({
template: "organizationInvitation.handlebars",
subjectLine: "Infisical organization invitation",
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
organizationId: organization._id.toString(),
token,
callback_url: (await getSiteURL()) + "/signupinvite"
}
});
if (!(await getSmtpConfigured())) {
completeInviteLink = `${
siteUrl + "/signupinvite"
}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`;
}
}
await updateSubscriptionOrgQuantity({ organizationId });
return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`,
completeInviteLink
});
};
/**
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
* magic link and issue a temporary signup token for user to complete setting up their account
* @param req
* @param res
* @returns
*/
export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user;
const {
body: { organizationId, email, code }
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
user = await User.findOne({ email }).select("+publicKey");
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) throw new Error("Failed to find any invitations for email");
await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
organizationId: membershipOrg.organization,
token: code
});
if (user && user?.publicKey) {
// case: user has already completed account
// membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED;
await membershipOrg.save();
await updateSubscriptionOrgQuantity({
organizationId
});
return res.status(200).send({
message: "Successfully verified email",
user
});
}
if (!user) {
// initialize user account
user = await new User({
email
}).save();
}
// generate temporary signup token
const token = createToken({
payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
message: "Successfully verified email",
user,
token
});
};

@ -0,0 +1,387 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
IncidentContactOrg,
Membership,
MembershipOrg,
Organization,
Workspace
} from "../../models";
import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/organization";
import { ACCEPTED } from "../../variables";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { OrganizationNotFoundError } from "../../utils/errors";
import { ForbiddenError } from "@casl/ability";
export const getOrganizations = async (req: Request, res: Response) => {
const organizations = (
await MembershipOrg.find({
user: req.user._id,
status: ACCEPTED
}).populate("organization")
).map((m) => m.organization);
return res.status(200).send({
organizations
});
};
/**
* Return organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganization = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgv1, req);
// ensure user has membership
await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
})
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
return res.status(200).send({
organization
});
};
/**
* Return organization memberships for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationMembers = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
const users = await MembershipOrg.find({
organization: organizationId
}).populate("user", "+publicKey");
return res.status(200).send({
users
});
};
/**
* Return workspaces that user is part of in organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
})
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString())
);
const workspaces = (
await Membership.find({
user: req.user._id
}).populate("workspace")
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces
});
};
/**
* Change name of organization with id [organizationId] to [name]
* @param req
* @param res
* @returns
*/
export const changeOrganizationName = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { name }
} = await validateRequest(reqValidator.ChangeOrgNamev1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Settings
);
const organization = await Organization.findOneAndUpdate(
{
_id: organizationId
},
{
name
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully changed organization name",
organization
});
};
/**
* Return incident contacts of organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId
});
return res.status(200).send({
incidentContactsOrg
});
};
/**
* Add and return new incident contact with email [email] for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { email }
} = await validateRequest(reqValidator.CreateOrgIncideContact, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId },
{ email, organization: organizationId },
{ upsert: true, new: true }
);
return res.status(200).send({
incidentContactOrg
});
};
/**
* Delete incident contact with email [email] for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { email }
} = await validateRequest(reqValidator.DelOrgIncideContact, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email,
organization: organizationId
});
return res.status(200).send({
message: "Successfully deleted organization incident contact",
incidentContactOrg
});
};
/**
* Redirect user to billing portal or add card page depending on
* if there is a card on file
* @param req
* @param res
* @returns
*/
export const createOrganizationPortalSession = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`
);
if (pmtMethods.length < 1) {
// case: organization has no payment method on file
// -> redirect to add payment method portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{
success_url: (await getSiteURL()) + "/dashboard",
cancel_url: (await getSiteURL()) + "/dashboard"
}
);
return res.status(200).send({ url });
} else {
// case: organization has payment method on file
// -> redirect to billing portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/billing-portal`,
{
return_url: (await getSiteURL()) + "/dashboard"
}
);
return res.status(200).send({ url });
}
};
/**
* Given a org id, return the projects each member of the org belongs to
* @param req
* @param res
* @returns
*/
export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = (
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString());
const memberships = await Membership.find({
workspace: { $in: workspacesSet }
}).populate("workspace");
const userToWorkspaceIds: any = {};
memberships.forEach((membership) => {
const user = membership.user.toString();
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
return res.json(userToWorkspaceIds);
};

@ -0,0 +1,370 @@
import { Request, Response } from "express";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp");
import * as bigintConversion from "bigint-conversion";
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
import { clearTokens, createToken, sendMail } from "../../helpers";
import { TokenService } from "../../services";
import { AuthTokenType, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
import { BadRequestError } from "../../utils/errors";
import {
getAuthSecret,
getHttpsEnabled,
getJwtSignupLifetime,
getSiteURL
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Password reset step 1: Send email verification link to email [email]
* for account recovery.
* @param req
* @param res
* @returns
*/
export const emailPasswordReset = async (req: Request, res: Response) => {
const {
body: { email }
} = await validateRequest(reqValidator.EmailPasswordResetV1, req);
const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) {
// case: user has already completed account
return res.status(200).send({
message: "If an account exists with this email, a password reset link has been sent"
});
}
const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email
});
await sendMail({
template: "passwordReset.handlebars",
subjectLine: "Infisical password reset",
recipients: [email],
substitutions: {
email,
token,
callback_url: (await getSiteURL()) + "/password-reset"
}
});
return res.status(200).send({
message: "If an account exists with this email, a password reset link has been sent"
});
};
/**
* Password reset step 2: Verify email verification link sent to email [email]
* @param req
* @param res
* @returns
*/
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
const {
body: { email, code }
} = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req);
const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or
// hasn't even completed their account
return res.status(403).send({
error: "Failed email verification for password reset"
});
}
await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email,
token: code
});
// generate temporary password-reset token
const token = createToken({
payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
message: "Successfully verified email",
user,
token
});
};
/**
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol
const {
body: { clientPublicKey }
} = await validateRequest(reqValidator.Srp1V1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{ email: req.user.email },
{
email: req.user.email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
* Change account SRP authentication information for user
* Requires verifying [clientProof] as part of step 2 of SRP protocol
* as initiated in POST /srp1
* @param req
* @param res
* @returns
*/
export const changePassword = async (req: Request, res: Response) => {
const {
body: {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}
} = await validateRequest(reqValidator.ChangePasswordV1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email });
if (!loginSRPDetailFromDB) {
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// change password
await User.findByIdAndUpdate(
req.user._id.toString(),
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
{
new: true
}
);
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId);
}
// clear httpOnly cookie
res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean
});
return res.status(200).send({
message: "Successfully changed password"
});
}
return res.status(400).send({
error: "Failed to change password. Try again?"
});
}
);
};
/**
* Create or change backup private key for user
* @param req
* @param res
* @returns
*/
export const createBackupPrivateKey = async (req: Request, res: Response) => {
// create/change backup private key
// requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1
const {
body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email });
if (!loginSRPDetailFromDB) {
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// create new or replace backup private key
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
{ user: req.user._id },
{
user: req.user._id,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
},
{ upsert: true, new: true }
).select("+user, encryptedPrivateKey");
// issue tokens
return res.status(200).send({
message: "Successfully updated backup private key",
backupPrivateKey
});
}
return res.status(400).send({
message: "Failed to update backup private key"
});
}
);
};
/**
* Return backup private key for user
* @param req
* @param res
* @returns
*/
export const getBackupPrivateKey = async (req: Request, res: Response) => {
const backupPrivateKey = await BackupPrivateKey.findOne({
user: req.user._id
}).select("+encryptedPrivateKey +iv +tag");
if (!backupPrivateKey) throw new Error("Failed to find backup private key");
return res.status(200).send({
backupPrivateKey
});
};
export const resetPassword = async (req: Request, res: Response) => {
const {
body: {
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.ResetPasswordV1, req);
await User.findByIdAndUpdate(
req.user._id.toString(),
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully reset password"
});
};

@ -0,0 +1,209 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Key } from "../../models";
import {
pullSecrets as pull,
v1PushSecrets as push,
reformatPullSecrets
} from "../../helpers/secret";
import { pushKeys } from "../../helpers/key";
import { eventPushSecrets } from "../../events";
import { EventService } from "../../services";
import { TelemetryService } from "../../services";
interface PushSecret {
ciphertextKey: string;
ivKey: string;
tagKey: string;
hashKey: string;
ciphertextValue: string;
ivValue: string;
tagValue: string;
hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: "shared" | "personal";
}
/**
* Upload (encrypted) secrets to workspace with id [workspaceId]
* for environment [environment]
* @param req
* @param res
* @returns
*/
export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment");
}
// sanitize secrets
secrets = secrets.filter((s: PushSecret) => s.ciphertextKey !== "" && s.ciphertextValue !== "");
await push({
userId: req.user._id,
workspaceId,
environment,
secrets
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: "secrets pushed",
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : "cli"
}
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath: "/"
})
});
return res.status(200).send({
message: "Successfully uploaded workspace secrets"
});
};
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment] and (encrypted) workspace key
* @param req
* @param res
* @returns
*/
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment");
}
secrets = await pull({
userId: req.user._id.toString(),
workspaceId,
environment,
channel: channel ? channel : "cli",
ipAddress: req.realIP
});
const key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate("sender", "+publicKey");
if (channel !== "cli") {
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: "secrets pulled",
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : "cli"
}
});
}
return res.status(200).send({
secrets,
key
});
};
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment] and (encrypted) workspace key
* via service token
* @param req
* @param res
* @returns
*/
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment");
}
const secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment,
channel: "cli",
ipAddress: req.realIP
});
const key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
if (postHogClient) {
// capture secrets pulled event in production
postHogClient.capture({
distinctId: req.serviceToken.user.email,
event: "secrets pulled",
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : "cli"
}
});
}
return res.status(200).send({
secrets: reformatPullSecrets({ secrets }),
key
});
};

@ -0,0 +1,721 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import {
BadRequestError,
ResourceNotFoundError,
UnauthorizedRequestError
} from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretImports";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
export const createSecretImp = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create secret import'
#swagger.description = 'Create secret import'
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of workspace where to create secret import",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to create secret import",
"example": "dev"
},
"directory": {
"type": "string",
"description": "Path where to create secret import like / or /foo/bar. Default is /",
"example": "/foo/bar"
},
"secretImport": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"description": "Slug of environment to import from",
"example": "development"
},
"secretPath": {
"type": "string",
"description": "Path where to import from like / or /foo/bar.",
"example": "/user/oauth"
}
}
}
},
"required": ["workspaceId", "environment", "directory", "secretImport"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully created secret import"
}
},
"description": "Confirmation of secret import creation"
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Secret import already exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
#swagger.responses[404] = {
description: "Resource Not Found. For example, 'Failed to find folder'"
}
*/
const {
body: { workspaceId, environment, directory, secretImport }
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/")
throw ResourceNotFoundError({ message: "Failed to find folder" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
const doc = new SecretImport({
workspace: workspaceId,
environment,
folderId,
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
});
await doc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: doc._id.toString(),
folderId: doc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath: directory
}
},
{
workspaceId: doc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
}
const doesImportExist = importSecDoc.imports.find(
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
);
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath
});
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath: directory
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
/**
* Update secret import
* @param req
* @param res
* @returns
*/
export const updateSecretImport = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update secret import'
#swagger.description = 'Update secret import'
#swagger.parameters['id'] = {
in: 'path',
description: 'ID of secret import to update',
required: true,
type: 'string',
example: 'import12345'
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImports": {
"type": "array",
"description": "List of secret imports to update to",
"items": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"description": "Slug of environment to import from",
"example": "dev"
},
"secretPath": {
"type": "string",
"description": "Path where to import secrets from like / or /foo/bar",
"example": "/foo/bar"
}
},
"required": ["environment", "secretPath"]
}
}
},
"required": ["secretImports"]
}
}
}
}
#swagger.responses[200] = {
description: 'Successfully updated the secret import',
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully updated secret import"
}
}
}
}
}
}
#swagger.responses[400] = {
description: 'Bad Request - Import not found',
}
#swagger.responses[403] = {
description: 'Forbidden access due to insufficient permissions',
}
#swagger.responses[401] = {
description: 'Unauthorized access due to invalid token or scope',
}
*/
const {
body: { secretImports },
params: { id }
} = await validateRequest(reqValidator.UpdateSecretImportV1, req);
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// token permission check
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// non token entry check
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
const orderBefore = importSecDoc.imports;
importSecDoc.imports = secretImports;
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
importToEnvironment: importSecDoc.environment,
importToSecretPath: secretPath,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
orderBefore,
orderAfter: secretImports
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully updated secret import" });
};
/**
* Delete secret import
* @param req
* @param res
* @returns
*/
export const deleteSecretImport = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete secret import'
#swagger.description = 'Delete secret import'
#swagger.parameters['id'] = {
in: 'path',
description: 'ID of parent secret import document from which to delete secret import',
required: true,
type: 'string',
example: '12345abcde'
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImportEnv": {
"type": "string",
"description": "Slug of environment of import to delete",
"example": "someWorkspaceId"
},
"secretImportPath": {
"type": "string",
"description": "Path like / or /foo/bar of import to delete",
"example": "production"
}
},
"required": ["id", "secretImportEnv", "secretImportPath"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully delete secret import"
}
},
"description": "Confirmation of secret import deletion"
}
}
}
}
*/
const {
params: { id },
body: { secretImportEnv, secretImportPath }
} = await validateRequest(reqValidator.DeleteSecretImportV1, req);
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImportEnv,
importFromSecretPath: secretImportPath,
importToEnvironment: importSecDoc.environment,
importToSecretPath: secretPath
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully delete secret import" });
};
/**
* Get secret imports
* @param req
* @param res
* @returns
*/
export const getSecretImports = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get secret imports'
#swagger.description = 'Get secret imports'
#swagger.parameters['workspaceId'] = {
in: 'query',
description: 'ID of workspace where to get secret imports from',
required: true,
type: 'string',
example: 'workspace12345'
}
#swagger.parameters['environment'] = {
in: 'query',
description: 'Slug of environment where to get secret imports from',
required: true,
type: 'string',
example: 'production'
}
#swagger.parameters['directory'] = {
in: 'query',
description: 'Path where to get secret imports from like / or /foo/bar. Default is /',
required: false,
type: 'string',
example: 'folder12345'
}
#swagger.responses[200] = {
description: 'Successfully retrieved secret import',
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImport": {
$ref: '#/definitions/SecretImport'
}
}
}
}
}
}
#swagger.responses[403] = {
description: 'Forbidden access due to insufficient permissions',
}
#swagger.responses[401] = {
description: 'Unauthorized access due to invalid token or scope',
}
*/
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
return res.status(200).json({ secretImport: importSecDoc });
};
/**
* Get all secret imports
* @param req
* @param res
* @returns
*/
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: [] });
}
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment,
secretImportId: importSecDoc._id.toString(),
folderId,
numberOfImports: importSecDoc.imports.length
}
},
{
workspaceId: importSecDoc.workspace
}
);
const secrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).json({ secrets });
};

@ -0,0 +1,193 @@
import { Request, Response } from "express";
import {
GitAppInstallationSession,
GitAppOrganizationInstallation,
GitRisks
} from "../../ee/models";
import crypto from "crypto";
import { Types } from "mongoose";
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import {
STATUS_RESOLVED_FALSE_POSITIVE,
STATUS_RESOLVED_NOT_REVOKED,
STATUS_RESOLVED_REVOKED
} from "../../ee/models/gitRisks";
import { ProbotOctokit } from "probot";
import { Organization } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretScanning";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex");
const {
params: { organizationId }
} = await validateRequest(reqValidator.CreateInstalLSessionv1, req);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.SecretScanning
);
await GitAppInstallationSession.findByIdAndUpdate(
organization,
{
organization: organization.id,
sessionId: sessionId,
user: new Types.ObjectId(req.user._id)
},
{ upsert: true }
).lean();
res.send({
sessionId: sessionId
});
};
export const linkInstallationToOrganization = async (req: Request, res: Response) => {
const {
body: { sessionId, installationId }
} = await validateRequest(reqValidator.LinkInstallationToOrgv1, req);
const installationSession = await GitAppInstallationSession.findOneAndDelete({
sessionId: sessionId
});
if (!installationSession) {
throw UnauthorizedRequestError();
}
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: installationSession.organization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate(
{
organizationId: installationSession.organization
},
{
installationId: installationId,
organizationId: installationSession.organization,
user: installationSession.user
},
{
upsert: true
}
).lean();
const octokit = new ProbotOctokit({
auth: {
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
installationId: installationId.toString()
}
});
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
for (const repository of repositories) {
scanGithubFullRepoForSecretLeaks({
organizationId: installationSession.organization.toString(),
installationId,
repository: { id: repository.id, fullName: repository.full_name }
});
}
res.json(installationLink);
};
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
const { organizationId } = req.params;
try {
const appInstallation = await GitAppOrganizationInstallation.findOne({
organizationId: organizationId
}).lean();
if (!appInstallation) {
res.json({
appInstallationComplete: false
});
}
res.json({
appInstallationComplete: true
});
} catch {
res.json({
appInstallationComplete: false
});
}
};
export const getRisksForOrganization = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgRisksv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.SecretScanning
);
const risks = await GitRisks.find({ organization: organizationId })
.sort({ createdAt: -1 })
.lean();
res.json({
risks: risks
});
};
export const updateRisksStatus = async (req: Request, res: Response) => {
const {
params: { organizationId, riskId },
body: { status }
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const isRiskResolved =
status == STATUS_RESOLVED_FALSE_POSITIVE ||
status == STATUS_RESOLVED_REVOKED ||
status == STATUS_RESOLVED_NOT_REVOKED
? true
: false;
const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status,
isResolved: isRiskResolved
}).lean();
res.json(risk);
};

@ -0,0 +1,680 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { EventType, FolderVersion } from "../../ee/models";
import { EEAuditLogService, EESecretService } from "../../ee/services";
import { isValidScope } from "../../helpers/secrets";
import { validateRequest } from "../../helpers/validation";
import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder";
import {
appendFolder,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
validateFolderName
} from "../../services/FolderService";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders";
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
// verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create folder'
#swagger.description = 'Create folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where to create folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to create folder",
"example": "production"
},
"folderName": {
"type": "string",
"description": "Name of folder to create",
"example": "my_folder"
},
"directory": {
"type": "string",
"description": "Path where to create folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment", "folderName"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folder": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of folder",
"example": "someFolderId"
},
"name": {
"type": "string",
"description": "Name of folder",
"example": "my_folder"
},
"version": {
"type": "number",
"description": "Version of folder",
"example": 1
}
},
"description": "Details of created folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, folderName, directory }
} = await validateRequest(reqValidator.CreateFolderV1, req);
if (!validateFolderName(folderName)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// token check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// user check
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
// space has no folders initialized
if (!folders) {
const folder = new Folder({
workspace: workspaceId,
environment,
nodes: {
id: "root",
name: "root",
version: 1,
children: []
}
});
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: child.id,
folderName,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder: { id: child.id, name: folderName } });
}
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
if (!hasCreated) return res.json({ folder: child });
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: child.id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: child.id,
folderName,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder: child });
};
/**
* Update folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const updateFolderById = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update folder'
#swagger.description = 'Update folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['folderName'] = {
"description": "Name of folder to update",
"required": true,
"type": "string"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of workspace where to update folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to update folder",
"example": "production"
},
"name": {
"type": "string",
"description": "Name of folder to update to",
"example": "updated_folder_name"
},
"directory": {
"type": "string",
"description": "Path where to update folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment", "name"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "Successfully updated folder"
},
"folder": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of updated folder",
"example": "updated_folder_name"
},
"id": {
"type": "string",
"description": "ID of created folder",
"example": "abc123"
}
},
"description": "Details of the updated folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, name, directory },
params: { folderName }
} = await validateRequest(reqValidator.UpdateFolderV1, req);
if (!validateFolderName(name)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const folder = parentFolder.children.find(({ name }) => name === folderName);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
const oldFolderName = folder.name;
parentFolder.version += 1;
folder.name = name;
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
oldFolderName,
newFolderName: name,
folderPath
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({
message: "Successfully updated folder",
folder: { name: folder.name, id: folder.id }
});
};
/**
* Delete folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const deleteFolder = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete folder'
#swagger.description = 'Delete folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['folderName'] = {
"description": "Name of folder to delete",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where to delete folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to delete folder",
"example": "production"
},
"directory": {
"type": "string",
"description": "Path where to delete folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "successfully deleted folders"
},
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of deleted folder",
"example": "abc123"
},
"name": {
"type": "string",
"description": "Name of deleted folder",
"example": "someFolderName"
}
}
},
"description": "List of IDs and names of deleted folders"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
params: { folderName },
body: { environment, workspaceId, directory }
} = await validateRequest(reqValidator.DeleteFolderV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) throw ERR_FOLDER_NOT_FOUND;
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
const deletedFolder = parentFolder.children.splice(index, 1)[0];
parentFolder.version += 1;
const delFolderIds = getAllFolderIds(deletedFolder);
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
});
await folderVersion.save();
if (delFolderIds.length) {
await Secret.deleteMany({
folder: { $in: delFolderIds.map(({ id }) => id) },
workspace: workspaceId,
environment
});
}
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_FOLDER,
metadata: {
environment,
folderId: deletedFolder.id,
folderName: deletedFolder.name,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
};
/**
* Get folders for workspace with id [workspaceId] and environment [environment]
* considering directory/path [directory]
* @param req
* @param res
* @returns
*/
export const getFolders = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get folders'
#swagger.description = 'Get folders'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace where to get folders from",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['environment'] = {
"description": "Slug of environment where to get folders from",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['directory'] = {
"description": "Path where to get fodlers from like / or /foo/bar. Default is /",
"required": false,
"type": "string",
"in": "query"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "someFolderName"
}
}
},
"description": "List of folders"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For instance, 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetFoldersV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
return res.send({ folders: [], dir: [] });
}
const folder = getFolderByPath(folders.nodes, directory);
return res.send({
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
});
};

@ -0,0 +1,75 @@
import { Request, Response } from "express";
import { ServiceToken } from "../../models";
import { createToken } from "../../helpers/auth";
import { getJwtServiceSecret } from "../../config";
/**
* Return service token on request
* @param req
* @param res
* @returns
*/
export const getServiceToken = async (req: Request, res: Response) => {
return res.status(200).send({
serviceToken: req.serviceToken,
});
};
/**
* Create and return a new service token
* @param req
* @param res
* @returns
*/
export const createServiceToken = async (req: Request, res: Response) => {
let token;
try {
const {
name,
workspaceId,
environment,
expiresIn,
publicKey,
encryptedKey,
nonce,
} = req.body;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment");
}
// compute access token expiration date
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
const serviceToken = await new ServiceToken({
name,
user: req.user._id,
workspace: workspaceId,
environment,
expiresAt,
publicKey,
encryptedKey,
nonce,
}).save();
token = createToken({
payload: {
serviceTokenId: serviceToken._id.toString(),
workspaceId,
},
expiresIn: expiresIn,
secret: await getJwtServiceSecret(),
});
} catch (err) {
return res.status(400).send({
message: "Failed to create service token",
});
}
return res.status(200).send({
token,
});
};

@ -0,0 +1,99 @@
import { Request, Response } from "express";
import { AuthMethod, User } from "../../models";
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
import { createToken } from "../../helpers/auth";
import {
getAuthSecret,
getJwtSignupLifetime,
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
import { AuthTokenType } from "../../variables";
/**
* Signup step 1: Initialize account for user under email [email] and send a verification code
* to that email
* @param req
* @param res
* @returns
*/
export const beginEmailSignup = async (req: Request, res: Response) => {
const {
body: { email }
} = await validateRequest(reqValidator.BeginEmailSignUpV1, req);
// validate that email is not disposable
validateUserEmail(email);
const user = await User.findOne({ email }).select("+publicKey");
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: "Failed to send email verification code for complete account"
});
}
// send send verification email
await sendEmailVerification({ email });
return res.status(200).send({
message: `Sent an email verification code to ${email}`
});
};
/**
* Signup step 2: Verify that code [code] was sent to email [email] and issue
* a temporary signup token for user to complete setting up their account
* @param req
* @param res
* @returns
*/
export const verifyEmailSignup = async (req: Request, res: Response) => {
let user;
const {
body: { email, code }
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
// initialize user account
user = await User.findOne({ email }).select("+publicKey");
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: "Failed email verification for complete user"
});
}
// verify email
if (await getSmtpConfigured()) {
await checkEmailVerification({
email,
code
});
}
if (!user) {
user = await new User({
email,
authMethods: [AuthMethod.EMAIL]
}).save();
}
// generate temporary signup token
const token = createToken({
payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
message: "Successfuly verified email",
user,
token
});
};

File diff suppressed because it is too large Load Diff

@ -0,0 +1,56 @@
import { Request, Response } from "express";
import { validateRequest } from "../../helpers/validation";
import { UserAction } from "../../models";
import * as reqValidator from "../../validation/action";
/**
* Add user action [action]
* @param req
* @param res
* @returns
*/
export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id]
const {
body: { action }
} = await validateRequest(reqValidator.AddUserActionV1, req);
const userAction = await UserAction.findOneAndUpdate(
{
user: req.user._id,
action
},
{ user: req.user._id, action },
{
new: true,
upsert: true
}
);
return res.status(200).send({
message: "Successfully recorded user action",
userAction
});
};
/**
* Return user action [action] for user
* @param req
* @param res
* @returns
*/
export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id]
const {
query: { action }
} = await validateRequest(reqValidator.GetUserActionV1, req);
const userAction = await UserAction.findOne({
user: req.user._id,
action
});
return res.status(200).send({
userAction
});
};

@ -0,0 +1,13 @@
import { Request, Response } from "express";
/**
* Return user on request
* @param req
* @param res
* @returns
*/
export const getUser = async (req: Request, res: Response) => {
return res.status(200).send({
user: req.user,
});
};

@ -0,0 +1,268 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
import { Webhook } from "../../models";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/webhooks";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
export const createWebhook = async (req: Request, res: Response) => {
const {
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.CreateWebhookV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Webhooks
);
const webhook = new Webhook({
workspace: workspaceId,
environment,
secretPath,
url: webhookUrl
});
if (webhookSecretKey) {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
webhook.algorithm = ALGORITHM_AES_256_GCM;
webhook.keyEncoding = ENCODING_SCHEME_BASE64;
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: webhookSecretKey,
key: encryptionKey
});
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
webhook.algorithm = ALGORITHM_AES_256_GCM;
webhook.keyEncoding = ENCODING_SCHEME_UTF8;
}
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment,
secretPath,
webhookUrl,
isDisabled: false
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
webhook,
message: "successfully created webhook"
});
};
export const updateWebhook = async (req: Request, res: Response) => {
const {
body: { isDisabled },
params: { webhookId }
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Webhooks
);
if (typeof isDisabled !== undefined) {
webhook.isDisabled = isDisabled;
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_WEBHOOK_STATUS,
metadata: {
webhookId: webhook._id.toString(),
environment: webhook.environment,
secretPath: webhook.secretPath,
webhookUrl: webhook.url,
isDisabled
}
},
{
workspaceId: webhook.workspace
}
);
return res.status(200).send({
webhook,
message: "successfully updated webhook"
});
};
export const deleteWebhook = async (req: Request, res: Response) => {
const {
params: { webhookId }
} = await validateRequest(reqValidator.DeleteWebhookV1, req);
let webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Webhooks
);
webhook = await Webhook.findByIdAndDelete(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment: webhook.environment,
secretPath: webhook.secretPath,
webhookUrl: webhook.url,
isDisabled: webhook.isDisabled
}
},
{
workspaceId: webhook.workspace
}
);
return res.status(200).send({
message: "successfully removed webhook"
});
};
export const testWebhook = async (req: Request, res: Response) => {
const {
params: { webhookId }
} = await validateRequest(reqValidator.TestWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
try {
await triggerWebhookRequest(
webhook,
getWebhookPayload(
"test",
webhook.workspace.toString(),
webhook.environment,
webhook.secretPath
)
);
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "success",
lastRunErrorMessage: null
});
} catch (err) {
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "failed",
lastRunErrorMessage: (err as Error).message
});
return res.status(400).send({
message: "Failed to receive response",
error: (err as Error).message
});
}
return res.status(200).send({
message: "Successfully received response"
});
};
export const listWebhooks = async (req: Request, res: Response) => {
const {
query: { environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.ListWebhooksV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
const optionalFilters: Record<string, string> = {};
if (environment) optionalFilters.environment = environment as string;
if (secretPath) optionalFilters.secretPath = secretPath as string;
const webhooks = await Webhook.find({
workspace: new Types.ObjectId(workspaceId as string),
...optionalFilters
});
return res.status(200).send({
webhooks
});
};

@ -0,0 +1,359 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import {
IUser,
Integration,
IntegrationAuth,
Membership,
Organization,
ServiceToken,
Workspace
} from "../../models";
import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
import { EELicenseService } from "../../ee/services";
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
import { OrganizationNotFoundError } from "../../utils/errors";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
/**
* Return public keys of members of workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
const publicKeys = (
await Membership.find({
workspace: workspaceId
}).populate<{ user: IUser }>("user", "publicKey")
).map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id
};
});
return res.status(200).send({
publicKeys
});
};
/**
* Return memberships for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
const users = await Membership.find({
workspace: workspaceId
}).populate("user", "+publicKey");
return res.status(200).send({
users
});
};
/**
* Return workspaces that user is part of
* @param req
* @param res
* @returns
*/
export const getWorkspaces = async (req: Request, res: Response) => {
const workspaces = (
await Membership.find({
user: req.user._id
}).populate("workspace")
).map((m) => m.workspace);
return res.status(200).send({
workspaces
});
};
/**
* Return workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceV1, req);
const workspace = await Workspace.findOne({
_id: workspaceId
});
return res.status(200).send({
workspace
});
};
/**
* Create new workspace named [workspaceName] under organization with id
* [organizationId] and add user as admin
* @param req
* @param res
* @returns
*/
export const createWorkspace = async (req: Request, res: Response) => {
const {
body: { organizationId, workspaceName }
} = await validateRequest(reqValidator.CreateWorkspaceV1, req);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Workspace
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message:
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
const workspace = await create({
name: workspaceName,
organizationId: new Types.ObjectId(organizationId)
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN]
});
return res.status(200).send({
workspace
});
};
/**
* Delete workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Workspace
);
// delete workspace
const workspace = await deleteWork({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
workspace
});
};
/**
* Change name of workspace with id [workspaceId] to [name]
* @param req
* @param res
* @returns
*/
export const changeWorkspaceName = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { name }
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Workspace
);
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
name
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully changed workspace name",
workspace
});
};
/**
* Return integrations for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const integrations = await Integration.find({
workspace: workspaceId
});
return res.status(200).send({
integrations
});
};
/**
* Return (integration) authorizations for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const authorizations = await IntegrationAuth.find({
workspace: workspaceId
});
return res.status(200).send({
authorizations
});
};
/**
* Return service service tokens for workspace [workspaceId] belonging to user
* @param req
* @param res
* @returns
*/
export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.ServiceTokens
);
// ?? FIX.
const serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId
});
return res.status(200).send({
serviceTokens
});
};

@ -0,0 +1,315 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
const jsrp = require("jsrp");
import { LoginSRPDetail, User } from "../../models";
import { createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { BadRequestError, InternalServerError } from "../../utils/errors";
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
if (!req.headers["user-agent"])
throw InternalServerError({ message: "User-Agent header is required" });
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"));
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getAuthSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
// case: user does not have MFA enabled
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV;
response.protectedKeyTag = user.protectedKeyTag;
}
return res.status(200).send(response);
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
}
);
};
/**
* Send MFA token to email [email]
* @param req
* @param res
*/
export const sendMfaToken = async (req: Request, res: Response) => {
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email: req.user.email
});
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [req.user.email],
substitutions: {
code
}
});
return res.status(200).send({
message: "Successfully sent new MFA code"
});
};
/**
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid
* @param req
* @param res
*/
export const verifyMfaToken = async (req: Request, res: Response) => {
const {
body: { mfaToken }
} = await validateRequest(reqValidator.VerifyMfaTokenV2, req);
await TokenService.validateToken({
type: TOKEN_EMAIL_MFA,
email: req.user.email,
token: mfaToken
});
const user = await User.findOne({
email: req.user.email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
await LoginSRPDetail.deleteOne({ userId: user.id });
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
const resObj: VerifyMfaTokenRes = {
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey as string,
encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string,
tag: user.tag as string
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey;
resObj.protectedKeyIV = user.protectedKeyIV;
resObj.protectedKeyTag = user.protectedKeyTag;
}
return res.status(200).send(resObj);
};

@ -0,0 +1,604 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
Folder,
Integration,
Membership,
Secret,
ServiceToken,
ServiceTokenData,
Workspace
} from "../../models";
import { EventType, SecretVersion } from "../../ee/models";
import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/environments";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { SecretImport } from "../../models";
import { Webhook } from "../../models";
/**
* Create new workspace environment named [environmentName]
* with slug [environmentSlug] under workspace with id
* @param req
* @param res
* @returns
*/
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": [],
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of workspace where to create environment",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "Name of the environment to create",
"example": "development"
},
"environmentSlug": {
"type": "string",
"description": "Slug of environment to create",
"example": "dev-environment"
}
},
"required": ["environmentName", "environmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Sucess message",
"example": "Successfully created environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was created",
"example": "abc123"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of created environment",
"example": "Staging"
},
"slug": {
"type": "string",
"description": "Slug of created environment",
"example": "staging"
}
}
}
},
"description": "Details of the created environment"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug }
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Environments
);
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization);
if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed
if (workspace.environments.length >= plan.environmentLimit) {
// case: number of environments used exceeds the number of environments allowed
return res.status(400).send({
message:
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
});
}
}
if (
!workspace ||
workspace?.environments.find(
({ name, slug }) => slug === environmentSlug || environmentName === name
)
) {
throw new Error("Failed to create workspace environment");
}
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase()
});
await workspace.save();
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_ENVIRONMENT,
metadata: {
name: environmentName,
slug: environmentSlug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully created new environment",
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug
}
});
};
/**
* Swaps the ordering of two environments in the database. This is purely for aesthetic purposes.
* @param req
* @param res
* @returns
*/
export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw BadRequestError({ message: "Couldn't load workspace" });
}
const environmentIndex = workspace.environments.findIndex(
(env) => env.name === environmentName && env.slug === environmentSlug
);
const otherEnvironmentIndex = workspace.environments.findIndex(
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
);
if (environmentIndex === -1 || otherEnvironmentIndex === -1) {
throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" });
}
// swap the order of the environments
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [
workspace.environments[otherEnvironmentIndex],
workspace.environments[environmentIndex]
];
await workspace.save();
return res.status(200).send({
message: "Successfully reordered environments",
workspace: workspaceId
});
};
/**
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
* Old slug [oldEnvironmentSlug] must be provided
* @param req
* @param res
* @returns
*/
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update environment'
#swagger.description = 'Update environment'
#swagger.security = [{
"apiKeyAuth": [],
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of workspace where to update environment",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "Name of environment to update to",
"example": "Staging-Renamed"
},
"environmentSlug": {
"type": "string",
"description": "Slug of environment to update to",
"example": "staging-renamed"
},
"oldEnvironmentSlug": {
"type": "string",
"description": "Current slug of environment",
"example": "staging-old"
}
},
"required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "Successfully update environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was updated",
"example": "abc123"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of updated environment",
"example": "Staging-Renamed"
},
"slug": {
"type": "string",
"description": "Slug of updated environment",
"example": "staging-renamed"
}
}
}
},
"description": "Details of the renamed environment"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug, oldEnvironmentSlug }
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error("Invalid environment given.");
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error("Failed to create workspace environment");
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug && (name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error("Invalid environment given");
}
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === oldEnvironmentSlug);
if (envIndex === -1) {
throw new Error("Invalid environment given");
}
const oldEnvironment = workspace.environments[envIndex];
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{
workspace: workspaceId,
"scopes.environment": oldEnvironmentSlug
},
{ $set: { "scopes.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Folder.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
{ $set: { "imports.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
);
await Webhook.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_ENVIRONMENT,
metadata: {
oldName: oldEnvironment.name,
newName: environmentName,
oldSlug: oldEnvironment.slug,
newSlug: environmentSlug.toLowerCase()
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully update environment",
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug
}
});
};
/**
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete environment'
#swagger.description = 'Delete environment'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of workspace where to delete environment",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentSlug": {
"type": "string",
"description": "Slug of environment to delete",
"example": "dev"
}
},
"required": ["environmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "Successfully deleted environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was deleted",
"example": "abc123"
},
"environment": {
"type": "string",
"description": "Slug of deleted environment",
"example": "dev"
}
},
"description": "Response after deleting an environment from a workspace"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentSlug }
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error("Failed to create workspace environment");
}
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
if (envIndex === -1) {
throw new Error("Invalid environment given");
}
const oldEnvironment = workspace.environments[envIndex];
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
// await ServiceToken.deleteMany({
// workspace: workspaceId,
// environment: environmentSlug,
// });
const result = await ServiceTokenData.updateMany(
{ workspace: workspaceId },
{ $pull: { scopes: { environment: environmentSlug } } }
);
if (result.modifiedCount > 0) {
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
}
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
);
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_ENVIRONMENT,
metadata: {
name: oldEnvironment.name,
slug: oldEnvironment.slug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully deleted environment",
workspace: workspaceId,
environment: environmentSlug
});
};

@ -0,0 +1,25 @@
import * as authController from "./authController";
import * as signupController from "./signupController";
import * as usersController from "./usersController";
import * as organizationsController from "./organizationsController";
import * as workspaceController from "./workspaceController";
import * as serviceTokenDataController from "./serviceTokenDataController";
import * as secretController from "./secretController";
import * as secretsController from "./secretsController";
import * as environmentController from "./environmentController";
import * as tagController from "./tagController";
import * as membershipController from "./membershipController";
export {
authController,
signupController,
usersController,
organizationsController,
workspaceController,
serviceTokenDataController,
secretController,
secretsController,
environmentController,
tagController,
membershipController
};

@ -0,0 +1,107 @@
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { getSiteURL } from "../../config";
import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { sendMail } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
import { IUser, Key, Membership, MembershipOrg, Workspace } from "../../models";
import { BadRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/membership";
import { ACCEPTED, MEMBER } from "../../variables";
export const addUserToWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { members }
} = await validateRequest(reqValidator.AddUserToWorkspaceV2, req);
// check workspace
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw new Error("Failed to find workspace");
// check permission
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
// validate members are part of the organization
const orgMembers = await MembershipOrg.find({
status: ACCEPTED,
_id: { $in: members.map(({ orgMembershipId }) => orgMembershipId) },
organization: workspace.organization
})
.populate<{ user: IUser }>("user")
.select({ _id: 1, user: 1 })
.lean();
if (orgMembers.length !== members.length)
throw BadRequestError({ message: "Org member not found" });
const existingMember = await Membership.find({
workspace: workspaceId,
user: { $in: orgMembers.map(({ user }) => user) }
});
if (existingMember?.length)
throw BadRequestError({ message: "Some users are already part of workspace" });
await Membership.insertMany(
orgMembers.map(({ user }) => ({ user: user._id, workspace: workspaceId, role: MEMBER }))
);
const encKeyGroupedByOrgMemberId = members.reduce<Record<string, (typeof members)[number]>>(
(prev, curr) => ({ ...prev, [curr.orgMembershipId]: curr }),
{}
);
await Key.insertMany(
orgMembers.map(({ user, _id: id }) => ({
encryptedKey: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedKey,
nonce: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedNonce,
sender: req.user._id,
receiver: user._id,
workspace: workspaceId
}))
);
await sendMail({
template: "workspaceInvitation.handlebars",
subjectLine: "Infisical workspace invitation",
recipients: orgMembers.map(({ user }) => user.email),
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: workspace.name,
callback_url: (await getSiteURL()) + "/login"
}
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
metadata: orgMembers.map(({ user }) => ({
userId: user._id.toString(),
email: user.email
}))
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
success: true,
data: orgMembers
});
};

@ -0,0 +1,485 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
IdentityMembershipOrg,
Membership,
MembershipOrg,
Workspace
} from "../../models";
import { Role } from "../../ee/models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import {
createOrganization as create,
deleteOrganization,
updateSubscriptionOrgQuantity
} from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../variables";
import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { EELicenseService } from "../../ee/services";
import { ForbiddenError } from "@casl/ability";
/**
* Return memberships for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organization user memberships'
#swagger.description = 'Return organization user memberships'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#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"
}
}
}
}
}
}
*/
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
const memberships = await MembershipOrg.find({
organization: organizationId
}).populate("user", "+publicKey");
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 user membership'
#swagger.description = 'Update organization user membership'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#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"
}
}
}
}
}
}
*/
const {
params: { organizationId, membershipId },
body: { role }
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Member
);
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) {
const orgRole = await Role.findOne({
slug: role,
isOrgRole: true,
organization: new Types.ObjectId(organizationId)
});
if (!orgRole) throw BadRequestError({ message: "Role not found" });
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.rbac) return res.status(400).send({
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: orgRole
});
return res.status(200).send({
membership
});
}
const membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
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 user membership'
#swagger.description = 'Delete organization user membership'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#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"
}
}
}
}
}
}
*/
const {
params: { organizationId, membershipId }
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
const membershipOrg = await MembershipOrg.findOne({
_id: new Types.ObjectId(membershipId),
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) throw ResourceNotFoundError();
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: membershipOrg.organization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
// delete organization membership
const membership = await deleteMembershipOrg({
membershipOrgId: membershipId
});
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
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"
}
}
}
}
}
}
*/
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString())
);
const workspaces = (
await Membership.find({
user: req.user._id
}).populate("workspace")
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces
});
};
/**
* Create new organization named [organizationName]
* and add user as owner
* @param req
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
const {
body: { name }
} = await validateRequest(reqValidator.CreateOrgv2, req);
// create organization and add user as member
const organization = await create({
email: req.user.email,
name
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization
});
};
/**
* Delete organization with id [organizationId]
* @param req
* @param res
*/
export const deleteOrganizationById = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.DeleteOrgv2, req);
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: new Types.ObjectId(organizationId),
role: ADMIN
});
if (!membershipOrg) throw UnauthorizedRequestError();
const organization = await deleteOrganization({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send({
organization
});
};
/**
* Return list of identity memberships for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationIdentityMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organization identity memberships'
#swagger.description = 'Return organization identity memberships'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string",
"in": "path"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identityMemberships": {
"type": "array",
"items": {
$ref: "#/components/schemas/IdentityMembershipOrg"
},
"description": "Identity memberships of organization"
}
}
}
}
}
}
*/
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgIdentityMembershipsV2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Identity
);
const identityMemberships = await IdentityMembershipOrg.find({
organization: new Types.ObjectId(organizationId)
}).populate("identity customRole");
return res.status(200).send({
identityMemberships
});
}

@ -0,0 +1,419 @@
import { Request, Response } from "express";
import mongoose, { Types } from "mongoose";
import {
CreateSecretRequestBody,
ModifySecretRequestBody,
SanitizedSecretForCreate,
SanitizedSecretModify
} from "../../types/secret";
const { ValidationError } = mongoose.Error;
import {
ValidationError as RouteValidationError,
UnauthorizedRequestError
} from "../../utils/errors";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
SECRET_PERSONAL,
SECRET_SHARED
} from "../../variables";
import { TelemetryService } from "../../services";
import { Secret, User } from "../../models";
import { AccountNotFoundError } from "../../utils/errors";
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params;
const sanitizedSecret: SanitizedSecretForCreate = {
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
secretKeyIV: secretToCreate.secretKeyIV,
secretKeyTag: secretToCreate.secretKeyTag,
secretKeyHash: secretToCreate.secretKeyHash,
secretValueCiphertext: secretToCreate.secretValueCiphertext,
secretValueIV: secretToCreate.secretValueIV,
secretValueTag: secretToCreate.secretValueTag,
secretValueHash: secretToCreate.secretValueHash,
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
secretCommentIV: secretToCreate.secretCommentIV,
secretCommentTag: secretToCreate.secretCommentTag,
secretCommentHash: secretToCreate.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment,
type: secretToCreate.type,
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
};
const secret = await new Secret(sanitizedSecret).save();
if (postHogClient) {
postHogClient.capture({
event: "secrets added",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
workspaceId,
environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secret
});
};
/**
* Create many secrets for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params;
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [];
secretsToCreate.forEach((rawSecret) => {
const safeUpdateFields: SanitizedSecretForCreate = {
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
secretKeyIV: rawSecret.secretKeyIV,
secretKeyTag: rawSecret.secretKeyTag,
secretKeyHash: rawSecret.secretKeyHash,
secretValueCiphertext: rawSecret.secretValueCiphertext,
secretValueIV: rawSecret.secretValueIV,
secretValueTag: rawSecret.secretValueTag,
secretValueHash: rawSecret.secretValueHash,
secretCommentCiphertext: rawSecret.secretCommentCiphertext,
secretCommentIV: rawSecret.secretCommentIV,
secretCommentTag: rawSecret.secretCommentTag,
secretCommentHash: rawSecret.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment,
type: rawSecret.type,
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
};
sanitizedSecretesToCreate.push(safeUpdateFields);
});
const secrets = await Secret.insertMany(sanitizedSecretesToCreate);
if (postHogClient) {
postHogClient.capture({
event: "secrets added",
distinctId: req.user.email,
properties: {
numberOfSecrets: (secretsToCreate ?? []).length,
workspaceId,
environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secrets
});
};
/**
* Delete secrets in workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params;
const secretIdsToDelete: string[] = req.body.secretIds;
const secretIdsUserCanDelete = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const secretsUserCanDeleteSet: Set<string> = new Set(
secretIdsUserCanDelete.map((objectId) => objectId._id.toString())
);
// Filter out IDs that user can delete and then map them to delete operations
const deleteOperationsToPerform = secretIdsToDelete
.filter(secretIdToDelete => {
if (!secretsUserCanDeleteSet.has(secretIdToDelete)) {
throw RouteValidationError({
message: "You cannot delete secrets that you do not have access to"
});
}
return true;
})
.map(secretIdToDelete => ({
deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } }
}));
const numSecretsDeleted = deleteOperationsToPerform.length;
await Secret.bulkWrite(deleteOperationsToPerform);
if (postHogClient) {
postHogClient.capture({
event: "secrets deleted",
distinctId: req.user.email,
properties: {
numberOfSecrets: numSecretsDeleted,
environment: environmentName,
workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send();
};
/**
* Delete secret with id [secretId]
* @param req
* @param res
*/
export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id);
if (postHogClient) {
postHogClient.capture({
event: "secrets deleted",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
workspaceId: req._secret.workspace.toString(),
environment: req._secret.environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secret: req._secret
});
};
/**
* Update secrets for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @returns
*/
export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params;
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const secretIdsUserCanModify = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const secretsUserCanModifySet: Set<string> = new Set(
secretIdsUserCanModify.map((objectId) => objectId._id.toString())
);
const updateOperationsToPerform: any = [];
secretsModificationsRequested.forEach((userModifiedSecret) => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
secretKeyIV: userModifiedSecret.secretKeyIV,
secretKeyTag: userModifiedSecret.secretKeyTag,
secretKeyHash: userModifiedSecret.secretKeyHash,
secretValueCiphertext: userModifiedSecret.secretValueCiphertext,
secretValueIV: userModifiedSecret.secretValueIV,
secretValueTag: userModifiedSecret.secretValueTag,
secretValueHash: userModifiedSecret.secretValueHash,
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
secretCommentIV: userModifiedSecret.secretCommentIV,
secretCommentTag: userModifiedSecret.secretCommentTag,
secretCommentHash: userModifiedSecret.secretCommentHash
};
const updateOperation = {
updateOne: {
filter: { _id: userModifiedSecret._id, workspace: workspaceId },
update: { $inc: { version: 1 }, $set: sanitizedSecret }
}
};
updateOperationsToPerform.push(updateOperation);
} else {
throw UnauthorizedRequestError({
message: "You do not have permission to modify one or more of the requested secrets"
});
}
});
await Secret.bulkWrite(updateOperationsToPerform);
if (postHogClient) {
postHogClient.capture({
event: "secrets modified",
distinctId: req.user.email,
properties: {
numberOfSecrets: (secretsModificationsRequested ?? []).length,
environment: environmentName,
workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.status(200).send();
};
/**
* Update a secret within workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params;
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
secretKeyIV: secretModificationsRequested.secretKeyIV,
secretKeyTag: secretModificationsRequested.secretKeyTag,
secretKeyHash: secretModificationsRequested.secretKeyHash,
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
secretValueIV: secretModificationsRequested.secretValueIV,
secretValueTag: secretModificationsRequested.secretValueTag,
secretValueHash: secretModificationsRequested.secretValueHash,
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
secretCommentIV: secretModificationsRequested.secretCommentIV,
secretCommentTag: secretModificationsRequested.secretCommentTag,
secretCommentHash: secretModificationsRequested.secretCommentHash
};
const singleModificationUpdate = await Secret.updateOne(
{ _id: secretModificationsRequested._id, workspace: workspaceId },
{ $inc: { version: 1 }, $set: sanitizedSecret }
)
.catch((error) => {
if (error instanceof ValidationError) {
throw RouteValidationError({
message: "Unable to apply modifications, please try again",
stack: error.stack
});
}
throw error;
});
if (postHogClient) {
postHogClient.capture({
event: "secrets modified",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
environment: environmentName,
workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.status(200).send(singleModificationUpdate);
};
/**
* Return secrets for workspace with id [workspaceId], environment [environment] and user
* with id [req.user._id]
* @param req
* @param res
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { environment } = req.query;
const { workspaceId } = req.params;
let userId: Types.ObjectId | undefined = undefined; // used for getting personal secrets for user
let userEmail: string | undefined = undefined; // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user;
const user = await User.findById(req.serviceTokenData.user, "email");
if (!user) throw AccountNotFoundError();
userEmail = user.email;
}
const secrets = await Secret.find({
workspace: workspaceId,
environment,
$or: [{ user: userId }, { user: { $exists: false } }],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
})
.catch((err) => {
throw RouteValidationError({
message: "Failed to get secrets, please try again",
stack: err.stack
});
})
if (postHogClient) {
postHogClient.capture({
event: "secrets pulled",
distinctId: userEmail,
properties: {
numberOfSecrets: (secrets ?? []).length,
environment,
workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.json(secrets);
};
/**
* Return secret with id [secretId]
* @param req
* @param res
* @returns
*/
export const getSecret = async (req: Request, res: Response) => {
// if (postHogClient) {
// postHogClient.capture({
// event: 'secrets pulled',
// distinctId: req.user.email,
// properties: {
// numberOfSecrets: 1,
// workspaceId: req._secret.workspace.toString(),
// environment: req._secret.environment,
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
// userAgent: req.headers?.['user-agent']
// }
// });
// }
return res.status(200).send({
secret: req._secret
});
};

File diff suppressed because it is too large Load Diff

@ -0,0 +1,193 @@
import { Request, Response } from "express";
import crypto from "crypto";
import bcrypt from "bcrypt";
import { ServiceTokenData } from "../../models";
import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { ActorType, EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/serviceTokenData";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { Types } from "mongoose";
/**
* Return service token data associated with service token on request
* @param req
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"serviceTokenData": {
"type": "object",
$ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token"
}
}
}
}
}
}
*/
if (!(req.authData.authPayload instanceof ServiceTokenData))
throw BadRequestError({
message: "Failed accepted client validation for service token data"
});
const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
.select("+encryptedKey +iv +tag")
.populate("user")
.lean();
return res.status(200).json(serviceTokenData);
};
/**
* Create new service token data for workspace with id [workspaceId] and
* environment [environment].
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
const {
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv }
} = await validateRequest(reqValidator.CreateServiceTokenV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
);
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
user,
scopes,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error("Failed to find service token data");
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SERVICE_TOKEN,
metadata: {
name,
scopes
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
serviceToken,
serviceTokenData
});
};
/**
* Delete service token data with id [serviceTokenDataId].
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const {
params: { serviceTokenDataId }
} = await validateRequest(reqValidator.DeleteServiceTokenV2, req);
let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens
);
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData)
return res.status(200).send({
message: "Failed to delete service token"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SERVICE_TOKEN,
metadata: {
name: serviceTokenData.name,
scopes: serviceTokenData?.scopes
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
};

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