mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
146 Commits
org-based-
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
2eb9592b1a | ||
|
bbd9fa4a56 | ||
|
318ad25c11 | ||
|
c372eb7d20 | ||
|
68a99a0b32 | ||
|
7512231e20 | ||
|
f0e580d68b | ||
|
116015d3cf | ||
|
308ff50197 | ||
|
9df5cbbe85 | ||
|
a714a64bc2 | ||
|
ea18d99793 | ||
|
7c098529f7 | ||
|
e20c623e91 | ||
|
3260932741 | ||
|
f0e73474b7 | ||
|
7db829b0b5 | ||
|
ccaa9fd96e | ||
|
b4db06c763 | ||
|
3ebd2fdc6d | ||
|
8d06a6c969 | ||
|
2996efe9d5 | ||
|
43879f6813 | ||
|
72d4490ee7 | ||
|
2336a7265b | ||
|
d428fd055b | ||
|
e4b89371f0 | ||
|
6f9b30b46e | ||
|
35d589a15f | ||
|
8d77f2d8f3 | ||
|
7070a69711 | ||
|
7a65f8c837 | ||
|
678306b350 | ||
|
8864c811fe | ||
|
79206efcd0 | ||
|
06d30fc10f | ||
|
abd28d9269 | ||
|
c6c64b5499 | ||
|
5481b84a94 | ||
|
ab878e00c9 | ||
|
6773996d40 | ||
|
2e20b38bce | ||
|
bccbedfc31 | ||
|
0ab811194d | ||
|
7b54109168 | ||
|
2d088a865f | ||
|
0a8ec6b9da | ||
|
01b29c3917 | ||
|
5439ddeadf | ||
|
9d17d5277b | ||
|
c70fc7826a | ||
|
9ed2bb38c3 | ||
|
f458cf0d40 | ||
|
ce3dc86f78 | ||
|
d1927cb9cf | ||
|
e80426f72e | ||
|
f8a8ea2118 | ||
|
f5cd68168b | ||
|
1a0a9a7402 | ||
|
b74ce14d80 | ||
|
afdc5e8531 | ||
|
b84579b866 | ||
|
4f3cf046fa | ||
|
c71af00146 | ||
|
793440feb6 | ||
|
b24d748462 | ||
|
4c49119ac5 | ||
|
90f09c7a78 | ||
|
00876f788c | ||
|
f09c48d79b | ||
|
57daeb71e6 | ||
|
98b5f713a5 | ||
|
120d7e42bf | ||
|
c2bd259c12 | ||
|
242d770098 | ||
|
1855fc769d | ||
|
217fef65e8 | ||
|
e15ed4cc58 | ||
|
8a0fd62785 | ||
|
c69601c14e | ||
|
faf6323a58 | ||
|
c73ee49425 | ||
|
b82d1b6a5d | ||
|
3dcda44c50 | ||
|
f320b08ca8 | ||
|
df6e5674cf | ||
|
6bac143a8e | ||
|
38b93e499f | ||
|
a521538010 | ||
|
8cc2553452 | ||
|
b1cb9de001 | ||
|
036256b350 | ||
|
d3a06b82e6 | ||
|
87436cfb57 | ||
|
5c58a4d1a3 | ||
|
03a91b2c59 | ||
|
751361bd54 | ||
|
b4b88daf36 | ||
|
6546740bd9 | ||
|
b32558c66f | ||
|
effd30857e | ||
|
60998c8944 | ||
|
3c4d9fd4a9 | ||
|
ad70c783e8 | ||
|
7347362738 | ||
|
4b7f2e808b | ||
|
57f9d13189 | ||
|
bd2e8ac922 | ||
|
79694750af | ||
|
03db367a4e | ||
|
b0fb848a92 | ||
|
3a7b697549 | ||
|
4fdfcd50dc | ||
|
db205b855a | ||
|
e707f0d235 | ||
|
27f4225c44 | ||
|
28a9d8e739 | ||
|
a1321e4749 | ||
|
d4db01bbde | ||
|
39634b8aae | ||
|
4815ff13ee | ||
|
fb503756d9 | ||
|
48a97fb39d | ||
|
eeaee4409c | ||
|
8d457bb0bf | ||
|
d8ea26feb7 | ||
|
50c0fae557 | ||
|
d5064fe75a | ||
|
70e083bae0 | ||
|
6a943e275a | ||
|
526dc6141b | ||
|
dcab9dcdda | ||
|
1b0591def8 | ||
|
4b4305bddc | ||
|
fcaff76afa | ||
|
ae9eb20189 | ||
|
3905d16a7c | ||
|
ecafdb0d01 | ||
|
8313245ae1 | ||
|
dc146d0883 | ||
|
24dd79b566 | ||
|
00650df501 | ||
|
44f087991c | ||
|
6ff5fb69d4 | ||
|
9fe2021d9f | ||
|
fe2f2f972e |
12
.env.example
12
.env.example
@@ -3,16 +3,18 @@
|
|||||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||||
|
|
||||||
|
# Required
|
||||||
|
DB_CONNECTION_URI=postgres://infisical:infisical@db:5432/infisical
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Required secrets to sign JWT tokens
|
# Required secrets to sign JWT tokens
|
||||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
||||||
|
|
||||||
# MongoDB
|
# Postgres creds
|
||||||
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
|
POSTGRES_PASSWORD=infisical
|
||||||
# to the MongoDB container instance or Mongo Cloud
|
POSTGRES_USER=infisical
|
||||||
# Required
|
POSTGRES_DB=infisical
|
||||||
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
1
.env.migration.example
Normal file
1
.env.migration.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DB_CONNECTION_URI=
|
4
.env.test.example
Normal file
4
.env.test.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
DB_CONNECTION_URI=postgres://infisical:infisical@localhost/infisical?sslmode=disable
|
||||||
|
AUTH_SECRET=4bnfe4e407b8921c104518903515b218
|
||||||
|
ENCRYPTION_KEY=4bnfe4e407b8921c104518903515b218
|
75
.github/workflows/check-api-for-breaking-changes.yml
vendored
Normal file
75
.github/workflows/check-api-for-breaking-changes.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
name: "Check API For Breaking Changes"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
paths:
|
||||||
|
- "backend/src/server/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
|
43
.github/workflows/check-be-pull-request.yml
vendored
43
.github/workflows/check-be-pull-request.yml
vendored
@@ -1,43 +0,0 @@
|
|||||||
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
|
|
35
.github/workflows/check-be-ts-and-lint.yml
vendored
Normal file
35
.github/workflows/check-be-ts-and-lint.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
@@ -5,9 +5,14 @@ on:
|
|||||||
- "infisical/v*.*.*-postgres"
|
- "infisical/v*.*.*-postgres"
|
||||||
|
|
||||||
jobs:
|
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:
|
infisical-standalone:
|
||||||
name: Build infisical standalone image postgres
|
name: Build infisical standalone image postgres
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: [infisical-tests]
|
||||||
steps:
|
steps:
|
||||||
- name: Extract version from tag
|
- name: Extract version from tag
|
||||||
id: extract_version
|
id: extract_version
|
||||||
|
47
.github/workflows/run-backend-tests.yml
vendored
Normal file
47
.github/workflows/run-backend-tests.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,7 @@ node_modules
|
|||||||
.env.gamma
|
.env.gamma
|
||||||
.env.prod
|
.env.prod
|
||||||
.env.infisical
|
.env.infisical
|
||||||
|
.env.migration
|
||||||
*~
|
*~
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
10
Makefile
10
Makefile
@@ -5,16 +5,10 @@ push:
|
|||||||
docker-compose -f docker-compose.yml push
|
docker-compose -f docker-compose.yml push
|
||||||
|
|
||||||
up-dev:
|
up-dev:
|
||||||
docker-compose -f docker-compose.dev.yml up --build
|
docker compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
up-pg-dev:
|
|
||||||
docker compose -f docker-compose.pg.yml up --build
|
|
||||||
|
|
||||||
i-dev:
|
|
||||||
infisical run -- docker-compose -f docker-compose.dev.yml up --build
|
|
||||||
|
|
||||||
up-prod:
|
up-prod:
|
||||||
docker-compose -f docker-compose.yml up --build
|
docker-compose -f docker-compose.prod.yml up --build
|
||||||
|
|
||||||
down:
|
down:
|
||||||
docker-compose down
|
docker-compose down
|
||||||
|
@@ -84,13 +84,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
|
|||||||
Linux/macOS:
|
Linux/macOS:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.yml up
|
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.prod.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows Command Prompt:
|
Windows Command Prompt:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.yml up
|
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.prod.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
Create an account at `http://localhost:80`
|
Create an account at `http://localhost:80`
|
||||||
|
@@ -1,2 +1,3 @@
|
|||||||
vitest-environment-infisical.ts
|
vitest-environment-infisical.ts
|
||||||
vitest.config.ts
|
vitest.config.ts
|
||||||
|
vitest.e2e.config.ts
|
||||||
|
@@ -21,6 +21,18 @@ module.exports = {
|
|||||||
tsconfigRootDir: __dirname
|
tsconfigRootDir: __dirname
|
||||||
},
|
},
|
||||||
root: true,
|
root: true,
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["./e2e-test/**/*"],
|
||||||
|
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: {
|
rules: {
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
||||||
|
71
backend/e2e-test/routes/v1/identity.spec.ts
Normal file
71
backend/e2e-test/routes/v1/identity.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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,6 +1,7 @@
|
|||||||
import { seedData1 } from "@app/db/seed-data";
|
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
|
|
||||||
|
import { seedData1 } from "@app/db/seed-data";
|
||||||
|
|
||||||
describe("Login V1 Router", async () => {
|
describe("Login V1 Router", async () => {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
@@ -1,6 +1,40 @@
|
|||||||
import { seedData1 } from "@app/db/seed-data";
|
import { seedData1 } from "@app/db/seed-data";
|
||||||
import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project";
|
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 () => {
|
describe("Project Environment Router", async () => {
|
||||||
test("Get default environments", async () => {
|
test("Get default environments", async () => {
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
@@ -31,24 +65,10 @@ describe("Project Environment Router", async () => {
|
|||||||
expect(payload.workspace.environments.length).toBe(3);
|
expect(payload.workspace.environments.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockProjectEnv = { name: "temp", slug: "temp", id: "" }; // id will be filled in create op
|
const mockProjectEnv = { name: "temp", slug: "temp" }; // id will be filled in create op
|
||||||
test("Create environment", async () => {
|
test("Create environment", async () => {
|
||||||
const res = await testServer.inject({
|
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||||
method: "POST",
|
expect(newEnvironment).toEqual(
|
||||||
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
|
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${jwtAuthToken}`
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
name: mockProjectEnv.name,
|
|
||||||
slug: mockProjectEnv.slug
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const payload = JSON.parse(res.payload);
|
|
||||||
expect(payload).toHaveProperty("environment");
|
|
||||||
expect(payload.environment).toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: mockProjectEnv.name,
|
name: mockProjectEnv.name,
|
||||||
@@ -59,14 +79,15 @@ describe("Project Environment Router", async () => {
|
|||||||
updatedAt: expect.any(String)
|
updatedAt: expect.any(String)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
mockProjectEnv.id = payload.environment.id;
|
await deleteProjectEnvironment(newEnvironment.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update environment", async () => {
|
test("Update environment", async () => {
|
||||||
|
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||||
const updatedName = { name: "temp#2", slug: "temp2" };
|
const updatedName = { name: "temp#2", slug: "temp2" };
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`,
|
url: `/api/v1/workspace/${seedData1.project.id}/environments/${newEnvironment.id}`,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${jwtAuthToken}`
|
authorization: `Bearer ${jwtAuthToken}`
|
||||||
},
|
},
|
||||||
@@ -82,7 +103,7 @@ describe("Project Environment Router", async () => {
|
|||||||
expect(payload).toHaveProperty("environment");
|
expect(payload).toHaveProperty("environment");
|
||||||
expect(payload.environment).toEqual(
|
expect(payload.environment).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: newEnvironment.id,
|
||||||
name: updatedName.name,
|
name: updatedName.name,
|
||||||
slug: updatedName.slug,
|
slug: updatedName.slug,
|
||||||
projectId: seedData1.project.id,
|
projectId: seedData1.project.id,
|
||||||
@@ -91,61 +112,21 @@ describe("Project Environment Router", async () => {
|
|||||||
updatedAt: expect.any(String)
|
updatedAt: expect.any(String)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
mockProjectEnv.name = updatedName.name;
|
await deleteProjectEnvironment(newEnvironment.id);
|
||||||
mockProjectEnv.slug = updatedName.slug;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Delete environment", async () => {
|
test("Delete environment", async () => {
|
||||||
const res = await testServer.inject({
|
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||||
method: "DELETE",
|
const deletedProjectEnvironment = await deleteProjectEnvironment(newEnvironment.id);
|
||||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`,
|
expect(deletedProjectEnvironment).toEqual(
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${jwtAuthToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const payload = JSON.parse(res.payload);
|
|
||||||
expect(payload).toHaveProperty("environment");
|
|
||||||
expect(payload.environment).toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: deletedProjectEnvironment.id,
|
||||||
name: mockProjectEnv.name,
|
name: mockProjectEnv.name,
|
||||||
slug: mockProjectEnv.slug,
|
slug: mockProjectEnv.slug,
|
||||||
position: 1,
|
position: 4,
|
||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
updatedAt: expect.any(String)
|
updatedAt: expect.any(String)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// after all these opreations the list of environment should be still same
|
|
||||||
test("Default list of environment", 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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,40 @@
|
|||||||
import { seedData1 } from "@app/db/seed-data";
|
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 () => {
|
describe("Secret Folder Router", async () => {
|
||||||
test.each([
|
test.each([
|
||||||
{ name: "folder1", path: "/" }, // one in root
|
{ name: "folder1", path: "/" }, // one in root
|
||||||
@@ -7,30 +42,15 @@ describe("Secret Folder Router", async () => {
|
|||||||
{ name: "folder2", path: "/" },
|
{ name: "folder2", path: "/" },
|
||||||
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
|
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
|
||||||
])("Create folder $name in $path", async ({ name, path }) => {
|
])("Create folder $name in $path", async ({ name, path }) => {
|
||||||
const res = await testServer.inject({
|
const createdFolder = await createFolder({ path, name });
|
||||||
method: "POST",
|
|
||||||
url: `/api/v1/folders`,
|
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${jwtAuthToken}`
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
workspaceId: seedData1.project.id,
|
|
||||||
environment: seedData1.environment.slug,
|
|
||||||
name,
|
|
||||||
path
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
|
||||||
const payload = JSON.parse(res.payload);
|
|
||||||
expect(payload).toHaveProperty("folder");
|
|
||||||
// check for default environments
|
// check for default environments
|
||||||
expect(payload).toEqual({
|
expect(createdFolder).toEqual(
|
||||||
folder: expect.objectContaining({
|
expect.objectContaining({
|
||||||
name,
|
name,
|
||||||
id: expect.any(String)
|
id: expect.any(String)
|
||||||
})
|
})
|
||||||
});
|
);
|
||||||
|
await deleteFolder({ path, id: createdFolder.id });
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
@@ -43,6 +63,8 @@ describe("Secret Folder Router", async () => {
|
|||||||
},
|
},
|
||||||
{ path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } }
|
{ path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } }
|
||||||
])("Get folders $path", async ({ path, expected }) => {
|
])("Get folders $path", async ({ path, expected }) => {
|
||||||
|
const newFolders = await Promise.all(expected.folders.map(({ name }) => createFolder({ name, path })));
|
||||||
|
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/api/v1/folders`,
|
url: `/api/v1/folders`,
|
||||||
@@ -59,36 +81,22 @@ describe("Secret Folder Router", async () => {
|
|||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
const payload = JSON.parse(res.payload);
|
const payload = JSON.parse(res.payload);
|
||||||
expect(payload).toHaveProperty("folders");
|
expect(payload).toHaveProperty("folders");
|
||||||
expect(payload.folders.length).toBe(expected.length);
|
expect(payload.folders.length >= expected.folders.length).toBeTruthy();
|
||||||
expect(payload).toEqual({ folders: expected.folders.map((el) => expect.objectContaining(el)) });
|
expect(payload).toEqual({
|
||||||
});
|
folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
|
||||||
|
|
||||||
let toBeDeleteFolderId = "";
|
|
||||||
test("Update a deep folder", async () => {
|
|
||||||
const res = await testServer.inject({
|
|
||||||
method: "PATCH",
|
|
||||||
url: `/api/v1/folders/folder1`,
|
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${jwtAuthToken}`
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
workspaceId: seedData1.project.id,
|
|
||||||
environment: seedData1.environment.slug,
|
|
||||||
name: "folder-updated",
|
|
||||||
path: "/level1/level2"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id })));
|
||||||
const payload = JSON.parse(res.payload);
|
});
|
||||||
expect(payload).toHaveProperty("folder");
|
|
||||||
expect(payload.folder).toEqual(
|
test("Update a deep folder", async () => {
|
||||||
|
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
|
||||||
|
expect(newFolder).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "folder-updated"
|
name: "folder-updated"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
toBeDeleteFolderId = payload.folder.id;
|
|
||||||
|
|
||||||
const resUpdatedFolders = await testServer.inject({
|
const resUpdatedFolders = await testServer.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -106,14 +114,16 @@ describe("Secret Folder Router", async () => {
|
|||||||
expect(resUpdatedFolders.statusCode).toBe(200);
|
expect(resUpdatedFolders.statusCode).toBe(200);
|
||||||
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
|
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
|
||||||
expect(updatedFolderList).toHaveProperty("folders");
|
expect(updatedFolderList).toHaveProperty("folders");
|
||||||
expect(updatedFolderList.folders.length).toEqual(1);
|
|
||||||
expect(updatedFolderList.folders[0].name).toEqual("folder-updated");
|
expect(updatedFolderList.folders[0].name).toEqual("folder-updated");
|
||||||
|
|
||||||
|
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Delete a deep folder", async () => {
|
test("Delete a deep folder", async () => {
|
||||||
|
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: `/api/v1/folders/${toBeDeleteFolderId}`,
|
url: `/api/v1/folders/${newFolder.id}`,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${jwtAuthToken}`
|
authorization: `Bearer ${jwtAuthToken}`
|
||||||
},
|
},
|
||||||
|
@@ -1,32 +1,57 @@
|
|||||||
import { seedData1 } from "@app/db/seed-data";
|
import { seedData1 } from "@app/db/seed-data";
|
||||||
|
|
||||||
describe("Secret Folder Router", async () => {
|
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([
|
test.each([
|
||||||
{ importEnv: "dev", importPath: "/" }, // one in root
|
{ importEnv: "dev", importPath: "/" }, // one in root
|
||||||
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
||||||
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
||||||
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");
|
|
||||||
// check for default environments
|
// check for default environments
|
||||||
expect(payload.secretImport).toEqual(
|
const payload = await createSecretImport(importPath, importEnv);
|
||||||
|
expect(payload).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
importPath: expect.any(String),
|
importPath: expect.any(String),
|
||||||
@@ -37,10 +62,12 @@ describe("Secret Folder Router", async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
await deleteSecretImport(payload.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
let testSecretImportId = "";
|
|
||||||
test("Get secret imports", async () => {
|
test("Get secret imports", async () => {
|
||||||
|
const createdImport1 = await createSecretImport("/", "dev");
|
||||||
|
const createdImport2 = await createSecretImport("/", "staging");
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/api/v1/secret-imports`,
|
url: `/api/v1/secret-imports`,
|
||||||
@@ -58,7 +85,6 @@ describe("Secret Folder Router", async () => {
|
|||||||
const payload = JSON.parse(res.payload);
|
const payload = JSON.parse(res.payload);
|
||||||
expect(payload).toHaveProperty("secretImports");
|
expect(payload).toHaveProperty("secretImports");
|
||||||
expect(payload.secretImports.length).toBe(2);
|
expect(payload.secretImports.length).toBe(2);
|
||||||
testSecretImportId = payload.secretImports[0].id;
|
|
||||||
expect(payload.secretImports).toEqual(
|
expect(payload.secretImports).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -72,12 +98,20 @@ describe("Secret Folder Router", async () => {
|
|||||||
})
|
})
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
await deleteSecretImport(createdImport1.id);
|
||||||
|
await deleteSecretImport(createdImport2.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Update secret import position", async () => {
|
test("Update secret import position", async () => {
|
||||||
const res = await testServer.inject({
|
const devImportDetails = { path: "/", envSlug: "dev" };
|
||||||
|
const stagingImportDetails = { path: "/", envSlug: "staging" };
|
||||||
|
|
||||||
|
const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug);
|
||||||
|
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
|
||||||
|
|
||||||
|
const updateImportRes = await testServer.inject({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/v1/secret-imports/${testSecretImportId}`,
|
url: `/api/v1/secret-imports/${createdImport1.id}`,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${jwtAuthToken}`
|
authorization: `Bearer ${jwtAuthToken}`
|
||||||
},
|
},
|
||||||
@@ -91,8 +125,8 @@ describe("Secret Folder Router", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
expect(updateImportRes.statusCode).toBe(200);
|
||||||
const payload = JSON.parse(res.payload);
|
const payload = JSON.parse(updateImportRes.payload);
|
||||||
expect(payload).toHaveProperty("secretImport");
|
expect(payload).toHaveProperty("secretImport");
|
||||||
// check for default environments
|
// check for default environments
|
||||||
expect(payload.secretImport).toEqual(
|
expect(payload.secretImport).toEqual(
|
||||||
@@ -102,7 +136,7 @@ describe("Secret Folder Router", async () => {
|
|||||||
position: 2,
|
position: 2,
|
||||||
importEnv: expect.objectContaining({
|
importEnv: expect.objectContaining({
|
||||||
name: expect.any(String),
|
name: expect.any(String),
|
||||||
slug: expect.any(String),
|
slug: expect.stringMatching(devImportDetails.envSlug),
|
||||||
id: expect.any(String)
|
id: expect.any(String)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -124,28 +158,19 @@ describe("Secret Folder Router", async () => {
|
|||||||
expect(secretImportsListRes.statusCode).toBe(200);
|
expect(secretImportsListRes.statusCode).toBe(200);
|
||||||
const secretImportList = JSON.parse(secretImportsListRes.payload);
|
const secretImportList = JSON.parse(secretImportsListRes.payload);
|
||||||
expect(secretImportList).toHaveProperty("secretImports");
|
expect(secretImportList).toHaveProperty("secretImports");
|
||||||
expect(secretImportList.secretImports[1].id).toEqual(testSecretImportId);
|
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 () => {
|
test("Delete secret import position", async () => {
|
||||||
const res = await testServer.inject({
|
const createdImport1 = await createSecretImport("/", "dev");
|
||||||
method: "DELETE",
|
const createdImport2 = await createSecretImport("/", "staging");
|
||||||
url: `/api/v1/secret-imports/${testSecretImportId}`,
|
const deletedImport = await deleteSecretImport(createdImport1.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");
|
|
||||||
// check for default environments
|
// check for default environments
|
||||||
expect(payload.secretImport).toEqual(
|
expect(deletedImport).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
importPath: expect.any(String),
|
importPath: expect.any(String),
|
||||||
@@ -175,5 +200,7 @@ describe("Secret Folder Router", async () => {
|
|||||||
expect(secretImportList).toHaveProperty("secretImports");
|
expect(secretImportList).toHaveProperty("secretImports");
|
||||||
expect(secretImportList.secretImports.length).toEqual(1);
|
expect(secretImportList.secretImports.length).toEqual(1);
|
||||||
expect(secretImportList.secretImports[0].position).toEqual(1);
|
expect(secretImportList.secretImports[0].position).toEqual(1);
|
||||||
|
|
||||||
|
await deleteSecretImport(createdImport2.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
579
backend/e2e-test/routes/v2/service-token.spec.ts
Normal file
579
backend/e2e-test/routes/v2/service-token.spec.ts
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
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,26 +1,30 @@
|
|||||||
// import { main } from "@app/server/app";
|
// eslint-disable-next-line
|
||||||
import { initEnvConfig } from "@app/lib/config/env";
|
import "ts-node/register";
|
||||||
|
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { mockSmtpServer } from "./mocks/smtp";
|
|
||||||
import { initLogger } from "@app/lib/logger";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
|
|
||||||
import "ts-node/register";
|
|
||||||
import { main } from "@app/server/app";
|
|
||||||
import { mockQueue } from "./mocks/queue";
|
|
||||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
|
||||||
import { seedData1 } from "@app/db/seed-data";
|
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 { AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
dotenv.config({ path: path.join(__dirname, "../.env.test") });
|
import { mockQueue } from "./mocks/queue";
|
||||||
|
import { mockSmtpServer } from "./mocks/smtp";
|
||||||
|
|
||||||
|
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
|
||||||
export default {
|
export default {
|
||||||
name: "knex-env",
|
name: "knex-env",
|
||||||
transformMode: "ssr",
|
transformMode: "ssr",
|
||||||
async setup() {
|
async setup() {
|
||||||
|
const logger = await initLogger();
|
||||||
|
const cfg = initEnvConfig(logger);
|
||||||
const db = knex({
|
const db = knex({
|
||||||
client: "pg",
|
client: "pg",
|
||||||
connection: process.env.DB_CONNECTION_URI,
|
connection: cfg.DB_CONNECTION_URI,
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: path.join(__dirname, "../src/db/migrations"),
|
directory: path.join(__dirname, "../src/db/migrations"),
|
||||||
extension: "ts",
|
extension: "ts",
|
||||||
@@ -37,8 +41,6 @@ export default {
|
|||||||
await db.seed.run();
|
await db.seed.run();
|
||||||
const smtp = mockSmtpServer();
|
const smtp = mockSmtpServer();
|
||||||
const queue = mockQueue();
|
const queue = mockQueue();
|
||||||
const logger = await initLogger();
|
|
||||||
const cfg = initEnvConfig(logger);
|
|
||||||
const server = await main({ db, smtp, logger, queue });
|
const server = await main({ db, smtp, logger, queue });
|
||||||
// @ts-expect-error type
|
// @ts-expect-error type
|
||||||
globalThis.testServer = server;
|
globalThis.testServer = server;
|
||||||
|
2177
backend/package-lock.json
generated
2177
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,8 @@
|
|||||||
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||||
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||||
"seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest && npm run seed:run"
|
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -67,10 +67,10 @@
|
|||||||
"tsx": "^4.4.0",
|
"tsx": "^4.4.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vite-tsconfig-paths": "^4.2.2",
|
"vite-tsconfig-paths": "^4.2.2",
|
||||||
"vitest": "^1.0.4"
|
"vitest": "^1.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-secrets-manager": "^3.485.0",
|
"@aws-sdk/client-secrets-manager": "^3.502.0",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@fastify/cookie": "^9.2.0",
|
"@fastify/cookie": "^9.2.0",
|
||||||
"@fastify/cors": "^8.4.1",
|
"@fastify/cors": "^8.4.1",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.12.0",
|
"@fastify/swagger": "^8.12.0",
|
||||||
"@fastify/swagger-ui": "^1.10.1",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@octokit/rest": "^20.0.2",
|
"@octokit/rest": "^20.0.2",
|
||||||
"@octokit/webhooks-types": "^7.3.1",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
@@ -90,8 +90,8 @@
|
|||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"argon2": "^0.31.2",
|
"argon2": "^0.31.2",
|
||||||
"aws-sdk": "^2.1532.0",
|
"aws-sdk": "^2.1545.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.4",
|
||||||
"axios-retry": "^4.0.0",
|
"axios-retry": "^4.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.1.1",
|
"bullmq": "^5.1.1",
|
||||||
@@ -109,7 +109,8 @@
|
|||||||
"mysql2": "^3.6.5",
|
"mysql2": "^3.6.5",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.9.7",
|
"nodemailer": "^6.9.9",
|
||||||
|
"ora": "^7.0.1",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
@@ -117,7 +118,7 @@
|
|||||||
"picomatch": "^3.0.1",
|
"picomatch": "^3.0.1",
|
||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"posthog-node": "^3.6.0",
|
"posthog-node": "^3.6.0",
|
||||||
"probot": "^12.3.3",
|
"probot": "^13.0.0",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"tweetnacl-util": "^0.15.1",
|
"tweetnacl-util": "^0.15.1",
|
||||||
@@ -125,4 +126,4 @@
|
|||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zod-to-json-schema": "^3.22.0"
|
"zod-to-json-schema": "^3.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,11 +7,10 @@ import promptSync from "prompt-sync";
|
|||||||
const prompt = promptSync({ sigint: true });
|
const prompt = promptSync({ sigint: true });
|
||||||
|
|
||||||
const migrationName = prompt("Enter name for seedfile: ");
|
const migrationName = prompt("Enter name for seedfile: ");
|
||||||
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1;
|
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seeds")).length || 1;
|
||||||
execSync(
|
execSync(
|
||||||
`npx knex seed:make --knexfile ${path.join(
|
`npx knex seed:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${
|
||||||
__dirname,
|
fileCounter + 1
|
||||||
"../src/db/knexfile.ts"
|
}-${migrationName}`,
|
||||||
)} -x ts ${fileCounter}-${migrationName}`,
|
|
||||||
{ stdio: "inherit" }
|
{ stdio: "inherit" }
|
||||||
);
|
);
|
||||||
|
@@ -3,13 +3,9 @@ import dotenv from "dotenv";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
import { writeFileSync } from "fs";
|
import { writeFileSync } from "fs";
|
||||||
import promptSync from "prompt-sync";
|
|
||||||
|
|
||||||
const prompt = promptSync({ sigint: true });
|
|
||||||
|
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: path.join(__dirname, "../.env"),
|
path: path.join(__dirname, "../../.env.migration")
|
||||||
debug: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const db = knex({
|
const db = knex({
|
||||||
@@ -94,17 +90,7 @@ const main = async () => {
|
|||||||
.orderBy("table_name")
|
.orderBy("table_name")
|
||||||
).filter((el) => !el.tableName.includes("_migrations"));
|
).filter((el) => !el.tableName.includes("_migrations"));
|
||||||
|
|
||||||
console.log("Select a table to generate schema");
|
|
||||||
console.table(tables);
|
|
||||||
console.log("all: all tables");
|
|
||||||
const selectedTables = prompt("Type table numbers comma seperated: ");
|
|
||||||
const tableNumbers =
|
|
||||||
selectedTables !== "all" ? selectedTables.split(",").map((el) => Number(el)) : [];
|
|
||||||
|
|
||||||
for (let i = 0; i < tables.length; i += 1) {
|
for (let i = 0; i < tables.length; i += 1) {
|
||||||
// skip if not desired table
|
|
||||||
if (selectedTables !== "all" && !tableNumbers.includes(i)) continue;
|
|
||||||
|
|
||||||
const { tableName } = tables[i];
|
const { tableName } = tables[i];
|
||||||
const columns = await db(tableName).columnInfo();
|
const columns = await db(tableName).columnInfo();
|
||||||
const columnNames = Object.keys(columns);
|
const columnNames = Object.keys(columns);
|
||||||
@@ -124,16 +110,16 @@ const main = async () => {
|
|||||||
if (colInfo.nullable) {
|
if (colInfo.nullable) {
|
||||||
ztype = ztype.concat(".nullable().optional()");
|
ztype = ztype.concat(".nullable().optional()");
|
||||||
}
|
}
|
||||||
schema = schema.concat(`${!schema ? "\n" : ""} ${columnName}: ${ztype},\n`);
|
schema = schema.concat(
|
||||||
|
`${!schema ? "\n" : ""} ${columnName}: ${ztype}${colNum === columnNames.length - 1 ? "" : ","}\n`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashcase = tableName.split("_").join("-");
|
const dashcase = tableName.split("_").join("-");
|
||||||
const pascalCase = tableName
|
const pascalCase = tableName
|
||||||
.split("_")
|
.split("_")
|
||||||
.reduce(
|
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
|
||||||
(prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
||||||
`// Code generated by automation script, DO NOT EDIT.
|
`// Code generated by automation script, DO NOT EDIT.
|
||||||
@@ -152,15 +138,6 @@ export type T${pascalCase}Insert = Omit<T${pascalCase}, TImmutableDBKeys>;
|
|||||||
export type T${pascalCase}Update = Partial<Omit<T${pascalCase}, TImmutableDBKeys>>;
|
export type T${pascalCase}Update = Partial<Omit<T${pascalCase}, TImmutableDBKeys>>;
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
// const file = readFileSync(path.join(__dirname, "../src/db/schemas/index.ts"), "utf8");
|
|
||||||
// if (!file.includes(`export * from "./${dashcase};"`)) {
|
|
||||||
// appendFileSync(
|
|
||||||
// path.join(__dirname, "../src/db/schemas/index.ts"),
|
|
||||||
// `\nexport * from "./${dashcase}";`,
|
|
||||||
// "utf8"
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -6,6 +6,7 @@ import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-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 { 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 { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
@@ -105,6 +106,7 @@ declare module "fastify" {
|
|||||||
secretRotation: TSecretRotationServiceFactory;
|
secretRotation: TSecretRotationServiceFactory;
|
||||||
snapshot: TSecretSnapshotServiceFactory;
|
snapshot: TSecretSnapshotServiceFactory;
|
||||||
saml: TSamlConfigServiceFactory;
|
saml: TSamlConfigServiceFactory;
|
||||||
|
scim: TScimServiceFactory;
|
||||||
auditLog: TAuditLogServiceFactory;
|
auditLog: TAuditLogServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
|
4
backend/src/@types/knex.d.ts
vendored
4
backend/src/@types/knex.d.ts
vendored
@@ -83,6 +83,9 @@ import {
|
|||||||
TSamlConfigs,
|
TSamlConfigs,
|
||||||
TSamlConfigsInsert,
|
TSamlConfigsInsert,
|
||||||
TSamlConfigsUpdate,
|
TSamlConfigsUpdate,
|
||||||
|
TScimTokens,
|
||||||
|
TScimTokensInsert,
|
||||||
|
TScimTokensUpdate,
|
||||||
TSecretApprovalPolicies,
|
TSecretApprovalPolicies,
|
||||||
TSecretApprovalPoliciesApprovers,
|
TSecretApprovalPoliciesApprovers,
|
||||||
TSecretApprovalPoliciesApproversInsert,
|
TSecretApprovalPoliciesApproversInsert,
|
||||||
@@ -262,6 +265,7 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityProjectMembershipsInsert,
|
TIdentityProjectMembershipsInsert,
|
||||||
TIdentityProjectMembershipsUpdate
|
TIdentityProjectMembershipsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||||
TSecretApprovalPolicies,
|
TSecretApprovalPolicies,
|
||||||
TSecretApprovalPoliciesInsert,
|
TSecretApprovalPoliciesInsert,
|
||||||
|
@@ -5,9 +5,13 @@ import dotenv from "dotenv";
|
|||||||
import type { Knex } from "knex";
|
import type { Knex } from "knex";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
// Update with your config settings.
|
// Update with your config settings. .
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: path.join(__dirname, "../../.env"),
|
path: path.join(__dirname, "../../../.env.migration"),
|
||||||
|
debug: true
|
||||||
|
});
|
||||||
|
dotenv.config({
|
||||||
|
path: path.join(__dirname, "../../../.env"),
|
||||||
debug: true
|
debug: true
|
||||||
});
|
});
|
||||||
export default {
|
export default {
|
||||||
|
31
backend/src/db/migrations/20240208234120_scim-token.ts
Normal file
31
backend/src/db/migrations/20240208234120_scim-token.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ScimToken))) {
|
||||||
|
await knex.schema.createTable(TableName.ScimToken, (t) => {
|
||||||
|
t.string("id", 36).primary().defaultTo(knex.fn.uuid());
|
||||||
|
t.bigInteger("ttlDays").defaultTo(365).notNullable();
|
||||||
|
t.string("description").notNullable();
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.boolean("scimEnabled").defaultTo(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.ScimToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ScimToken);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ScimToken);
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.dropColumn("scimEnabled");
|
||||||
|
});
|
||||||
|
}
|
@@ -26,6 +26,7 @@ export * from "./project-memberships";
|
|||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./saml-configs";
|
export * from "./saml-configs";
|
||||||
|
export * from "./scim-tokens";
|
||||||
export * from "./secret-approval-policies";
|
export * from "./secret-approval-policies";
|
||||||
export * from "./secret-approval-policies-approvers";
|
export * from "./secret-approval-policies-approvers";
|
||||||
export * from "./secret-approval-request-secret-tags";
|
export * from "./secret-approval-request-secret-tags";
|
||||||
|
@@ -40,6 +40,7 @@ export enum TableName {
|
|||||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||||
IdentityOrgMembership = "identity_org_memberships",
|
IdentityOrgMembership = "identity_org_memberships",
|
||||||
IdentityProjectMembership = "identity_project_memberships",
|
IdentityProjectMembership = "identity_project_memberships",
|
||||||
|
ScimToken = "scim_tokens",
|
||||||
SecretApprovalPolicy = "secret_approval_policies",
|
SecretApprovalPolicy = "secret_approval_policies",
|
||||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||||
SecretApprovalRequest = "secret_approval_requests",
|
SecretApprovalRequest = "secret_approval_requests",
|
||||||
|
@@ -14,7 +14,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
authEnforced: z.boolean().default(false).nullable().optional()
|
authEnforced: z.boolean().default(false).nullable().optional(),
|
||||||
|
scimEnabled: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
21
backend/src/db/schemas/scim-tokens.ts
Normal file
21
backend/src/db/schemas/scim-tokens.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const ScimTokensSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
ttlDays: z.coerce.number().default(365),
|
||||||
|
description: z.string(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TScimTokens = z.infer<typeof ScimTokensSchema>;
|
||||||
|
export type TScimTokensInsert = Omit<TScimTokens, TImmutableDBKeys>;
|
||||||
|
export type TScimTokensUpdate = Partial<Omit<TScimTokens, TImmutableDBKeys>>;
|
@@ -6,13 +6,14 @@ import nacl from "tweetnacl";
|
|||||||
import { encodeBase64 } from "tweetnacl-util";
|
import { encodeBase64 } from "tweetnacl-util";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
decryptAsymmetric,
|
||||||
// decryptAsymmetric,
|
// decryptAsymmetric,
|
||||||
decryptSymmetric,
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
encryptSymmetric
|
encryptSymmetric128BitHexKeyUTF8
|
||||||
} from "@app/lib/crypto";
|
} from "@app/lib/crypto";
|
||||||
|
|
||||||
import { TUserEncryptionKeys } from "./schemas";
|
import { TSecrets, TUserEncryptionKeys } from "./schemas";
|
||||||
|
|
||||||
export const seedData1 = {
|
export const seedData1 = {
|
||||||
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
|
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
|
||||||
@@ -31,6 +32,14 @@ export const seedData1 = {
|
|||||||
name: "Development",
|
name: "Development",
|
||||||
slug: "dev"
|
slug: "dev"
|
||||||
},
|
},
|
||||||
|
machineIdentity: {
|
||||||
|
id: "88fa7aed-9288-401e-a4c9-fa9430be62a0",
|
||||||
|
name: "mac1",
|
||||||
|
clientCredentials: {
|
||||||
|
id: "3f6135db-f237-421d-af66-a8f4e80d443b",
|
||||||
|
secret: "da35a5a5a7b57f977a9a73394506e878a7175d06606df43dc93e1472b10cf339"
|
||||||
|
}
|
||||||
|
},
|
||||||
token: {
|
token: {
|
||||||
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
|
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
|
||||||
}
|
}
|
||||||
@@ -73,7 +82,7 @@ export const generateUserSrpKeys = async (password: string) => {
|
|||||||
ciphertext: encryptedPrivateKey,
|
ciphertext: encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag
|
tag: encryptedPrivateKeyTag
|
||||||
} = encryptSymmetric(privateKey, key.toString("base64"));
|
} = encryptSymmetric128BitHexKeyUTF8(privateKey, key);
|
||||||
|
|
||||||
// create the protected key by encrypting the symmetric key
|
// create the protected key by encrypting the symmetric key
|
||||||
// [key] with the derived key
|
// [key] with the derived key
|
||||||
@@ -81,7 +90,7 @@ export const generateUserSrpKeys = async (password: string) => {
|
|||||||
ciphertext: protectedKey,
|
ciphertext: protectedKey,
|
||||||
iv: protectedKeyIV,
|
iv: protectedKeyIV,
|
||||||
tag: protectedKeyTag
|
tag: protectedKeyTag
|
||||||
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
|
} = encryptSymmetric128BitHexKeyUTF8(key.toString("hex"), derivedKey);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
protectedKey,
|
protectedKey,
|
||||||
@@ -107,32 +116,102 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
|
|||||||
raw: true
|
raw: true
|
||||||
});
|
});
|
||||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||||
const key = decryptSymmetric({
|
|
||||||
|
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: user.protectedKey as string,
|
ciphertext: user.protectedKey as string,
|
||||||
iv: user.protectedKeyIV as string,
|
iv: user.protectedKeyIV as string,
|
||||||
tag: user.protectedKeyTag as string,
|
tag: user.protectedKeyTag as string,
|
||||||
key: derivedKey.toString("base64")
|
key: derivedKey
|
||||||
});
|
});
|
||||||
const privateKey = decryptSymmetric({
|
|
||||||
|
const privateKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: user.encryptedPrivateKey,
|
ciphertext: user.encryptedPrivateKey,
|
||||||
iv: user.iv,
|
iv: user.iv,
|
||||||
tag: user.tag,
|
tag: user.tag,
|
||||||
key
|
key: Buffer.from(key, "hex")
|
||||||
});
|
});
|
||||||
return privateKey;
|
return privateKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
|
export const buildUserProjectKey = (privateKey: string, publickey: string) => {
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
|
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
|
||||||
return { nonce, ciphertext };
|
return { nonce, ciphertext };
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const getUserProjectKey = async (privateKey: string) => {
|
export const getUserProjectKey = async (privateKey: string, ciphertext: string, nonce: string, publicKey: string) => {
|
||||||
// const key = decryptAsymmetric({
|
return decryptAsymmetric({
|
||||||
// ciphertext: decryptFileKey.encryptedKey,
|
ciphertext,
|
||||||
// nonce: decryptFileKey.nonce,
|
nonce,
|
||||||
// publicKey: decryptFileKey.sender.publicKey,
|
publicKey,
|
||||||
// privateKey: PRIVATE_KEY
|
privateKey
|
||||||
// });
|
});
|
||||||
// };
|
};
|
||||||
|
|
||||||
|
export const encryptSecret = (encKey: string, key: string, value?: string, comment?: string) => {
|
||||||
|
// encrypt key
|
||||||
|
const {
|
||||||
|
ciphertext: secretKeyCiphertext,
|
||||||
|
iv: secretKeyIV,
|
||||||
|
tag: secretKeyTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8(key, encKey);
|
||||||
|
|
||||||
|
// encrypt value
|
||||||
|
const {
|
||||||
|
ciphertext: secretValueCiphertext,
|
||||||
|
iv: secretValueIV,
|
||||||
|
tag: secretValueTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8(value ?? "", encKey);
|
||||||
|
|
||||||
|
// encrypt comment
|
||||||
|
const {
|
||||||
|
ciphertext: secretCommentCiphertext,
|
||||||
|
iv: secretCommentIV,
|
||||||
|
tag: secretCommentTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8(comment ?? "", encKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptSecret = (decryptKey: string, encSecret: TSecrets) => {
|
||||||
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
key: decryptKey,
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
iv: encSecret.secretKeyIV
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
key: decryptKey,
|
||||||
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
|
tag: encSecret.secretValueTag,
|
||||||
|
iv: encSecret.secretValueIV
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretComment =
|
||||||
|
encSecret.secretCommentIV && encSecret.secretCommentTag && encSecret.secretCommentCiphertext
|
||||||
|
? decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
key: decryptKey,
|
||||||
|
ciphertext: encSecret.secretCommentCiphertext,
|
||||||
|
tag: encSecret.secretCommentTag,
|
||||||
|
iv: encSecret.secretCommentIV
|
||||||
|
})
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: secretKey,
|
||||||
|
value: secretValue,
|
||||||
|
comment: secretComment,
|
||||||
|
version: encSecret.version
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
const [user] = await knex(TableName.Users)
|
const [user] = await knex(TableName.Users)
|
||||||
.insert([
|
.insert([
|
||||||
{
|
{
|
||||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore
|
||||||
id: seedData1.id,
|
id: seedData1.id,
|
||||||
email: seedData1.email,
|
email: seedData1.email,
|
||||||
superAdmin: true,
|
superAdmin: true,
|
||||||
@@ -48,7 +49,8 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await knex(TableName.AuthTokenSession).insert({
|
await knex(TableName.AuthTokenSession).insert({
|
||||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore
|
||||||
id: seedData1.token.id,
|
id: seedData1.token.id,
|
||||||
userId: seedData1.id,
|
userId: seedData1.id,
|
||||||
ip: "151.196.220.213",
|
ip: "151.196.220.213",
|
||||||
|
@@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
const [org] = await knex(TableName.Organization)
|
const [org] = await knex(TableName.Organization)
|
||||||
.insert([
|
.insert([
|
||||||
{
|
{
|
||||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore
|
||||||
id: seedData1.organization.id,
|
id: seedData1.organization.id,
|
||||||
name: "infisical",
|
name: "infisical",
|
||||||
slug: "infisical",
|
slug: "infisical",
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { OrgMembershipRole, TableName } from "../schemas";
|
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { seedData1 } from "../seed-data";
|
|
||||||
|
import { OrgMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||||
|
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_ENVS = [
|
export const DEFAULT_PROJECT_ENVS = [
|
||||||
{ name: "Development", slug: "dev" },
|
{ name: "Development", slug: "dev" },
|
||||||
@@ -20,21 +24,32 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
name: seedData1.project.name,
|
name: seedData1.project.name,
|
||||||
orgId: seedData1.organization.id,
|
orgId: seedData1.organization.id,
|
||||||
slug: "first-project",
|
slug: "first-project",
|
||||||
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore
|
||||||
id: seedData1.project.id
|
id: seedData1.project.id
|
||||||
})
|
})
|
||||||
.returning("*");
|
.returning("*");
|
||||||
|
|
||||||
// await knex(TableName.ProjectKeys).insert({
|
|
||||||
// projectId: project.id,
|
|
||||||
// senderId: seedData1.id
|
|
||||||
// });
|
|
||||||
|
|
||||||
await knex(TableName.ProjectMembership).insert({
|
await knex(TableName.ProjectMembership).insert({
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
role: OrgMembershipRole.Admin,
|
role: OrgMembershipRole.Admin,
|
||||||
userId: seedData1.id
|
userId: seedData1.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
|
||||||
|
if (!user) throw new Error("User not found");
|
||||||
|
|
||||||
|
const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
|
||||||
|
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
|
||||||
|
await knex(TableName.ProjectKeys).insert({
|
||||||
|
projectId: project.id,
|
||||||
|
nonce: projectKey.nonce,
|
||||||
|
encryptedKey: projectKey.ciphertext,
|
||||||
|
receiverId: seedData1.id,
|
||||||
|
senderId: seedData1.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// create default environments and default folders
|
||||||
const envs = await knex(TableName.Environment)
|
const envs = await knex(TableName.Environment)
|
||||||
.insert(
|
.insert(
|
||||||
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
|
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
|
||||||
@@ -46,4 +61,19 @@ export async function seed(knex: Knex): Promise<void> {
|
|||||||
)
|
)
|
||||||
.returning("*");
|
.returning("*");
|
||||||
await knex(TableName.SecretFolder).insert(envs.map(({ id }) => ({ name: "root", envId: id, parentId: null })));
|
await knex(TableName.SecretFolder).insert(envs.map(({ id }) => ({ name: "root", envId: id, parentId: null })));
|
||||||
|
|
||||||
|
// save secret secret blind index
|
||||||
|
const encKey = process.env.ENCRYPTION_KEY;
|
||||||
|
if (!encKey) throw new Error("Missing ENCRYPTION_KEY");
|
||||||
|
const salt = crypto.randomBytes(16).toString("base64");
|
||||||
|
const secretBlindIndex = encryptSymmetric128BitHexKeyUTF8(salt, encKey);
|
||||||
|
// insert secret blind index for project
|
||||||
|
await knex(TableName.SecretBlindIndex).insert({
|
||||||
|
projectId: project.id,
|
||||||
|
encryptedSaltCipherText: secretBlindIndex.ciphertext,
|
||||||
|
saltIV: secretBlindIndex.iv,
|
||||||
|
saltTag: secretBlindIndex.tag,
|
||||||
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
|
keyEncoding: SecretKeyEncoding.UTF8
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
83
backend/src/db/seeds/4-machine-identity.ts
Normal file
83
backend/src/db/seeds/4-machine-identity.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
|
||||||
|
import { seedData1 } from "../seed-data";
|
||||||
|
|
||||||
|
export async function seed(knex: Knex): Promise<void> {
|
||||||
|
// Deletes ALL existing entries
|
||||||
|
await knex(TableName.Identity).del();
|
||||||
|
await knex(TableName.IdentityOrgMembership).del();
|
||||||
|
|
||||||
|
// Inserts seed entries
|
||||||
|
await knex(TableName.Identity).insert([
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore
|
||||||
|
id: seedData1.machineIdentity.id,
|
||||||
|
name: seedData1.machineIdentity.name,
|
||||||
|
authMethod: IdentityAuthMethod.Univeral
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const identityUa = await knex(TableName.IdentityUniversalAuth)
|
||||||
|
.insert([
|
||||||
|
{
|
||||||
|
identityId: seedData1.machineIdentity.id,
|
||||||
|
clientId: seedData1.machineIdentity.clientCredentials.id,
|
||||||
|
clientSecretTrustedIps: JSON.stringify([
|
||||||
|
{
|
||||||
|
type: "ipv4",
|
||||||
|
prefix: 0,
|
||||||
|
ipAddress: "0.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ipv6",
|
||||||
|
prefix: 0,
|
||||||
|
ipAddress: "::"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
accessTokenTrustedIps: JSON.stringify([
|
||||||
|
{
|
||||||
|
type: "ipv4",
|
||||||
|
prefix: 0,
|
||||||
|
ipAddress: "0.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ipv6",
|
||||||
|
prefix: 0,
|
||||||
|
ipAddress: "::"
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
accessTokenTTL: 2592000,
|
||||||
|
accessTokenMaxTTL: 2592000,
|
||||||
|
accessTokenNumUsesLimit: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.returning("*");
|
||||||
|
const clientSecretHash = await bcrypt.hash(seedData1.machineIdentity.clientCredentials.secret, 10);
|
||||||
|
await knex(TableName.IdentityUaClientSecret).insert([
|
||||||
|
{
|
||||||
|
identityUAId: identityUa[0].id,
|
||||||
|
description: "",
|
||||||
|
clientSecretTTL: 0,
|
||||||
|
clientSecretNumUses: 0,
|
||||||
|
clientSecretNumUsesLimit: 0,
|
||||||
|
clientSecretPrefix: seedData1.machineIdentity.clientCredentials.secret.slice(0, 4),
|
||||||
|
clientSecretHash,
|
||||||
|
isClientSecretRevoked: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
await knex(TableName.IdentityOrgMembership).insert([
|
||||||
|
{
|
||||||
|
identityId: seedData1.machineIdentity.id,
|
||||||
|
orgId: seedData1.organization.id,
|
||||||
|
role: OrgMembershipRole.Admin
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await knex(TableName.IdentityProjectMembership).insert({
|
||||||
|
identityId: seedData1.machineIdentity.id,
|
||||||
|
role: ProjectMembershipRole.Admin,
|
||||||
|
projectId: seedData1.project.id
|
||||||
|
});
|
||||||
|
}
|
@@ -3,6 +3,7 @@ import { registerOrgRoleRouter } from "./org-role-router";
|
|||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
import { registerProjectRoleRouter } from "./project-role-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerSamlRouter } from "./saml-router";
|
import { registerSamlRouter } from "./saml-router";
|
||||||
|
import { registerScimRouter } from "./scim-router";
|
||||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||||
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
||||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||||
@@ -33,6 +34,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
prefix: "/secret-rotation-providers"
|
prefix: "/secret-rotation-providers"
|
||||||
});
|
});
|
||||||
await server.register(registerSamlRouter, { prefix: "/sso" });
|
await server.register(registerSamlRouter, { prefix: "/sso" });
|
||||||
|
await server.register(registerScimRouter, { prefix: "/scim" });
|
||||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||||
|
@@ -11,6 +11,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/secret-snapshots",
|
url: "/:workspaceId/secret-snapshots",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return project secret snapshots ids",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
apiKeyAuth: [],
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -74,6 +81,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/audit-logs",
|
url: "/:workspaceId/audit-logs",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return audit logs",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
@@ -19,7 +19,6 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
|
||||||
|
|
||||||
type TSAMLConfig = {
|
type TSAMLConfig = {
|
||||||
callbackUrl: string;
|
callbackUrl: string;
|
||||||
@@ -28,6 +27,7 @@ type TSAMLConfig = {
|
|||||||
cert: string;
|
cert: string;
|
||||||
audience: string;
|
audience: string;
|
||||||
wantAuthnResponseSigned?: boolean;
|
wantAuthnResponseSigned?: boolean;
|
||||||
|
disableRequestedAuthnContext?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -67,7 +67,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
throw new BadRequestError({ message: "Failed to authenticate with SAML SSO" });
|
throw new BadRequestError({ message: "Failed to authenticate with SAML SSO" });
|
||||||
|
|
||||||
const samlConfig: TSAMLConfig = {
|
const samlConfig: TSAMLConfig = {
|
||||||
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${samlConfigId}`,
|
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`,
|
||||||
entryPoint: ssoConfig.entryPoint,
|
entryPoint: ssoConfig.entryPoint,
|
||||||
issuer: ssoConfig.issuer,
|
issuer: ssoConfig.issuer,
|
||||||
cert: ssoConfig.cert,
|
cert: ssoConfig.cert,
|
||||||
@@ -77,7 +77,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
samlConfig.wantAuthnResponseSigned = false;
|
samlConfig.wantAuthnResponseSigned = false;
|
||||||
}
|
}
|
||||||
if (ssoConfig.authProvider === SamlProviders.AZURE_SAML) {
|
if (ssoConfig.authProvider === SamlProviders.AZURE_SAML) {
|
||||||
if (req.body.RelayState && JSON.parse(req.body.RelayState).spIntiaited) {
|
samlConfig.disableRequestedAuthnContext = true;
|
||||||
|
if (req.body?.RelayState && JSON.parse(req.body.RelayState).spInitiated) {
|
||||||
samlConfig.audience = `spn:${ssoConfig.issuer}`;
|
samlConfig.audience = `spn:${ssoConfig.issuer}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +93,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req, profile, cb) => {
|
async (req, profile, cb) => {
|
||||||
try {
|
try {
|
||||||
const serverCfg = await getServerCfg();
|
|
||||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||||
const { firstName } = profile;
|
const { firstName } = profile;
|
||||||
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
|
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
|
||||||
@@ -105,7 +105,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
email,
|
email,
|
||||||
firstName: profile.firstName as string,
|
firstName: profile.firstName as string,
|
||||||
lastName: profile.lastName as string,
|
lastName: profile.lastName as string,
|
||||||
isSignupAllowed: Boolean(serverCfg.allowSignUp),
|
|
||||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
||||||
|
331
backend/src/ee/routes/v1/scim-router.ts
Normal file
331
backend/src/ee/routes/v1/scim-router.ts
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ScimTokensSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, function (req, body, done) {
|
||||||
|
try {
|
||||||
|
const strBody = body instanceof Buffer ? body.toString() : body;
|
||||||
|
|
||||||
|
const json: unknown = JSON.parse(strBody);
|
||||||
|
done(null, json);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
done(error, undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/scim-tokens",
|
||||||
|
method: "POST",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
organizationId: z.string().trim(),
|
||||||
|
description: z.string().trim().default(""),
|
||||||
|
ttlDays: z.number().min(0).default(0)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
scimToken: z.string().trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { scimToken } = await server.services.scim.createScimToken({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
description: req.body.description,
|
||||||
|
ttlDays: req.body.ttlDays
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scimToken };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/scim-tokens",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
scimTokens: z.array(ScimTokensSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const scimTokens = await server.services.scim.listScimTokens({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
orgId: req.query.organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scimTokens };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/scim-tokens/:scimTokenId",
|
||||||
|
method: "DELETE",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
scimTokenId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
scimToken: ScimTokensSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const scimToken = await server.services.scim.deleteScimToken({
|
||||||
|
scimTokenId: req.params.scimTokenId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scimToken };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// SCIM server endpoints
|
||||||
|
server.route({
|
||||||
|
url: "/Users",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
startIndex: z.coerce.number().default(1),
|
||||||
|
count: z.coerce.number().default(20),
|
||||||
|
filter: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
Resources: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
emails: z.array(
|
||||||
|
z.object({
|
||||||
|
primary: z.boolean(),
|
||||||
|
value: z.string().email(),
|
||||||
|
type: z.string().trim()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
itemsPerPage: z.number(),
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
startIndex: z.number(),
|
||||||
|
totalResults: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const users = await req.server.services.scim.listScimUsers({
|
||||||
|
offset: req.query.startIndex,
|
||||||
|
limit: req.query.count,
|
||||||
|
filter: req.query.filter,
|
||||||
|
orgId: req.permission.orgId as string
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Users/:userId",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
201: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
emails: z.array(
|
||||||
|
z.object({
|
||||||
|
primary: z.boolean(),
|
||||||
|
value: z.string().email(),
|
||||||
|
type: z.string().trim()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await req.server.services.scim.getScimUser({
|
||||||
|
userId: req.params.userId,
|
||||||
|
orgId: req.permission.orgId as string
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Users",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
userName: z.string().trim().email(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
// emails: z.array( // optional?
|
||||||
|
// z.object({
|
||||||
|
// primary: z.boolean(),
|
||||||
|
// value: z.string().email(),
|
||||||
|
// type: z.string().trim()
|
||||||
|
// })
|
||||||
|
// ),
|
||||||
|
// displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim().email(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
emails: z.array(
|
||||||
|
z.object({
|
||||||
|
primary: z.boolean(),
|
||||||
|
value: z.string().email(),
|
||||||
|
type: z.string().trim()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await req.server.services.scim.createScimUser({
|
||||||
|
email: req.body.userName,
|
||||||
|
firstName: req.body.name.givenName,
|
||||||
|
lastName: req.body.name.familyName,
|
||||||
|
orgId: req.permission.orgId as string
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Users/:userId",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
Operations: z.array(
|
||||||
|
z.object({
|
||||||
|
op: z.string().trim(),
|
||||||
|
path: z.string().trim().optional(),
|
||||||
|
value: z.union([
|
||||||
|
z.object({
|
||||||
|
active: z.boolean()
|
||||||
|
}),
|
||||||
|
z.string().trim()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await req.server.services.scim.updateScimUser({
|
||||||
|
userId: req.params.userId,
|
||||||
|
orgId: req.permission.orgId as string,
|
||||||
|
operations: req.body.Operations
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/Users/:userId",
|
||||||
|
method: "PUT",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
schemas: z.array(z.string()),
|
||||||
|
id: z.string().trim(),
|
||||||
|
userName: z.string().trim(),
|
||||||
|
name: z.object({
|
||||||
|
familyName: z.string().trim(),
|
||||||
|
givenName: z.string().trim()
|
||||||
|
}),
|
||||||
|
emails: z.array(
|
||||||
|
z.object({
|
||||||
|
primary: z.boolean(),
|
||||||
|
value: z.string().email(),
|
||||||
|
type: z.string().trim()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
displayName: z.string().trim(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const user = await req.server.services.scim.replaceScimUser({
|
||||||
|
userId: req.params.userId,
|
||||||
|
orgId: req.permission.orgId as string,
|
||||||
|
active: req.body.active
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -57,6 +57,13 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:secretSnapshotId/rollback",
|
url: "/:secretSnapshotId/rollback",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Roll back project secrets to those captured in a secret snapshot version.",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
apiKeyAuth: [],
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretSnapshotId: z.string().trim()
|
secretSnapshotId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
@@ -15,7 +15,7 @@ export type TListProjectAuditLogDTO = {
|
|||||||
|
|
||||||
export type TCreateAuditLogDTO = {
|
export type TCreateAuditLogDTO = {
|
||||||
event: Event;
|
event: Event;
|
||||||
actor: UserActor | IdentityActor | ServiceActor;
|
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
} & BaseAuthData;
|
} & BaseAuthData;
|
||||||
@@ -105,6 +105,8 @@ interface IdentityActorMetadata {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScimClientActorMetadata {}
|
||||||
|
|
||||||
export interface UserActor {
|
export interface UserActor {
|
||||||
type: ActorType.USER;
|
type: ActorType.USER;
|
||||||
metadata: UserActorMetadata;
|
metadata: UserActorMetadata;
|
||||||
@@ -120,7 +122,12 @@ export interface IdentityActor {
|
|||||||
metadata: IdentityActorMetadata;
|
metadata: IdentityActorMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Actor = UserActor | ServiceActor | IdentityActor;
|
export interface ScimClientActor {
|
||||||
|
type: ActorType.SCIM_CLIENT;
|
||||||
|
metadata: ScimClientActorMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
|
||||||
|
|
||||||
interface GetSecretsEvent {
|
interface GetSecretsEvent {
|
||||||
type: EventType.GET_SECRETS;
|
type: EventType.GET_SECRETS;
|
||||||
|
27
backend/src/ee/services/license/__mocks__/licence-fns.ts
Normal file
27
backend/src/ee/services/license/__mocks__/licence-fns.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const getDefaultOnPremFeatures = () => {
|
||||||
|
return {
|
||||||
|
_id: null,
|
||||||
|
slug: null,
|
||||||
|
tier: -1,
|
||||||
|
workspaceLimit: null,
|
||||||
|
workspacesUsed: 0,
|
||||||
|
memberLimit: null,
|
||||||
|
membersUsed: 0,
|
||||||
|
environmentLimit: null,
|
||||||
|
environmentsUsed: 0,
|
||||||
|
secretVersioning: true,
|
||||||
|
pitRecovery: false,
|
||||||
|
ipAllowlisting: true,
|
||||||
|
rbac: false,
|
||||||
|
customRateLimits: false,
|
||||||
|
customAlerts: false,
|
||||||
|
auditLogs: false,
|
||||||
|
auditLogsRetentionDays: 0,
|
||||||
|
samlSSO: false,
|
||||||
|
status: null,
|
||||||
|
trial_end: null,
|
||||||
|
has_used_trial: true,
|
||||||
|
secretApproval: false,
|
||||||
|
secretRotation: true
|
||||||
|
};
|
||||||
|
};
|
@@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
|
scim: false,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
|
@@ -44,7 +44,7 @@ type TLicenseServiceFactoryDep = {
|
|||||||
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||||
|
|
||||||
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
||||||
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/licence-login";
|
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
|
||||||
|
|
||||||
const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`;
|
const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`;
|
||||||
export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: TLicenseServiceFactoryDep) => {
|
export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: TLicenseServiceFactoryDep) => {
|
||||||
@@ -92,7 +92,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
|||||||
// else it would reach catch statement
|
// else it would reach catch statement
|
||||||
isValidLicense = true;
|
isValidLicense = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`init-license: encountered an error when init license [error]`, error);
|
logger.error(error, `init-license: encountered an error when init license`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -25,6 +25,7 @@ export type TFeatureSet = {
|
|||||||
auditLogs: false;
|
auditLogs: false;
|
||||||
auditLogsRetentionDays: 0;
|
auditLogsRetentionDays: 0;
|
||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
|
scim: false;
|
||||||
status: null;
|
status: null;
|
||||||
trial_end: null;
|
trial_end: null;
|
||||||
has_used_trial: true;
|
has_used_trial: true;
|
||||||
|
@@ -16,6 +16,7 @@ export enum OrgPermissionSubjects {
|
|||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
IncidentAccount = "incident-contact",
|
IncidentAccount = "incident-contact",
|
||||||
Sso = "sso",
|
Sso = "sso",
|
||||||
|
Scim = "scim",
|
||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity"
|
Identity = "identity"
|
||||||
@@ -29,6 +30,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
|
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||||
|
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
||||||
@@ -69,6 +71,11 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
|
||||||
|
|
||||||
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
|
||||||
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
|
||||||
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||||
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||||
|
@@ -195,7 +195,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
updateQuery.certTag = certTag;
|
updateQuery.certTag = certTag;
|
||||||
}
|
}
|
||||||
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
||||||
await orgDAL.updateById(orgId, { authEnforced: false });
|
await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false });
|
||||||
|
|
||||||
return ssoConfig;
|
return ssoConfig;
|
||||||
};
|
};
|
||||||
@@ -300,19 +300,9 @@ export const samlConfigServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const samlLogin = async ({
|
const samlLogin = async ({ firstName, email, lastName, authProvider, orgId, relayState }: TSamlLoginDTO) => {
|
||||||
firstName,
|
|
||||||
email,
|
|
||||||
lastName,
|
|
||||||
authProvider,
|
|
||||||
orgId,
|
|
||||||
relayState,
|
|
||||||
isSignupAllowed
|
|
||||||
}: TSamlLoginDTO) => {
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
let user = await userDAL.findUserByEmail(email);
|
let user = await userDAL.findUserByEmail(email);
|
||||||
const isSamlSignUpDisabled = !isSignupAllowed && !user;
|
|
||||||
if (isSamlSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Saml SSO login" });
|
|
||||||
|
|
||||||
const organization = await orgDAL.findOrgById(orgId);
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
||||||
|
@@ -41,7 +41,6 @@ export type TSamlLoginDTO = {
|
|||||||
lastName?: string;
|
lastName?: string;
|
||||||
authProvider: string;
|
authProvider: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
isSignupAllowed: boolean;
|
|
||||||
// saml thingy
|
// saml thingy
|
||||||
relayState?: string;
|
relayState?: string;
|
||||||
};
|
};
|
||||||
|
10
backend/src/ee/services/scim/scim-dal.ts
Normal file
10
backend/src/ee/services/scim/scim-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TScimDALFactory = ReturnType<typeof scimDALFactory>;
|
||||||
|
|
||||||
|
export const scimDALFactory = (db: TDbClient) => {
|
||||||
|
const scimTokenOrm = ormify(db, TableName.ScimToken);
|
||||||
|
return scimTokenOrm;
|
||||||
|
};
|
58
backend/src/ee/services/scim/scim-fns.ts
Normal file
58
backend/src/ee/services/scim/scim-fns.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { TListScimUsers, TScimUser } from "./scim-types";
|
||||||
|
|
||||||
|
export const buildScimUserList = ({
|
||||||
|
scimUsers,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
}: {
|
||||||
|
scimUsers: TScimUser[];
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}): TListScimUsers => {
|
||||||
|
return {
|
||||||
|
Resources: scimUsers,
|
||||||
|
itemsPerPage: limit,
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
||||||
|
startIndex: offset,
|
||||||
|
totalResults: scimUsers.length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildScimUser = ({
|
||||||
|
userId,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
active
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
active: boolean;
|
||||||
|
}): TScimUser => {
|
||||||
|
return {
|
||||||
|
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||||
|
id: userId,
|
||||||
|
userName: email,
|
||||||
|
displayName: `${firstName} ${lastName}`,
|
||||||
|
name: {
|
||||||
|
givenName: firstName,
|
||||||
|
middleName: null,
|
||||||
|
familyName: lastName
|
||||||
|
},
|
||||||
|
emails: [
|
||||||
|
{
|
||||||
|
primary: true,
|
||||||
|
value: email,
|
||||||
|
type: "work"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
active,
|
||||||
|
groups: [],
|
||||||
|
meta: {
|
||||||
|
resourceType: "User",
|
||||||
|
location: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
430
backend/src/ee/services/scim/scim-service.ts
Normal file
430
backend/src/ee/services/scim/scim-service.ts
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||||
|
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { deleteOrgMembership } from "@app/services/org/org-fns";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { buildScimUser, buildScimUserList } from "./scim-fns";
|
||||||
|
import {
|
||||||
|
TCreateScimTokenDTO,
|
||||||
|
TCreateScimUserDTO,
|
||||||
|
TDeleteScimTokenDTO,
|
||||||
|
TGetScimUserDTO,
|
||||||
|
TListScimUsers,
|
||||||
|
TListScimUsersDTO,
|
||||||
|
TReplaceScimUserDTO,
|
||||||
|
TScimTokenJwtPayload,
|
||||||
|
TUpdateScimUserDTO
|
||||||
|
} from "./scim-types";
|
||||||
|
|
||||||
|
type TScimServiceFactoryDep = {
|
||||||
|
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">;
|
||||||
|
orgDAL: Pick<
|
||||||
|
TOrgDALFactory,
|
||||||
|
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
|
||||||
|
>;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
smtpService: TSmtpService;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||||
|
|
||||||
|
export const scimServiceFactory = ({
|
||||||
|
licenseService,
|
||||||
|
scimDAL,
|
||||||
|
userDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
permissionService,
|
||||||
|
smtpService
|
||||||
|
}: TScimServiceFactoryDep) => {
|
||||||
|
const createScimToken = async ({ actor, actorId, actorOrgId, orgId, description, ttlDays }: TCreateScimTokenDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.scim)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create a SCIM token due to plan restriction. Upgrade plan to create a SCIM token."
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const scimTokenData = await scimDAL.create({
|
||||||
|
orgId,
|
||||||
|
description,
|
||||||
|
ttlDays
|
||||||
|
});
|
||||||
|
|
||||||
|
const scimToken = jwt.sign(
|
||||||
|
{
|
||||||
|
scimTokenId: scimTokenData.id,
|
||||||
|
authTokenType: AuthTokenType.SCIM_TOKEN
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
return { scimToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
const listScimTokens = async ({ actor, actorId, actorOrgId, orgId }: TOrgPermission) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.scim)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to get SCIM tokens due to plan restriction. Upgrade plan to get SCIM tokens."
|
||||||
|
});
|
||||||
|
|
||||||
|
const scimTokens = await scimDAL.find({ orgId });
|
||||||
|
return scimTokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteScimToken = async ({ scimTokenId, actor, actorId, actorOrgId }: TDeleteScimTokenDTO) => {
|
||||||
|
let scimToken = await scimDAL.findById(scimTokenId);
|
||||||
|
if (!scimToken) throw new BadRequestError({ message: "Failed to find SCIM token to delete" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, scimToken.orgId, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(scimToken.orgId);
|
||||||
|
if (!plan.scim)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to delete the SCIM token due to plan restriction. Upgrade plan to delete the SCIM token."
|
||||||
|
});
|
||||||
|
|
||||||
|
scimToken = await scimDAL.deleteById(scimTokenId);
|
||||||
|
|
||||||
|
return scimToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
// SCIM server endpoints
|
||||||
|
const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
|
||||||
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
|
if (!org.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseFilter = (filterToParse: string | undefined) => {
|
||||||
|
if (!filterToParse) return {};
|
||||||
|
const [parsedName, parsedValue] = filterToParse.split("eq").map((s) => s.trim());
|
||||||
|
|
||||||
|
let attributeName = parsedName;
|
||||||
|
if (parsedName === "userName") {
|
||||||
|
attributeName = "email";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [attributeName]: parsedValue };
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOpts = {
|
||||||
|
...(offset && { offset }),
|
||||||
|
...(limit && { limit })
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = await orgDAL.findMembership(
|
||||||
|
{
|
||||||
|
orgId,
|
||||||
|
...parseFilter(filter)
|
||||||
|
},
|
||||||
|
findOpts
|
||||||
|
);
|
||||||
|
|
||||||
|
const scimUsers = users.map(({ userId, firstName, lastName, email }) =>
|
||||||
|
buildScimUser({
|
||||||
|
userId: userId ?? "",
|
||||||
|
firstName: firstName ?? "",
|
||||||
|
lastName: lastName ?? "",
|
||||||
|
email,
|
||||||
|
active: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildScimUserList({
|
||||||
|
scimUsers,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => {
|
||||||
|
const [membership] = await orgDAL
|
||||||
|
.findMembership({
|
||||||
|
userId,
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
return buildScimUser({
|
||||||
|
userId: membership.userId as string,
|
||||||
|
firstName: membership.firstName as string,
|
||||||
|
lastName: membership.lastName as string,
|
||||||
|
email: membership.email,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createScimUser = async ({ firstName, lastName, email, orgId }: TCreateScimUserDTO) => {
|
||||||
|
const org = await orgDAL.findById(orgId);
|
||||||
|
|
||||||
|
if (!org)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "Organization not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!org.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
let user = await userDAL.findOne({
|
||||||
|
email
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
await userDAL.transaction(async (tx) => {
|
||||||
|
const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx });
|
||||||
|
if (orgMembership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User already exists in the database",
|
||||||
|
status: 409
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orgMembership) {
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
orgId,
|
||||||
|
inviteEmail: email,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Invited
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
user = await userDAL.transaction(async (tx) => {
|
||||||
|
const newUser = await userDAL.create(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
authMethods: [AuthMethod.EMAIL]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
inviteEmail: email,
|
||||||
|
orgId,
|
||||||
|
userId: newUser.id,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Invited
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newUser;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.ScimUserProvisioned,
|
||||||
|
subjectLine: "Infisical organization invitation",
|
||||||
|
recipients: [email],
|
||||||
|
substitutions: {
|
||||||
|
organizationName: org.name,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/api/v1/sso/redirect/saml2/organizations/${org.slug}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return buildScimUser({
|
||||||
|
userId: user.id,
|
||||||
|
firstName: user.firstName as string,
|
||||||
|
lastName: user.lastName as string,
|
||||||
|
email: user.email,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => {
|
||||||
|
const [membership] = await orgDAL
|
||||||
|
.findMembership({
|
||||||
|
userId,
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
operations.forEach((operation) => {
|
||||||
|
if (operation.op.toLowerCase() === "replace") {
|
||||||
|
if (operation.path === "active" && operation.value === "False") {
|
||||||
|
// azure scim op format
|
||||||
|
active = false;
|
||||||
|
} else if (typeof operation.value === "object" && operation.value.active === false) {
|
||||||
|
// okta scim op format
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
await deleteOrgMembership({
|
||||||
|
orgMembershipId: membership.id,
|
||||||
|
orgId: membership.orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildScimUser({
|
||||||
|
userId: membership.userId as string,
|
||||||
|
firstName: membership.firstName as string,
|
||||||
|
lastName: membership.lastName as string,
|
||||||
|
email: membership.email,
|
||||||
|
active
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => {
|
||||||
|
const [membership] = await orgDAL
|
||||||
|
.findMembership({
|
||||||
|
userId,
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "User not found",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership.scimEnabled)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "SCIM is disabled for the organization",
|
||||||
|
status: 403
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
// tx
|
||||||
|
await deleteOrgMembership({
|
||||||
|
orgMembershipId: membership.id,
|
||||||
|
orgId: membership.orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildScimUser({
|
||||||
|
userId: membership.userId as string,
|
||||||
|
firstName: membership.firstName as string,
|
||||||
|
lastName: membership.lastName as string,
|
||||||
|
email: membership.email,
|
||||||
|
active
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fnValidateScimToken = async (token: TScimTokenJwtPayload) => {
|
||||||
|
const scimToken = await scimDAL.findById(token.scimTokenId);
|
||||||
|
if (!scimToken) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const { ttlDays, createdAt } = scimToken;
|
||||||
|
|
||||||
|
// ttl check
|
||||||
|
if (Number(ttlDays) > 0) {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const scimTokenCreatedAt = new Date(createdAt);
|
||||||
|
const ttlInMilliseconds = Number(scimToken.ttlDays) * 86400 * 1000;
|
||||||
|
const expirationDate = new Date(scimTokenCreatedAt.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
|
if (currentDate > expirationDate)
|
||||||
|
throw new ScimRequestError({
|
||||||
|
detail: "The access token expired",
|
||||||
|
status: 401
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { scimTokenId: scimToken.id, orgId: scimToken.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createScimToken,
|
||||||
|
listScimTokens,
|
||||||
|
deleteScimToken,
|
||||||
|
listScimUsers,
|
||||||
|
getScimUser,
|
||||||
|
createScimUser,
|
||||||
|
updateScimUser,
|
||||||
|
replaceScimUser,
|
||||||
|
fnValidateScimToken
|
||||||
|
};
|
||||||
|
};
|
87
backend/src/ee/services/scim/scim-types.ts
Normal file
87
backend/src/ee/services/scim/scim-types.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateScimTokenDTO = {
|
||||||
|
description: string;
|
||||||
|
ttlDays: number;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TDeleteScimTokenDTO = {
|
||||||
|
scimTokenId: string;
|
||||||
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
// SCIM server endpoint types
|
||||||
|
|
||||||
|
export type TListScimUsersDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
filter?: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListScimUsers = {
|
||||||
|
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"];
|
||||||
|
totalResults: number;
|
||||||
|
Resources: TScimUser[];
|
||||||
|
itemsPerPage: number;
|
||||||
|
startIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetScimUserDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateScimUserDTO = {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateScimUserDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
operations: {
|
||||||
|
op: string;
|
||||||
|
path?: string;
|
||||||
|
value?:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TReplaceScimUserDTO = {
|
||||||
|
userId: string;
|
||||||
|
active: boolean;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TScimTokenJwtPayload = {
|
||||||
|
scimTokenId: string;
|
||||||
|
authTokenType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TScimUser = {
|
||||||
|
schemas: string[];
|
||||||
|
id: string;
|
||||||
|
userName: string;
|
||||||
|
displayName: string;
|
||||||
|
name: {
|
||||||
|
givenName: string;
|
||||||
|
middleName: null;
|
||||||
|
familyName: string;
|
||||||
|
};
|
||||||
|
emails: {
|
||||||
|
primary: boolean;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
}[];
|
||||||
|
active: boolean;
|
||||||
|
groups: string[];
|
||||||
|
meta: {
|
||||||
|
resourceType: string;
|
||||||
|
location: null;
|
||||||
|
};
|
||||||
|
};
|
@@ -95,7 +95,7 @@ const envSchema = z
|
|||||||
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
|
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
|
||||||
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
|
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||||
// LICENCE
|
// LICENCE
|
||||||
LICENSE_SERVER_URL: zpStr(z.string().optional()),
|
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
|
||||||
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
|
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
|
||||||
LICENSE_KEY: zpStr(z.string().optional()),
|
LICENSE_KEY: zpStr(z.string().optional()),
|
||||||
STANDALONE_MODE: z
|
STANDALONE_MODE: z
|
||||||
|
@@ -44,7 +44,7 @@ export const encryptSymmetric = (plaintext: string, key: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) => {
|
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string | Buffer) => {
|
||||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
|
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
|
||||||
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
|
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
|
||||||
|
|
||||||
@@ -58,7 +58,12 @@ export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string)
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decryptSymmetric128BitHexKeyUTF8 = ({ ciphertext, iv, tag, key }: TDecryptSymmetricInput): string => {
|
export const decryptSymmetric128BitHexKeyUTF8 = ({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
key
|
||||||
|
}: Omit<TDecryptSymmetricInput, "key"> & { key: string | Buffer }): string => {
|
||||||
const decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64"));
|
const decipher = crypto.createDecipheriv(SecretEncryptionAlgo.AES_256_GCM, key, Buffer.from(iv, "base64"));
|
||||||
|
|
||||||
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
||||||
|
@@ -58,3 +58,35 @@ export class BadRequestError extends Error {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ScimRequestError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
schemas: string[];
|
||||||
|
|
||||||
|
detail: string;
|
||||||
|
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
name,
|
||||||
|
error,
|
||||||
|
detail,
|
||||||
|
status
|
||||||
|
}: {
|
||||||
|
message?: string;
|
||||||
|
name?: string;
|
||||||
|
error?: unknown;
|
||||||
|
detail: string;
|
||||||
|
status: number;
|
||||||
|
}) {
|
||||||
|
super(detail ?? "The request is invalid");
|
||||||
|
this.name = name || "ScimRequestError";
|
||||||
|
this.schemas = ["urn:ietf:params:scim:api:messages:2.0:Error"];
|
||||||
|
this.error = error;
|
||||||
|
this.detail = detail;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -37,7 +37,7 @@ type TMain = {
|
|||||||
export const main = async ({ db, smtp, logger, queue }: TMain) => {
|
export const main = async ({ db, smtp, logger, queue }: TMain) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const server = fasitfy({
|
const server = fasitfy({
|
||||||
logger,
|
logger: appCfg.NODE_ENV === "test" ? false : logger,
|
||||||
trustProxy: true,
|
trustProxy: true,
|
||||||
connectionTimeout: 30 * 1000,
|
connectionTimeout: 30 * 1000,
|
||||||
ignoreTrailingSlash: true
|
ignoreTrailingSlash: true
|
||||||
|
@@ -63,6 +63,11 @@ export const injectAuditLogInfo = fp(async (server: FastifyZodProvider) => {
|
|||||||
identityId: req.auth.identityId
|
identityId: req.auth.identityId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
|
||||||
|
payload.actor = {
|
||||||
|
type: ActorType.SCIM_CLIENT,
|
||||||
|
metadata: {}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new BadRequestError({ message: "Missing logic for other actor" });
|
throw new BadRequestError({ message: "Missing logic for other actor" });
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ import fp from "fastify-plugin";
|
|||||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
|
||||||
import { TServiceTokens, TUsers } from "@app/db/schemas";
|
import { TServiceTokens, TUsers } from "@app/db/schemas";
|
||||||
|
import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
@@ -35,6 +36,12 @@ export type TAuthMode =
|
|||||||
actor: ActorType.IDENTITY;
|
actor: ActorType.IDENTITY;
|
||||||
identityId: string;
|
identityId: string;
|
||||||
identityName: string;
|
identityName: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
authMode: AuthMode.SCIM_TOKEN;
|
||||||
|
actor: ActorType.SCIM_CLIENT;
|
||||||
|
scimTokenId: string;
|
||||||
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||||
@@ -55,6 +62,7 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
|
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
|
||||||
|
|
||||||
switch (decodedToken.authTokenType) {
|
switch (decodedToken.authTokenType) {
|
||||||
case AuthTokenType.ACCESS_TOKEN:
|
case AuthTokenType.ACCESS_TOKEN:
|
||||||
return {
|
return {
|
||||||
@@ -70,6 +78,12 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
|||||||
token: decodedToken as TIdentityAccessTokenJwtPayload,
|
token: decodedToken as TIdentityAccessTokenJwtPayload,
|
||||||
actor: ActorType.IDENTITY
|
actor: ActorType.IDENTITY
|
||||||
} as const;
|
} as const;
|
||||||
|
case AuthTokenType.SCIM_TOKEN:
|
||||||
|
return {
|
||||||
|
authMode: AuthMode.SCIM_TOKEN,
|
||||||
|
token: decodedToken as TScimTokenJwtPayload,
|
||||||
|
actor: ActorType.SCIM_CLIENT
|
||||||
|
} as const;
|
||||||
default:
|
default:
|
||||||
return { authMode: null, token: null } as const;
|
return { authMode: null, token: null } as const;
|
||||||
}
|
}
|
||||||
@@ -113,6 +127,11 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
|
req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AuthMode.SCIM_TOKEN: {
|
||||||
|
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
|
||||||
|
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId };
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new UnauthorizedError({ name: "Unknown token strategy" });
|
throw new UnauthorizedError({ name: "Unknown token strategy" });
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,8 @@ export const injectPermission = fp(async (server) => {
|
|||||||
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
|
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId };
|
||||||
} else if (req.auth.actor === ActorType.SERVICE) {
|
} else if (req.auth.actor === ActorType.SERVICE) {
|
||||||
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId };
|
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId };
|
||||||
|
} else if (req.auth.actor === ActorType.SCIM_CLIENT) {
|
||||||
|
req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -2,7 +2,13 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { BadRequestError, DatabaseError, InternalServerError, UnauthorizedError } from "@app/lib/errors";
|
import {
|
||||||
|
BadRequestError,
|
||||||
|
DatabaseError,
|
||||||
|
InternalServerError,
|
||||||
|
ScimRequestError,
|
||||||
|
UnauthorizedError
|
||||||
|
} from "@app/lib/errors";
|
||||||
|
|
||||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
@@ -21,6 +27,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
error: "PermissionDenied",
|
error: "PermissionDenied",
|
||||||
message: `You are not allowed to ${error.action} on ${error.subjectType}`
|
message: `You are not allowed to ${error.action} on ${error.subjectType}`
|
||||||
});
|
});
|
||||||
|
} else if (error instanceof ScimRequestError) {
|
||||||
|
void res.status(error.status).send({
|
||||||
|
schemas: error.schemas,
|
||||||
|
status: error.status,
|
||||||
|
detail: error.detail
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
void res.send(error);
|
void res.send(error);
|
||||||
}
|
}
|
||||||
|
@@ -25,13 +25,13 @@ export const fastifySwagger = fp(async (fastify) => {
|
|||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
bearer: {
|
bearerAuth: {
|
||||||
type: "http",
|
type: "http",
|
||||||
scheme: "bearer",
|
scheme: "bearer",
|
||||||
bearerFormat: "JWT",
|
bearerFormat: "JWT",
|
||||||
description: "A service token in Infisical"
|
description: "An access token in Infisical"
|
||||||
},
|
},
|
||||||
apiKey: {
|
apiKeyAuth: {
|
||||||
type: "apiKey",
|
type: "apiKey",
|
||||||
in: "header",
|
in: "header",
|
||||||
name: "X-API-Key",
|
name: "X-API-Key",
|
||||||
|
@@ -11,6 +11,8 @@ import { permissionDALFactory } from "@app/ee/services/permission/permission-dal
|
|||||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
|
import { scimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||||
|
import { scimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||||
import { secretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
|
import { secretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
|
||||||
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
|
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
|
||||||
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
@@ -155,6 +157,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const auditLogDAL = auditLogDALFactory(db);
|
const auditLogDAL = auditLogDALFactory(db);
|
||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
|
const scimDAL = scimDALFactory(db);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDAL = permissionDALFactory(db);
|
const permissionDAL = permissionDALFactory(db);
|
||||||
@@ -188,6 +191,7 @@ export const registerRoutes = async (
|
|||||||
trustedIpDAL,
|
trustedIpDAL,
|
||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
const auditLogQueue = auditLogQueueServiceFactory({
|
const auditLogQueue = auditLogQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
@@ -210,6 +214,16 @@ export const registerRoutes = async (
|
|||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
const scimService = scimServiceFactory({
|
||||||
|
licenseService,
|
||||||
|
scimDAL,
|
||||||
|
userDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
permissionService,
|
||||||
|
smtpService
|
||||||
|
});
|
||||||
|
|
||||||
const telemetryService = telemetryServiceFactory();
|
const telemetryService = telemetryServiceFactory();
|
||||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||||
@@ -486,6 +500,7 @@ export const registerRoutes = async (
|
|||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
license: licenseService,
|
license: licenseService,
|
||||||
trustedIp: trustedIpService,
|
trustedIp: trustedIpService,
|
||||||
|
scim: scimService,
|
||||||
secretBlindIndex: secretBlindIndexService,
|
secretBlindIndex: secretBlindIndexService,
|
||||||
telemetry: telemetryService
|
telemetry: telemetryService
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
@@ -72,7 +72,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
user: UsersSchema,
|
user: UsersSchema,
|
||||||
token: z.string()
|
organization: OrganizationsSchema,
|
||||||
|
token: z.string(),
|
||||||
|
new: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -81,7 +83,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
if (serverCfg.initialized)
|
if (serverCfg.initialized)
|
||||||
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
|
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
|
||||||
const { user, token } = await server.services.superAdmin.adminSignUp({
|
const { user, token, organization } = await server.services.superAdmin.adminSignUp({
|
||||||
...req.body,
|
...req.body,
|
||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userAgent: req.headers["user-agent"] || ""
|
userAgent: req.headers["user-agent"] || ""
|
||||||
@@ -107,7 +109,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
return {
|
return {
|
||||||
message: "Successfully set up admin account",
|
message: "Successfully set up admin account",
|
||||||
user: user.user,
|
user: user.user,
|
||||||
token: token.access
|
token: token.access,
|
||||||
|
organization,
|
||||||
|
new: "123"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -5,6 +5,7 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid
|
|||||||
url: "/token/renew",
|
url: "/token/renew",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Renew access token",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
accessToken: z.string().trim()
|
accessToken: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
@@ -9,8 +9,14 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/",
|
url: "/",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
organizationId: z.string().trim(),
|
organizationId: z.string().trim(),
|
||||||
@@ -50,8 +56,14 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:identityId",
|
url: "/:identityId",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string()
|
identityId: z.string()
|
||||||
}),
|
}),
|
||||||
@@ -93,8 +105,14 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:identityId",
|
url: "/:identityId",
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string()
|
identityId: z.string()
|
||||||
}),
|
}),
|
||||||
|
@@ -24,6 +24,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/universal-auth/login",
|
url: "/universal-auth/login",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Login with Universal Auth",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
clientId: z.string().trim(),
|
clientId: z.string().trim(),
|
||||||
clientSecret: z.string().trim()
|
clientSecret: z.string().trim()
|
||||||
@@ -67,6 +68,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Attach Universal Auth configuration onto identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim()
|
identityId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -141,6 +148,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update Universal Auth configuration on identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string()
|
identityId: z.string()
|
||||||
}),
|
}),
|
||||||
@@ -209,6 +222,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Retrieve Universal Auth configuration on identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string()
|
identityId: z.string()
|
||||||
}),
|
}),
|
||||||
@@ -246,6 +265,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create Universal Auth Client Secret for identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string()
|
identityId: z.string()
|
||||||
}),
|
}),
|
||||||
@@ -291,6 +316,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "List Universal Auth Client Secrets for identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string()
|
identityId: z.string()
|
||||||
}),
|
}),
|
||||||
@@ -327,6 +358,12 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Revoke Universal Auth Client Secrets for identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string(),
|
identityId: z.string(),
|
||||||
clientSecretId: z.string()
|
clientSecretId: z.string()
|
||||||
|
@@ -93,7 +93,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
.trim()
|
.trim()
|
||||||
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
|
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
|
||||||
.optional(),
|
.optional(),
|
||||||
authEnforced: z.boolean().optional()
|
authEnforced: z.boolean().optional(),
|
||||||
|
scimEnabled: z.boolean().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -10,6 +10,13 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/:workspaceId/environments",
|
url: "/:workspaceId/environments",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create environment",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -58,6 +65,13 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/:workspaceId/environments/:id",
|
url: "/:workspaceId/environments/:id",
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update environment",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
id: z.string().trim()
|
id: z.string().trim()
|
||||||
@@ -114,6 +128,13 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/:workspaceId/environments/:id",
|
url: "/:workspaceId/environments/:id",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete environment",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
id: z.string().trim()
|
id: z.string().trim()
|
||||||
|
@@ -10,6 +10,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
url: "/:workspaceId/memberships",
|
url: "/:workspaceId/memberships",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return project user memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -96,6 +103,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
url: "/:workspaceId/memberships/:membershipId",
|
url: "/:workspaceId/memberships/:membershipId",
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update project user membership",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
membershipId: z.string().trim()
|
membershipId: z.string().trim()
|
||||||
@@ -141,6 +155,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
url: "/:workspaceId/memberships/:membershipId",
|
url: "/:workspaceId/memberships/:membershipId",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete project user membership",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
membershipId: z.string().trim()
|
membershipId: z.string().trim()
|
||||||
|
@@ -11,6 +11,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "/",
|
url: "/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create folders",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
body: z.object({
|
body: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
@@ -57,6 +64,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "/:folderId",
|
url: "/:folderId",
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update folder",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
// old way this was name
|
// old way this was name
|
||||||
folderId: z.string()
|
folderId: z.string()
|
||||||
@@ -109,6 +123,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "/:folderId",
|
url: "/:folderId",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete a folder",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
folderId: z.string()
|
folderId: z.string()
|
||||||
}),
|
}),
|
||||||
@@ -158,6 +179,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "/",
|
url: "/",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Get folders",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
|
@@ -11,6 +11,13 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "/",
|
url: "/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create secret imports",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
body: z.object({
|
body: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
@@ -65,6 +72,13 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "/:secretImportId",
|
url: "/:secretImportId",
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update secret imports",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretImportId: z.string().trim()
|
secretImportId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -128,6 +142,13 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "/:secretImportId",
|
url: "/:secretImportId",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete secret imports",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretImportId: z.string().trim()
|
secretImportId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -181,6 +202,13 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "/",
|
url: "/",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Get secret imports",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
|
@@ -10,6 +10,13 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/:orgId/identity-memberships",
|
url: "/:orgId/identity-memberships",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return organization identity memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
orgId: z.string().trim()
|
orgId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
@@ -46,6 +46,12 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
url: "/:projectId/identity-memberships/:identityId",
|
url: "/:projectId/identity-memberships/:identityId",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update project identity memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
identityId: z.string().trim()
|
identityId: z.string().trim()
|
||||||
@@ -77,6 +83,12 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
url: "/:projectId/identity-memberships/:identityId",
|
url: "/:projectId/identity-memberships/:identityId",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete project identity memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim(),
|
projectId: z.string().trim(),
|
||||||
identityId: z.string().trim()
|
identityId: z.string().trim()
|
||||||
@@ -104,6 +116,12 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
url: "/:projectId/identity-memberships",
|
url: "/:projectId/identity-memberships",
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return project identity memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
@@ -9,6 +9,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:organizationId/memberships",
|
url: "/:organizationId/memberships",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return organization user memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
organizationId: z.string().trim()
|
organizationId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -46,6 +53,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:organizationId/workspaces",
|
url: "/:organizationId/workspaces",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return projects in organization that user is part of",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
organizationId: z.string().trim()
|
organizationId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -84,6 +98,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:organizationId/memberships/:membershipId",
|
url: "/:organizationId/memberships/:membershipId",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update organization user memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
|
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
role: z.string().trim()
|
role: z.string().trim()
|
||||||
@@ -113,6 +134,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:organizationId/memberships/:membershipId",
|
url: "/:organizationId/memberships/:membershipId",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete organization user memberships",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
|
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -10,6 +10,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/:workspaceId/encrypted-key",
|
url: "/:workspaceId/encrypted-key",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return encrypted project key",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
workspaceId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
|
@@ -21,6 +21,12 @@ export const registerServiceTokenRouter = async (server: FastifyZodProvider) =>
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
onRequest: verifyAuth([AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.SERVICE_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return Infisical Token data",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
response: {
|
response: {
|
||||||
200: ServiceTokensSchema.merge(
|
200: ServiceTokensSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
@@ -71,6 +71,12 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/me/organizations",
|
url: "/me/organizations",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Return organizations that current user is part of",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.array()
|
organizations: OrganizationsSchema.array()
|
||||||
@@ -179,13 +185,19 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/me",
|
url: "/me",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Retrieve the current user on the request",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
|
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await server.services.user.getMe(req.permission.id);
|
const user = await server.services.user.getMe(req.permission.id);
|
||||||
return { user };
|
return { user };
|
||||||
|
@@ -38,6 +38,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/raw",
|
url: "/raw",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "List secrets",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
workspaceId: z.string().trim().optional(),
|
workspaceId: z.string().trim().optional(),
|
||||||
environment: z.string().trim().optional(),
|
environment: z.string().trim().optional(),
|
||||||
@@ -121,6 +128,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/raw/:secretName",
|
url: "/raw/:secretName",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Get a secret by name",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim()
|
secretName: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -204,6 +218,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/raw/:secretName",
|
url: "/raw/:secretName",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create secret",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim()
|
secretName: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -274,6 +295,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/raw/:secretName",
|
url: "/raw/:secretName",
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update secret",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim()
|
secretName: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -341,6 +369,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/raw/:secretName",
|
url: "/raw/:secretName",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete secret",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
apiKeyAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim()
|
secretName: z.string().trim()
|
||||||
}),
|
}),
|
||||||
@@ -1057,7 +1092,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretName: z.string().trim(),
|
secretName: z.string().trim(),
|
||||||
type: z.nativeEnum(SecretType).default(SecretType.Shared),
|
|
||||||
secretKeyCiphertext: z.string().trim(),
|
secretKeyCiphertext: z.string().trim(),
|
||||||
secretKeyIV: z.string().trim(),
|
secretKeyIV: z.string().trim(),
|
||||||
secretKeyTag: z.string().trim(),
|
secretKeyTag: z.string().trim(),
|
||||||
@@ -1104,7 +1138,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
projectId,
|
projectId,
|
||||||
policy,
|
policy,
|
||||||
data: {
|
data: {
|
||||||
[CommitType.Create]: inputSecrets.filter(({ type }) => type === "shared")
|
[CommitType.Create]: inputSecrets
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -156,7 +156,19 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
const { user, accessToken, refreshToken } = await server.services.signup.completeAccountInvite({
|
const { user, accessToken, refreshToken } = await server.services.signup.completeAccountInvite({
|
||||||
...req.body,
|
...req.body,
|
||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userAgent
|
userAgent,
|
||||||
|
authorization: req.headers.authorization as string
|
||||||
|
});
|
||||||
|
|
||||||
|
void server.services.telemetry.sendLoopsEvent(user.email, user.firstName || "", user.lastName || "");
|
||||||
|
|
||||||
|
void server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.UserSignedUp,
|
||||||
|
distinctId: user.email,
|
||||||
|
properties: {
|
||||||
|
email: user.email,
|
||||||
|
attributionSource: "Team Invite"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
void res.setCookie("jid", refreshToken, {
|
void res.setCookie("jid", refreshToken, {
|
||||||
|
@@ -212,13 +212,16 @@ export const authSignupServiceFactory = ({
|
|||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag
|
encryptedPrivateKeyTag,
|
||||||
|
authorization
|
||||||
}: TCompleteAccountInviteDTO) => {
|
}: TCompleteAccountInviteDTO) => {
|
||||||
const user = await userDAL.findUserByEmail(email);
|
const user = await userDAL.findUserByEmail(email);
|
||||||
if (!user || (user && user.isAccepted)) {
|
if (!user || (user && user.isAccepted)) {
|
||||||
throw new Error("Failed to complete account for complete user");
|
throw new Error("Failed to complete account for complete user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateSignUpAuthorization(authorization, user.id);
|
||||||
|
|
||||||
const [orgMembership] = await orgDAL.findMembership({
|
const [orgMembership] = await orgDAL.findMembership({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: OrgMembershipStatus.Invited
|
status: OrgMembershipStatus.Invited
|
||||||
|
@@ -34,4 +34,5 @@ export type TCompleteAccountInviteDTO = {
|
|||||||
verifier: string;
|
verifier: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
authorization: string;
|
||||||
};
|
};
|
||||||
|
@@ -17,21 +17,24 @@ export enum AuthTokenType {
|
|||||||
API_KEY = "apiKey",
|
API_KEY = "apiKey",
|
||||||
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||||
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
|
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
|
||||||
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
|
||||||
|
SCIM_TOKEN = "scimToken"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthMode {
|
export enum AuthMode {
|
||||||
JWT = "jwt",
|
JWT = "jwt",
|
||||||
SERVICE_TOKEN = "serviceToken",
|
SERVICE_TOKEN = "serviceToken",
|
||||||
API_KEY = "apiKey",
|
API_KEY = "apiKey",
|
||||||
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
|
||||||
|
SCIM_TOKEN = "scimToken"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActorType { // would extend to AWS, Azure, ...
|
export enum ActorType { // would extend to AWS, Azure, ...
|
||||||
USER = "user", // userIdentity
|
USER = "user", // userIdentity
|
||||||
SERVICE = "service",
|
SERVICE = "service",
|
||||||
IDENTITY = "identity",
|
IDENTITY = "identity",
|
||||||
Machine = "machine"
|
Machine = "machine",
|
||||||
|
SCIM_CLIENT = "scimClient"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthModeJwtTokenPayload = {
|
export type AuthModeJwtTokenPayload = {
|
||||||
|
@@ -35,12 +35,12 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ttl check
|
// ttl check
|
||||||
if (accessTokenTTL > 0) {
|
if (Number(accessTokenTTL) > 0) {
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
if (accessTokenLastRenewedAt) {
|
if (accessTokenLastRenewedAt) {
|
||||||
// access token has been renewed
|
// access token has been renewed
|
||||||
const accessTokenRenewed = new Date(accessTokenLastRenewedAt);
|
const accessTokenRenewed = new Date(accessTokenLastRenewedAt);
|
||||||
const ttlInMilliseconds = accessTokenTTL * 1000;
|
const ttlInMilliseconds = Number(accessTokenTTL) * 1000;
|
||||||
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
|
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
if (currentDate > expirationDate)
|
if (currentDate > expirationDate)
|
||||||
@@ -50,7 +50,7 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
} else {
|
} else {
|
||||||
// access token has never been renewed
|
// access token has never been renewed
|
||||||
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||||
const ttlInMilliseconds = accessTokenTTL * 1000;
|
const ttlInMilliseconds = Number(accessTokenTTL) * 1000;
|
||||||
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
if (currentDate > expirationDate)
|
if (currentDate > expirationDate)
|
||||||
@@ -61,9 +61,9 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// max ttl checks
|
// max ttl checks
|
||||||
if (accessTokenMaxTTL > 0) {
|
if (Number(accessTokenMaxTTL) > 0) {
|
||||||
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||||
const ttlInMilliseconds = accessTokenMaxTTL * 1000;
|
const ttlInMilliseconds = Number(accessTokenMaxTTL) * 1000;
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
message: "Failed to renew MI access token due to Max TTL expiration"
|
message: "Failed to renew MI access token due to Max TTL expiration"
|
||||||
});
|
});
|
||||||
|
|
||||||
const extendToDate = new Date(currentDate.getTime() + accessTokenTTL);
|
const extendToDate = new Date(currentDate.getTime() + Number(accessTokenTTL));
|
||||||
if (extendToDate > expirationDate)
|
if (extendToDate > expirationDate)
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
message: "Failed to renew MI access token past its Max TTL expiration"
|
message: "Failed to renew MI access token past its Max TTL expiration"
|
||||||
|
@@ -69,9 +69,9 @@ export const identityUaServiceFactory = ({
|
|||||||
if (!validClientSecretInfo) throw new UnauthorizedError();
|
if (!validClientSecretInfo) throw new UnauthorizedError();
|
||||||
|
|
||||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||||
if (clientSecretTTL > 0) {
|
if (Number(clientSecretTTL) > 0) {
|
||||||
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
||||||
const ttlInMilliseconds = clientSecretTTL * 1000;
|
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
@@ -124,7 +124,10 @@ export const identityUaServiceFactory = ({
|
|||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
{
|
||||||
expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL
|
expiresIn:
|
||||||
|
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
||||||
|
? undefined
|
||||||
|
: Number(identityAccessToken.accessTokenMaxTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -165,7 +165,14 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
.where(buildFindFilter(filter))
|
.where(buildFindFilter(filter))
|
||||||
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||||
.select(selectAllTableCols(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users));
|
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.OrgMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
|
db.ref("scimEnabled").withSchema(TableName.Organization)
|
||||||
|
);
|
||||||
if (limit) void query.limit(limit);
|
if (limit) void query.limit(limit);
|
||||||
if (offset) void query.offset(offset);
|
if (offset) void query.offset(offset);
|
||||||
if (sort) {
|
if (sort) {
|
||||||
|
41
backend/src/services/org/org-fns.ts
Normal file
41
backend/src/services/org/org-fns.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
|
||||||
|
type TDeleteOrgMembership = {
|
||||||
|
orgMembershipId: string;
|
||||||
|
orgId: string;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteOrgMembership = async ({
|
||||||
|
orgMembershipId,
|
||||||
|
orgId,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
}: TDeleteOrgMembership) => {
|
||||||
|
const membership = await orgDAL.transaction(async (tx) => {
|
||||||
|
// delete org membership
|
||||||
|
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
|
||||||
|
|
||||||
|
const projects = await projectDAL.find({ orgId }, { tx });
|
||||||
|
|
||||||
|
// delete associated project memberships
|
||||||
|
await projectMembershipDAL.delete(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
projectId: projects.map((project) => project.id)
|
||||||
|
},
|
||||||
|
userId: orgMembership.userId as string
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return orgMembership;
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
};
|
@@ -126,16 +126,32 @@ export const orgServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
orgId,
|
orgId,
|
||||||
data: { name, slug, authEnforced }
|
data: { name, slug, authEnforced, scimEnabled }
|
||||||
}: TUpdateOrgDTO) => {
|
}: TUpdateOrgDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
if (authEnforced !== undefined) {
|
if (authEnforced !== undefined) {
|
||||||
|
if (!plan?.samlSSO)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to enforce/un-enforce SAML SSO due to plan restriction. Upgrade plan to enforce/un-enforce SAML SSO."
|
||||||
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authEnforced) {
|
if (scimEnabled !== undefined) {
|
||||||
|
if (!plan?.scim)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning."
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authEnforced || scimEnabled) {
|
||||||
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
||||||
if (!samlCfg)
|
if (!samlCfg)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -147,7 +163,8 @@ export const orgServiceFactory = ({
|
|||||||
const org = await orgDAL.updateById(orgId, {
|
const org = await orgDAL.updateById(orgId, {
|
||||||
name,
|
name,
|
||||||
slug: slug ? slugify(slug) : undefined,
|
slug: slug ? slugify(slug) : undefined,
|
||||||
authEnforced
|
authEnforced,
|
||||||
|
scimEnabled
|
||||||
});
|
});
|
||||||
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
||||||
return org;
|
return org;
|
||||||
|
@@ -38,5 +38,5 @@ export type TFindAllWorkspacesDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateOrgDTO = {
|
export type TUpdateOrgDTO = {
|
||||||
data: Partial<{ name: string; slug: string; authEnforced: boolean }>;
|
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
@@ -41,13 +41,12 @@ export const fnSecretsFromImports = async ({
|
|||||||
environment: importEnv.slug,
|
environment: importEnv.slug,
|
||||||
environmentInfo: importEnv,
|
environmentInfo: importEnv,
|
||||||
folderId: importedFolders?.[i]?.id,
|
folderId: importedFolders?.[i]?.id,
|
||||||
secrets: importedFolders?.[i]?.id
|
// this will ensure for cases when secrets are empty. Could be due to missing folder for a path or when emtpy secrets inside a given path
|
||||||
? importedSecsGroupByFolderId[importedFolders?.[i]?.id as string].map((item) => ({
|
secrets: (importedSecsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
environment: importEnv.slug,
|
environment: importEnv.slug,
|
||||||
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
}))
|
}))
|
||||||
: []
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@@ -57,6 +57,12 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
type: el.type,
|
type: el.type,
|
||||||
...(el.type === SecretType.Personal ? { userId } : {})
|
...(el.type === SecretType.Personal ? { userId } : {})
|
||||||
});
|
});
|
||||||
|
if (el.type === SecretType.Shared) {
|
||||||
|
void bd.orWhere({
|
||||||
|
secretBlindIndex: el.blindIndex,
|
||||||
|
type: SecretType.Personal
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.delete()
|
.delete()
|
||||||
|
@@ -581,7 +581,7 @@ export const secretServiceFactory = ({
|
|||||||
secretType = SecretType.Shared;
|
secretType = SecretType.Shared;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = await (typeof version === undefined
|
const secret = await (version === undefined
|
||||||
? secretDAL.findOne({
|
? secretDAL.findOne({
|
||||||
folderId,
|
folderId,
|
||||||
type: secretType,
|
type: secretType,
|
||||||
|
@@ -25,7 +25,8 @@ export enum SmtpTemplates {
|
|||||||
OrgInvite = "organizationInvitation.handlebars",
|
OrgInvite = "organizationInvitation.handlebars",
|
||||||
ResetPassword = "passwordReset.handlebars",
|
ResetPassword = "passwordReset.handlebars",
|
||||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||||
WorkspaceInvite = "workspaceInvitation.handlebars"
|
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||||
|
ScimUserProvisioned = "scimUserProvisioned.handlebars"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
export enum SmtpHost {
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Organization Invitation</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Join your organization on Infisical</h2>
|
||||||
|
<p>You've been invited to join the Infisical organization — {{organizationName}}</p>
|
||||||
|
<a href="{{callback_url}}">Join now</a>
|
||||||
|
<h3>What is Infisical?</h3>
|
||||||
|
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -96,7 +96,11 @@ export const superAdminServiceFactory = ({
|
|||||||
|
|
||||||
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
|
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
|
||||||
|
|
||||||
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, initialOrganizationName);
|
const organization = await orgService.createOrganization(
|
||||||
|
userInfo.user.id,
|
||||||
|
userInfo.user.email,
|
||||||
|
initialOrganizationName
|
||||||
|
);
|
||||||
|
|
||||||
await updateServerCfg({ initialized: true });
|
await updateServerCfg({ initialized: true });
|
||||||
const token = await authService.generateUserTokens({
|
const token = await authService.generateUserTokens({
|
||||||
@@ -106,7 +110,7 @@ export const superAdminServiceFactory = ({
|
|||||||
organizationId: undefined
|
organizationId: undefined
|
||||||
});
|
});
|
||||||
// TODO(akhilmhdh-pg): telemetry service
|
// TODO(akhilmhdh-pg): telemetry service
|
||||||
return { token, user: userInfo };
|
return { token, user: userInfo, organization };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -4,12 +4,16 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "test"
|
||||||
|
},
|
||||||
environment: "./e2e-test/vitest-environment-knex.ts",
|
environment: "./e2e-test/vitest-environment-knex.ts",
|
||||||
include: ["./e2e-test/**/*.spec.ts"],
|
include: ["./e2e-test/**/*.spec.ts"],
|
||||||
poolOptions: {
|
poolOptions: {
|
||||||
threads: {
|
threads: {
|
||||||
singleThread: true,
|
singleThread: true,
|
||||||
useAtomics: true
|
useAtomics: true,
|
||||||
|
isolate: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
version: '3'
|
version: "3.9"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
@@ -10,36 +10,84 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- frontend
|
|
||||||
- backend
|
- backend
|
||||||
networks:
|
- frontend
|
||||||
- infisical-dev
|
|
||||||
|
|
||||||
backend:
|
db:
|
||||||
container_name: infisical-dev-backend
|
image: postgres:14-alpine
|
||||||
restart: unless-stopped
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: infisical
|
||||||
|
POSTGRES_USER: infisical
|
||||||
|
POSTGRES_DB: infisical
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
container_name: infisical-dev-redis
|
||||||
|
environment:
|
||||||
|
- ALLOW_EMPTY_PASSWORD=yes
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
|
||||||
|
redis-commander:
|
||||||
|
container_name: infisical-dev-redis-commander
|
||||||
|
image: rediscommander/redis-commander
|
||||||
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongo
|
|
||||||
- smtp-server
|
|
||||||
- redis
|
- redis
|
||||||
|
environment:
|
||||||
|
- REDIS_HOSTS=local:redis:6379
|
||||||
|
ports:
|
||||||
|
- "8085:8081"
|
||||||
|
|
||||||
|
db-test:
|
||||||
|
profiles: ["test"]
|
||||||
|
image: postgres:14-alpine
|
||||||
|
ports:
|
||||||
|
- "5430:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: infisical
|
||||||
|
POSTGRES_USER: infisical
|
||||||
|
POSTGRES_DB: infisical-test
|
||||||
|
|
||||||
|
db-migration:
|
||||||
|
container_name: infisical-db-migration
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.dev
|
||||||
volumes:
|
|
||||||
- ./backend/src:/app/src
|
|
||||||
- ./backend/nodemon.json:/app/nodemon.json
|
|
||||||
- /app/node_modules
|
|
||||||
- ./backend/api-documentation.json:/app/api-documentation.json
|
|
||||||
- ./backend/swagger.ts:/app/swagger.ts
|
|
||||||
command: npm run dev
|
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
|
||||||
|
command: npm run migration:latest
|
||||||
|
|
||||||
|
backend:
|
||||||
|
container_name: infisical-dev-api
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_started
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
db-migration:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 4000:4000
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
|
||||||
networks:
|
volumes:
|
||||||
- infisical-dev
|
- ./backend/src:/app/src
|
||||||
extra_hosts:
|
|
||||||
- "host.docker.internal:host-gateway"
|
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
container_name: infisical-dev-frontend
|
container_name: infisical-dev-frontend
|
||||||
@@ -55,81 +103,31 @@ services:
|
|||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- NEXT_PUBLIC_ENV=development
|
- NEXT_PUBLIC_ENV=development
|
||||||
- INFISICAL_TELEMETRY_ENABLED=${TELEMETRY_ENABLED}
|
- INFISICAL_TELEMETRY_ENABLED=false
|
||||||
networks:
|
|
||||||
- infisical-dev
|
|
||||||
|
|
||||||
mongo:
|
pgadmin:
|
||||||
image: mongo
|
image: dpage/pgadmin4
|
||||||
container_name: infisical-dev-mongo
|
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env
|
|
||||||
environment:
|
environment:
|
||||||
- MONGO_INITDB_ROOT_USERNAME=root
|
PGADMIN_DEFAULT_EMAIL: admin@example.com
|
||||||
- MONGO_INITDB_ROOT_PASSWORD=example
|
PGADMIN_DEFAULT_PASSWORD: pass
|
||||||
volumes:
|
|
||||||
- mongo-data:/data/db
|
|
||||||
networks:
|
|
||||||
- infisical-dev
|
|
||||||
|
|
||||||
mongo-express:
|
|
||||||
container_name: infisical-dev-mongo-express
|
|
||||||
image: mongo-express
|
|
||||||
restart: always
|
|
||||||
depends_on:
|
|
||||||
- mongo
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
- ME_CONFIG_MONGODB_ADMINUSERNAME=root
|
|
||||||
- ME_CONFIG_MONGODB_ADMINPASSWORD=example
|
|
||||||
- ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/
|
|
||||||
ports:
|
ports:
|
||||||
- 8081:8081
|
- 5050:80
|
||||||
networks:
|
depends_on:
|
||||||
- infisical-dev
|
- db
|
||||||
|
|
||||||
smtp-server:
|
smtp-server:
|
||||||
container_name: infisical-dev-smtp-server
|
container_name: infisical-dev-smtp-server
|
||||||
image: lytrax/mailhog:latest # https://github.com/mailhog/MailHog/issues/353#issuecomment-821137362
|
image: lytrax/mailhog:latest # https://github.com/mailhog/MailHog/issues/353#issuecomment-821137362
|
||||||
restart: always
|
restart: always
|
||||||
logging:
|
logging:
|
||||||
driver: 'none' # disable saving logs
|
driver: "none" # disable saving logs
|
||||||
ports:
|
ports:
|
||||||
- 1025:1025 # SMTP server
|
- 1025:1025 # SMTP server
|
||||||
- 8025:8025 # Web UI
|
- 8025:8025 # Web UI
|
||||||
networks:
|
|
||||||
- infisical-dev
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis
|
|
||||||
container_name: infisical-dev-redis
|
|
||||||
environment:
|
|
||||||
- ALLOW_EMPTY_PASSWORD=yes
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- infisical-dev
|
|
||||||
|
|
||||||
redis-commander:
|
|
||||||
container_name: infisical-dev-redis-commander
|
|
||||||
image: rediscommander/redis-commander
|
|
||||||
restart: always
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
environment:
|
|
||||||
- REDIS_HOSTS=local:redis:6379
|
|
||||||
ports:
|
|
||||||
- "8085:8081"
|
|
||||||
networks:
|
|
||||||
- infisical-dev
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mongo-data:
|
postgres-data:
|
||||||
driver: local
|
driver: local
|
||||||
redis_data:
|
redis_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
networks:
|
|
||||||
infisical-dev:
|
|
||||||
|
@@ -1,143 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
nginx:
|
|
||||||
container_name: infisical-dev-nginx
|
|
||||||
image: nginx
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- 8080:80
|
|
||||||
volumes:
|
|
||||||
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
- frontend
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:14-alpine
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: infisical
|
|
||||||
POSTGRES_USER: infisical
|
|
||||||
POSTGRES_DB: infisical
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis
|
|
||||||
container_name: infisical-dev-redis
|
|
||||||
environment:
|
|
||||||
- ALLOW_EMPTY_PASSWORD=yes
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
|
|
||||||
redis-commander:
|
|
||||||
container_name: infisical-dev-redis-commander
|
|
||||||
image: rediscommander/redis-commander
|
|
||||||
restart: always
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
environment:
|
|
||||||
- REDIS_HOSTS=local:redis:6379
|
|
||||||
ports:
|
|
||||||
- "8085:8081"
|
|
||||||
|
|
||||||
db-test:
|
|
||||||
profiles: ["test"]
|
|
||||||
image: postgres:14-alpine
|
|
||||||
ports:
|
|
||||||
- "5430:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: infisical
|
|
||||||
POSTGRES_USER: infisical
|
|
||||||
POSTGRES_DB: infisical-test
|
|
||||||
|
|
||||||
backend:
|
|
||||||
container_name: infisical-dev-api
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile.dev
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
|
|
||||||
volumes:
|
|
||||||
- ./backend/src:/app/src
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
container_name: infisical-dev-frontend
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile.dev
|
|
||||||
volumes:
|
|
||||||
- ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files
|
|
||||||
- ./frontend/public:/app/public
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
- NEXT_PUBLIC_ENV=development
|
|
||||||
- INFISICAL_TELEMETRY_ENABLED=false
|
|
||||||
|
|
||||||
pgadmin:
|
|
||||||
image: dpage/pgadmin4
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
PGADMIN_DEFAULT_EMAIL: admin@example.com
|
|
||||||
PGADMIN_DEFAULT_PASSWORD: pass
|
|
||||||
ports:
|
|
||||||
- 5050:80
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
|
|
||||||
smtp-server:
|
|
||||||
container_name: infisical-dev-smtp-server
|
|
||||||
image: lytrax/mailhog:latest # https://github.com/mailhog/MailHog/issues/353#issuecomment-821137362
|
|
||||||
restart: always
|
|
||||||
logging:
|
|
||||||
driver: "none" # disable saving logs
|
|
||||||
ports:
|
|
||||||
- 1025:1025 # SMTP server
|
|
||||||
- 8025:8025 # Web UI
|
|
||||||
|
|
||||||
# mongo:
|
|
||||||
# image: mongo
|
|
||||||
# container_name: infisical-dev-mongo
|
|
||||||
# restart: always
|
|
||||||
# env_file: .env
|
|
||||||
# environment:
|
|
||||||
# - MONGO_INITDB_ROOT_USERNAME=root
|
|
||||||
# - MONGO_INITDB_ROOT_PASSWORD=example
|
|
||||||
# volumes:
|
|
||||||
# - mongo-data:/data/db
|
|
||||||
# ports:
|
|
||||||
# - 27017:27017
|
|
||||||
#
|
|
||||||
# mongo-express:
|
|
||||||
# container_name: infisical-dev-mongo-express
|
|
||||||
# image: mongo-express
|
|
||||||
# restart: always
|
|
||||||
# depends_on:
|
|
||||||
# - mongo
|
|
||||||
# env_file: .env
|
|
||||||
# environment:
|
|
||||||
# - ME_CONFIG_MONGODB_ADMINUSERNAME=root
|
|
||||||
# - ME_CONFIG_MONGODB_ADMINPASSWORD=example
|
|
||||||
# - ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/
|
|
||||||
# ports:
|
|
||||||
# - 8081:8081
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
driver: local
|
|
||||||
redis_data:
|
|
||||||
driver: local
|
|
||||||
mongo-data:
|
|
||||||
driver: local
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user