mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-22 04:21:34 +00:00
Compare commits
438 Commits
danie/fix-
...
snyk-upgra
Author | SHA1 | Date | |
---|---|---|---|
0fac5b87df | |||
fdd79c0568 | |||
4ef8abdb00 | |||
46f0fb7a41 | |||
2142f5736c | |||
ce764d70ad | |||
c2d0ddb2fc | |||
7ba9588509 | |||
cddb09e031 | |||
046dc83638 | |||
320074ef6c | |||
e780ee6573 | |||
a5a881c382 | |||
200d4a5af6 | |||
07318ec54b | |||
92d237a714 | |||
6ef988fa86 | |||
70822d0d98 | |||
e91499b301 | |||
92acb4d943 | |||
76daa20d69 | |||
a231813f01 | |||
3eb2bdb191 | |||
cadf6e1157 | |||
ceb7fafc06 | |||
3063bb9982 | |||
3d82a43615 | |||
028541a18a | |||
66a631ff46 | |||
28adb8f0ac | |||
5c988c2cd5 | |||
acf8a54abb | |||
387094aa27 | |||
4251e95c15 | |||
f4386c2d93 | |||
ff4b943854 | |||
879a5ecfac | |||
a831a7d848 | |||
3138784d1a | |||
0b258e3918 | |||
d0545a01b9 | |||
d71398344f | |||
25e3cc047b | |||
17149de567 | |||
cca2fb7ff5 | |||
f1f2d62993 | |||
be49de5f34 | |||
acfa89ba8b | |||
389ec85554 | |||
2a6b0efe22 | |||
74d9a76f75 | |||
9c67d43ebe | |||
d8f3531b50 | |||
57be73c17e | |||
a10129e750 | |||
adc10cf675 | |||
49f7780e52 | |||
26482c6b0a | |||
1cf9aaeb1b | |||
fed022ed09 | |||
64fbe4161c | |||
bbe769a961 | |||
45772f0108 | |||
31cc3ece0c | |||
52cfa1ba39 | |||
d9888f9dd1 | |||
4553c6bb37 | |||
554f0cfd00 | |||
0a5112d302 | |||
bdb0ed3e5e | |||
7816d8593e | |||
816c793ae3 | |||
9f0d09f8ed | |||
2cbd2ee75f | |||
368974cf01 | |||
8be976a694 | |||
cab47d0b98 | |||
aa81711824 | |||
10fbb99a15 | |||
4657985468 | |||
68ac1d285a | |||
fe7524fca1 | |||
bf9b47ad66 | |||
8e49825e16 | |||
27b4749205 | |||
5b1f07a661 | |||
50128bbac6 | |||
debf80cfdc | |||
4ab47ca175 | |||
021413fbd9 | |||
8a39276e04 | |||
b5e64bc8b8 | |||
faa842c3d2 | |||
28b24115b7 | |||
198dc05753 | |||
178492e9bd | |||
fb9cdb591c | |||
4c5100de6b | |||
b587e6a4a1 | |||
773756d731 | |||
9efece1f01 | |||
bb6e8b1a51 | |||
0f98fc94f0 | |||
7f1963f1ac | |||
6064c393c6 | |||
0cecf05a5b | |||
dc6497f9eb | |||
e445970f36 | |||
c33741d588 | |||
5dfc84190d | |||
a1d11c0fcd | |||
863bbd420c | |||
4b37b2afba | |||
a366dbb16d | |||
423ad49490 | |||
2a4bda481d | |||
5b550a97a1 | |||
0fa0e4eb0f | |||
65e3f0ec95 | |||
c20f6e51ae | |||
cee8ead78a | |||
82fe0bb5c4 | |||
0b7efa57be | |||
9c11226b71 | |||
ae3606c9fb | |||
a0e25b8ea2 | |||
0931a17af5 | |||
c16bf2afdb | |||
04b4e80dd1 | |||
f178220c5a | |||
ed353d3263 | |||
ec6ec8813e | |||
3ea529d525 | |||
f35f10558b | |||
28287b8ed4 | |||
0f3ec51d14 | |||
75813deb81 | |||
66e57d5d11 | |||
fb2a213214 | |||
c0b11b8350 | |||
bea24d9654 | |||
a7bc62f8e4 | |||
2ef7e8f58e | |||
41d3b9314e | |||
1e9d49008b | |||
49d07a6762 | |||
9ce71371a9 | |||
c1c66da92b | |||
4121c1d573 | |||
108f3cf117 | |||
a6e263eded | |||
419916ee0c | |||
f7e6a96a02 | |||
b0356ba941 | |||
7ea5323a37 | |||
23e198d891 | |||
9f9849ccfd | |||
0c53eb8e22 | |||
9b62937db2 | |||
ebb8d632c4 | |||
43aae87fb0 | |||
3415514fde | |||
c0e0ddde76 | |||
39ae66a84f | |||
e8ec5b8b49 | |||
592271de3b | |||
5680b984cf | |||
f378d6cc2b | |||
04c12d9a75 | |||
31b5f779fb | |||
bb92cef764 | |||
6090f86b74 | |||
8c3569a047 | |||
6fa11fe637 | |||
9287eb7031 | |||
e54b261c0f | |||
60747b10b6 | |||
bf278355c4 | |||
d3d429db37 | |||
f2dcc83a56 | |||
26576b6bcd | |||
4cca82c3c8 | |||
1b82a157cc | |||
5409cffe33 | |||
45327f10b1 | |||
37645ba126 | |||
858b49d766 | |||
a3a1a0007d | |||
075f457bd1 | |||
5156971d75 | |||
8f3de3cc90 | |||
69cba4e6c7 | |||
6dcab6646c | |||
8e13eb6077 | |||
819a9b8d27 | |||
ec3cf0208c | |||
4aa5822ae2 | |||
5364480ca2 | |||
4802a36473 | |||
8333250b0b | |||
0cfab8ab6b | |||
8fd99855bd | |||
f2c36c58f9 | |||
f47fdfe386 | |||
8a11eebab8 | |||
3b1fc4b156 | |||
84cab17f5c | |||
db773864d5 | |||
b9840ceba9 | |||
729ec7866a | |||
a7140941ee | |||
34d1bbc2ed | |||
3ad0382cb0 | |||
ccc409e9cd | |||
fe21ba0e54 | |||
80a802386c | |||
aec0e86182 | |||
8e3cddc1ea | |||
3612e5834c | |||
031a2416a9 | |||
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 | |||
97d2a15d3e | |||
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 | |||
a1cc118514 | |||
c73ee49425 | |||
ee69bccb6e | |||
0ff3ddb0c8 | |||
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 | |||
0fb87ab05f | |||
2ef8781378 | |||
3f96f0a8fb | |||
da377f6fda | |||
5cf1ec2400 | |||
a1321e4749 | |||
6c1489a87b | |||
d4db01bbde | |||
39634b8aae | |||
4815ff13ee | |||
fb503756d9 | |||
069b0cd6fb | |||
ed23bd40d2 | |||
82181f078a | |||
48a97fb39d | |||
eeaee4409c | |||
a9a5e92358 | |||
8d457bb0bf | |||
5878a221f8 | |||
fdbf59cd78 | |||
d8ea26feb7 | |||
2cc2a91812 | |||
92828b5295 | |||
50c0fae557 | |||
4e2f2281f9 | |||
d5064fe75a | |||
70e083bae0 | |||
6a943e275a | |||
526dc6141b | |||
dcab9dcdda | |||
1b0591def8 | |||
4b4305bddc | |||
22d89d791c | |||
fcaff76afa | |||
ae9eb20189 | |||
3905d16a7c | |||
ecafdb0d01 | |||
3f8ce42682 | |||
3ecfb3f9d2 | |||
9011394c34 | |||
c0096ca64c | |||
8bc952388c | |||
eef29cd2d4 | |||
6ef873f3a0 | |||
fe99c12c0d | |||
8313245ae1 | |||
332b0e2cc3 | |||
8bc9a5fed6 | |||
55e75bbbef | |||
61ff732ec0 | |||
609b224ca9 | |||
c23e16105b | |||
c10f4ece51 | |||
fc7015de83 | |||
bcdb1b11bc | |||
01d850f7e8 | |||
2d1b60a520 | |||
8de2302d98 | |||
0529b50ad7 | |||
c74fe0ca73 | |||
d5f8526a84 | |||
c1aa5c840c | |||
782ae7a41d | |||
d355956daf | |||
dc146d0883 | |||
24dd79b566 | |||
410476ecb5 | |||
f1c41be7d4 | |||
f138973ac7 | |||
5b9c0438a2 | |||
11399d73dc | |||
38ed39c2f8 | |||
4e3827780f | |||
644cdf5a67 | |||
0d6ea0d69e | |||
237979a1c6 | |||
4a566cf83f | |||
654b8ab5ca | |||
ac0780266b | |||
7a253ddcc7 | |||
b65677a708 | |||
c1eb97ee53 | |||
937e48dbc5 | |||
b3d4787e21 | |||
72d46efba5 | |||
b6eb08167f | |||
582472e4cc | |||
3b3b76548b | |||
f8416ad891 | |||
00650df501 | |||
31e49672d5 | |||
9248bdf463 | |||
87c061ae9b | |||
44f087991c | |||
6ff5fb69d4 | |||
9fe2021d9f | |||
fe2f2f972e |
.env.example.env.migration.example.env.test.exampleproject-token-old-add.png
.github
values.yaml
.gitignoreDockerfile.standalone-infisicalMakefileREADME.mdworkflows
backend
.eslintignore.eslintrc.jstsconfig.jsontsup.config.jsvitest.e2e.config.ts
docker-compose.dev.ymldocker-compose.pg.ymldocker-compose.prod.ymle2e-test
package-lock.jsonpackage.jsonscripts
src
@types
db
instance.tsknexfile.ts
migrations
20240204171758_org-based-auth.ts20240208234120_scim-token.ts20240216154123_ghost_users.ts20240222201806_admin-signup-control.ts
schemas
seed-data.tsseeds
ee
routes/v1
index.tslicense-router.tsorg-role-router.tsproject-role-router.tsproject-router.tssaml-router.tsscim-router.tssecret-approval-policy-router.tssecret-approval-request-router.tssecret-rotation-provider-router.tssecret-rotation-router.tssecret-scanning-router.tssecret-version-router.tssnapshot-router.tstrusted-ip-router.ts
services
audit-log
license
permission
saml-config
scim
secret-approval-policy
secret-approval-request
secret-rotation
secret-scanning
secret-snapshot
trusted-ip
lib
main.tsqueue
server
app.tsboot-strap-check.ts
lib
plugins
routes
index.ts
v1
admin-router.tsauth-router.tsbot-router.tsidentity-access-token-router.tsidentity-router.tsidentity-ua.tsindex.tsintegration-auth-router.tsintegration-router.tsinvite-org-router.tsorganization-router.tsproject-env-router.tsproject-key-router.tsproject-membership-router.tsproject-router.tssecret-folder-router.tssecret-import-router.tssecret-tag-router.tssso-router.tswebhook-router.ts
v2
identity-org-router.tsidentity-project-router.tsindex.tsmfa-router.tsorganization-router.tsproject-membership-router.tsproject-router.tsservice-token-router.tsuser-router.ts
v3
services
auth-token
auth
auth-fns.tsauth-login-service.tsauth-login-type.tsauth-signup-service.tsauth-signup-type.tsauth-type.ts
identity-access-token
identity-project
identity-ua
identity
integration-auth
integration
org
project-bot
project-env
project-key
project-membership
project-role
project
secret-blind-index
secret-folder
secret-import
secret-tag
secret
service-token
smtp
super-admin
telemetry
user
webhook
docs
api-reference/endpoints
environments
folders
identities
organizations
secret-imports
service-tokens
workspaces
create-workspace.mdxdelete-identity-membership.mdxdelete-membership.mdxdelete-workspace.mdxget-workspace.mdxinvite-member-to-workspace.mdxlist-identity-memberships.mdxmemberships.mdxremove-member-from-workspace.mdxrollback-snapshot.mdxupdate-identity-membership.mdxupdate-membership.mdxupdate-workspace.mdx
cli
contributing/platform/backend
documentation
guides
platform
images
guides/agent-with-ecs
access-token-deposit.pngecs-diagram.pngfile_browser_main.pngfilebrowser_afterlogin.pngsecrets-deposit.png
integrations/jenkins
platform/scim
azure
scim-azure-config.pngscim-azure-get-started.pngscim-azure-provisioning-status.pngscim-azure-select-user-mappings.pngscim-azure-start-provisioning.pngscim-azure-user-mappings.png
jumpcloud
okta
scim-okta-app-settings.pngscim-okta-auth.pngscim-okta-config.pngscim-okta-enable-provisioning.pngscim-okta-test.png
scim-copy-token.pngscim-create-token.pngscim-enable-provisioning.pngself-hosting/applicable-to-all
integrations
mint.jsonsdks/languages
self-hosting
spec.yamlfrontend
package-lock.jsonpackage.json
src
components
basic/table
signup
utilities
v2
context
helpers
hooks/api
layouts/AppLayout
pages
views
IntegrationsPage
Login
Org
MembersPage/components
OrgMembersTab/components/OrgMembersSection
OrgMembersTable
OrgRoleTabSection/OrgRoleModifySection
NonePage
Project/MembersPage/components/MemberListTab
SecretApprovalPage/components/SecretApprovalRequest/components
SecretMainPage/components/SecretListView
SecretOverviewPage
Settings
OrgSettingsPage/components
OrgAuthTab
OrgGeneralTab
OrgNameChangeSection
OrgSlugChangeSection
PersonalSettingsPage
ProjectSettingsPage/components
admin
DashboardPage
SignUpPage
helm-charts/infisical-standalone-postgres
12
.env.example
12
.env.example
@ -3,16 +3,18 @@
|
||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||
|
||||
# Required
|
||||
DB_CONNECTION_URI=postgres://infisical:infisical@db:5432/infisical
|
||||
|
||||
# JWT
|
||||
# Required secrets to sign JWT tokens
|
||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
||||
|
||||
# MongoDB
|
||||
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
|
||||
# to the MongoDB container instance or Mongo Cloud
|
||||
# Required
|
||||
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
||||
# Postgres creds
|
||||
POSTGRES_PASSWORD=infisical
|
||||
POSTGRES_USER=infisical
|
||||
POSTGRES_DB=infisical
|
||||
|
||||
# Redis
|
||||
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
|
8
.github/values.yaml
vendored
8
.github/values.yaml
vendored
@ -19,11 +19,11 @@ infisical:
|
||||
## @param backend.name Backend name
|
||||
##
|
||||
name: infisical
|
||||
replicaCount: 2
|
||||
replicaCount: 3
|
||||
image:
|
||||
repository: infisical/infisical
|
||||
tag: "latest-postgres"
|
||||
pullPolicy: IfNotPresent
|
||||
repository: infisical/staging_infisical
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
|
||||
deploymentAnnotations:
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
|
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
|
19
.github/workflows/check-fe-pull-request.yml → .github/workflows/check-fe-ts-and-lint.yml
vendored
19
.github/workflows/check-fe-pull-request.yml → .github/workflows/check-fe-ts-and-lint.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Check Frontend Pull Request
|
||||
name: Check Frontend Type and Lint check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -10,8 +10,8 @@ on:
|
||||
- "frontend/.eslintrc.js"
|
||||
|
||||
jobs:
|
||||
check-fe-pr:
|
||||
name: Check
|
||||
check-fe-ts-lint:
|
||||
name: Check Frontend Type and Lint check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
@ -25,12 +25,11 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: 📦 Install dependencies
|
||||
run: npm ci --only-production --ignore-scripts
|
||||
run: npm install
|
||||
working-directory: frontend
|
||||
# -
|
||||
# name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: frontend
|
||||
- name: 🏗️ Run build
|
||||
run: npm run build
|
||||
- name: 🏗️ Run Type check
|
||||
run: npm run type:check
|
||||
working-directory: frontend
|
||||
- name: 🏗️ Run Link check
|
||||
run: npm run lint:fix
|
||||
working-directory: frontend
|
@ -5,9 +5,14 @@ on:
|
||||
- "infisical/v*.*.*-postgres"
|
||||
|
||||
jobs:
|
||||
infisical-tests:
|
||||
name: Run tests before deployment
|
||||
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
|
||||
uses: ./.github/workflows/run-backend-tests.yml
|
||||
infisical-standalone:
|
||||
name: Build infisical standalone image postgres
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infisical-tests]
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
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
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -6,7 +6,7 @@ node_modules
|
||||
.env.gamma
|
||||
.env.prod
|
||||
.env.infisical
|
||||
|
||||
.env.migration
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
@ -63,3 +63,5 @@ yarn-error.log*
|
||||
.vscode/*
|
||||
|
||||
frontend-build
|
||||
|
||||
*.tgz
|
||||
|
@ -104,7 +104,6 @@ ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=backend-runner /app /backend
|
||||
COPY --from=backend-runner /app/dist/services/smtp/templates /backend/dist/templates
|
||||
|
||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||
|
||||
|
10
Makefile
10
Makefile
@ -5,16 +5,10 @@ push:
|
||||
docker-compose -f docker-compose.yml push
|
||||
|
||||
up-dev:
|
||||
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
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
|
||||
up-prod:
|
||||
docker-compose -f docker-compose.yml up --build
|
||||
docker-compose -f docker-compose.prod.yml up --build
|
||||
|
||||
down:
|
||||
docker-compose down
|
||||
|
34
README.md
34
README.md
@ -33,7 +33,7 @@
|
||||
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||
</a>
|
||||
<a href="https://cloudsmith.io/~infisical/repos/">
|
||||
<img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
|
||||
<img src="https://img.shields.io/badge/Downloads-6.95M-orange" alt="Cloudsmith downloads" />
|
||||
</a>
|
||||
<a href="https://infisical.com/slack">
|
||||
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||
@ -53,17 +53,19 @@ We're on a mission to make secret management more accessible to everyone, not ju
|
||||
|
||||
## Features
|
||||
|
||||
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.)
|
||||
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand
|
||||
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development
|
||||
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like GitHub, Vercel, Netlify, and more
|
||||
- [**Automatic Kubernetes deployment secret reloads**](https://infisical.com/docs/documentation/getting-started/kubernetes)
|
||||
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
||||
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery]()** to version every secret and project state
|
||||
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project
|
||||
- **Role-based Access Controls** per environment
|
||||
- [**Simple on-premise deployments** to AWS, Digital Ocean, and more](https://infisical.com/docs/self-hosting/overview)
|
||||
- [**Secret Scanning and Leak Prevention**](https://infisical.com/docs/cli/scanning-overview)
|
||||
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
|
||||
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
|
||||
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
|
||||
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
|
||||
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
|
||||
- **[Infisical Kubernetes operator](https://infisical.com/docs/documentation/getting-started/kubernetes)** to managed secrets in k8s, automatically reload deployments, and more.
|
||||
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
|
||||
- **[Self-hosting and on-prem](https://infisical.com/docs/self-hosting/overview)** to get complete control over your data.
|
||||
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
|
||||
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
|
||||
- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
|
||||
- **[Simple on-premise deployments](https://infisical.com/docs/self-hosting/overview)** to AWS, Digital Ocean, and more.
|
||||
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
|
||||
|
||||
And much more.
|
||||
|
||||
@ -82,13 +84,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
|
||||
Linux/macOS:
|
||||
|
||||
```console
|
||||
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.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:
|
||||
|
||||
```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`
|
||||
@ -115,9 +117,9 @@ Lean about Infisical's code scanning feature [here](https://infisical.com/docs/c
|
||||
|
||||
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
|
||||
|
||||
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://cal.com/vmatsiiako/infisical-demo):
|
||||
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo):
|
||||
|
||||
<a href="https://cal.com/vmatsiiako/infisical-demo"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||
<a href="[https://infisical.cal.com/vlad/infisical-demo](https://infisical.cal.com/vlad/infisical-demo)"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||
|
||||
## Security
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
vitest-environment-infisical.ts
|
||||
vitest.config.ts
|
||||
vitest.e2e.config.ts
|
||||
|
@ -21,6 +21,18 @@ module.exports = {
|
||||
tsconfigRootDir: __dirname
|
||||
},
|
||||
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: {
|
||||
"@typescript-eslint/no-empty-function": "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 { seedData1 } from "@app/db/seed-data";
|
||||
|
||||
describe("Login V1 Router", async () => {
|
||||
// eslint-disable-next-line
|
||||
const client = new jsrp.client();
|
||||
|
@ -1,6 +1,40 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project";
|
||||
|
||||
const createProjectEnvironment = async (name: string, slug: string) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
name,
|
||||
slug
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("environment");
|
||||
return payload.environment;
|
||||
};
|
||||
|
||||
const deleteProjectEnvironment = async (envId: string) => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${envId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("environment");
|
||||
return payload.environment;
|
||||
};
|
||||
|
||||
describe("Project Environment Router", async () => {
|
||||
test("Get default environments", async () => {
|
||||
const res = await testServer.inject({
|
||||
@ -31,24 +65,10 @@ describe("Project Environment Router", async () => {
|
||||
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 () => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
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(
|
||||
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||
expect(newEnvironment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: mockProjectEnv.name,
|
||||
@ -59,14 +79,15 @@ describe("Project Environment Router", async () => {
|
||||
updatedAt: expect.any(String)
|
||||
})
|
||||
);
|
||||
mockProjectEnv.id = payload.environment.id;
|
||||
await deleteProjectEnvironment(newEnvironment.id);
|
||||
});
|
||||
|
||||
test("Update environment", async () => {
|
||||
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||
const updatedName = { name: "temp#2", slug: "temp2" };
|
||||
const res = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`,
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${newEnvironment.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
@ -82,7 +103,7 @@ describe("Project Environment Router", async () => {
|
||||
expect(payload).toHaveProperty("environment");
|
||||
expect(payload.environment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
id: newEnvironment.id,
|
||||
name: updatedName.name,
|
||||
slug: updatedName.slug,
|
||||
projectId: seedData1.project.id,
|
||||
@ -91,61 +112,21 @@ describe("Project Environment Router", async () => {
|
||||
updatedAt: expect.any(String)
|
||||
})
|
||||
);
|
||||
mockProjectEnv.name = updatedName.name;
|
||||
mockProjectEnv.slug = updatedName.slug;
|
||||
await deleteProjectEnvironment(newEnvironment.id);
|
||||
});
|
||||
|
||||
test("Delete environment", async () => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("environment");
|
||||
expect(payload.environment).toEqual(
|
||||
const newEnvironment = await createProjectEnvironment(mockProjectEnv.name, mockProjectEnv.slug);
|
||||
const deletedProjectEnvironment = await deleteProjectEnvironment(newEnvironment.id);
|
||||
expect(deletedProjectEnvironment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
id: deletedProjectEnvironment.id,
|
||||
name: mockProjectEnv.name,
|
||||
slug: mockProjectEnv.slug,
|
||||
position: 1,
|
||||
position: 4,
|
||||
createdAt: 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";
|
||||
|
||||
const createFolder = async (dto: { path: string; name: string }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
name: dto.name,
|
||||
path: dto.path
|
||||
}
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
return res.json().folder;
|
||||
};
|
||||
|
||||
const deleteFolder = async (dto: { path: string; id: string }) => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/folders/${dto.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.project.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: dto.path
|
||||
}
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
return res.json().folder;
|
||||
};
|
||||
|
||||
describe("Secret Folder Router", async () => {
|
||||
test.each([
|
||||
{ name: "folder1", path: "/" }, // one in root
|
||||
@ -7,30 +42,15 @@ describe("Secret Folder Router", async () => {
|
||||
{ name: "folder2", path: "/" },
|
||||
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
|
||||
])("Create folder $name in $path", async ({ name, path }) => {
|
||||
const res = await testServer.inject({
|
||||
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");
|
||||
const createdFolder = await createFolder({ path, name });
|
||||
// check for default environments
|
||||
expect(payload).toEqual({
|
||||
folder: expect.objectContaining({
|
||||
expect(createdFolder).toEqual(
|
||||
expect.objectContaining({
|
||||
name,
|
||||
id: expect.any(String)
|
||||
})
|
||||
});
|
||||
);
|
||||
await deleteFolder({ path, id: createdFolder.id });
|
||||
});
|
||||
|
||||
test.each([
|
||||
@ -43,6 +63,8 @@ describe("Secret Folder Router", async () => {
|
||||
},
|
||||
{ path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } }
|
||||
])("Get folders $path", async ({ path, expected }) => {
|
||||
const newFolders = await Promise.all(expected.folders.map(({ name }) => createFolder({ name, path })));
|
||||
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/folders`,
|
||||
@ -59,36 +81,22 @@ describe("Secret Folder Router", async () => {
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("folders");
|
||||
expect(payload.folders.length).toBe(expected.length);
|
||||
expect(payload).toEqual({ folders: 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(payload.folders.length >= expected.folders.length).toBeTruthy();
|
||||
expect(payload).toEqual({
|
||||
folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("folder");
|
||||
expect(payload.folder).toEqual(
|
||||
await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id })));
|
||||
});
|
||||
|
||||
test("Update a deep folder", async () => {
|
||||
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
|
||||
expect(newFolder).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: "folder-updated"
|
||||
})
|
||||
);
|
||||
toBeDeleteFolderId = payload.folder.id;
|
||||
|
||||
const resUpdatedFolders = await testServer.inject({
|
||||
method: "GET",
|
||||
@ -106,14 +114,16 @@ describe("Secret Folder Router", async () => {
|
||||
expect(resUpdatedFolders.statusCode).toBe(200);
|
||||
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
|
||||
expect(updatedFolderList).toHaveProperty("folders");
|
||||
expect(updatedFolderList.folders.length).toEqual(1);
|
||||
expect(updatedFolderList.folders[0].name).toEqual("folder-updated");
|
||||
|
||||
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
|
||||
});
|
||||
|
||||
test("Delete a deep folder", async () => {
|
||||
const newFolder = await createFolder({ name: "folder-updated", path: "/level1/level2" });
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/folders/${toBeDeleteFolderId}`,
|
||||
url: `/api/v1/folders/${newFolder.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
|
@ -1,32 +1,57 @@
|
||||
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([
|
||||
{ importEnv: "dev", importPath: "/" }, // one in root
|
||||
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
||||
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
||||
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
|
||||
expect(payload.secretImport).toEqual(
|
||||
const payload = await createSecretImport(importPath, importEnv);
|
||||
expect(payload).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 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 () => {
|
||||
const createdImport1 = await createSecretImport("/", "dev");
|
||||
const createdImport2 = await createSecretImport("/", "staging");
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/secret-imports`,
|
||||
@ -58,7 +85,6 @@ describe("Secret Folder Router", async () => {
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(payload).toHaveProperty("secretImports");
|
||||
expect(payload.secretImports.length).toBe(2);
|
||||
testSecretImportId = payload.secretImports[0].id;
|
||||
expect(payload.secretImports).toEqual(
|
||||
expect.arrayContaining([
|
||||
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 () => {
|
||||
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",
|
||||
url: `/api/v1/secret-imports/${testSecretImportId}`,
|
||||
url: `/api/v1/secret-imports/${createdImport1.id}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
@ -91,8 +125,8 @@ describe("Secret Folder Router", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const payload = JSON.parse(res.payload);
|
||||
expect(updateImportRes.statusCode).toBe(200);
|
||||
const payload = JSON.parse(updateImportRes.payload);
|
||||
expect(payload).toHaveProperty("secretImport");
|
||||
// check for default environments
|
||||
expect(payload.secretImport).toEqual(
|
||||
@ -102,7 +136,7 @@ describe("Secret Folder Router", async () => {
|
||||
position: 2,
|
||||
importEnv: expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
slug: expect.any(String),
|
||||
slug: expect.stringMatching(devImportDetails.envSlug),
|
||||
id: expect.any(String)
|
||||
})
|
||||
})
|
||||
@ -124,28 +158,19 @@ describe("Secret Folder Router", async () => {
|
||||
expect(secretImportsListRes.statusCode).toBe(200);
|
||||
const secretImportList = JSON.parse(secretImportsListRes.payload);
|
||||
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 () => {
|
||||
const res = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/secret-imports/${testSecretImportId}`,
|
||||
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");
|
||||
const createdImport1 = await createSecretImport("/", "dev");
|
||||
const createdImport2 = await createSecretImport("/", "staging");
|
||||
const deletedImport = await deleteSecretImport(createdImport1.id);
|
||||
// check for default environments
|
||||
expect(payload.secretImport).toEqual(
|
||||
expect(deletedImport).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
importPath: expect.any(String),
|
||||
@ -175,5 +200,7 @@ describe("Secret Folder Router", async () => {
|
||||
expect(secretImportList).toHaveProperty("secretImports");
|
||||
expect(secretImportList.secretImports.length).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";
|
||||
import { initEnvConfig } from "@app/lib/config/env";
|
||||
// eslint-disable-next-line
|
||||
import "ts-node/register";
|
||||
|
||||
import dotenv from "dotenv";
|
||||
import jwt from "jsonwebtoken";
|
||||
import knex from "knex";
|
||||
import path from "path";
|
||||
import { 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 { 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 {
|
||||
name: "knex-env",
|
||||
transformMode: "ssr",
|
||||
async setup() {
|
||||
const logger = await initLogger();
|
||||
const cfg = initEnvConfig(logger);
|
||||
const db = knex({
|
||||
client: "pg",
|
||||
connection: process.env.DB_CONNECTION_URI,
|
||||
connection: cfg.DB_CONNECTION_URI,
|
||||
migrations: {
|
||||
directory: path.join(__dirname, "../src/db/migrations"),
|
||||
extension: "ts",
|
||||
@ -37,8 +41,6 @@ export default {
|
||||
await db.seed.run();
|
||||
const smtp = mockSmtpServer();
|
||||
const queue = mockQueue();
|
||||
const logger = await initLogger();
|
||||
const cfg = initEnvConfig(logger);
|
||||
const server = await main({ db, smtp, logger, queue });
|
||||
// @ts-expect-error type
|
||||
globalThis.testServer = server;
|
||||
@ -54,6 +56,7 @@ export default {
|
||||
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("[TEST] Error setting up environment", error);
|
||||
await db.destroy();
|
||||
throw error;
|
||||
}
|
||||
|
2565
backend/package-lock.json
generated
2565
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,12 +2,12 @@
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"main": "./dist/main.mjs",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
|
||||
"dev:docker": "nodemon",
|
||||
"build": "rimraf dist && tsup && cp -R ./src/lib/validator/disposable_emails.txt ./dist && cp -R ./src/services/smtp/templates ./dist",
|
||||
"build": "tsup",
|
||||
"start": "node dist/main.mjs",
|
||||
"type:check": "tsc --noEmit",
|
||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||
@ -24,8 +24,8 @@
|
||||
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||
"seed:run": "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"
|
||||
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@ -44,7 +44,13 @@
|
||||
"@types/pg": "^8.10.9",
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/prompt-sync": "^4.2.3",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
@ -55,45 +61,42 @@
|
||||
"prompt-sync": "^4.2.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsup": "^8.0.1",
|
||||
"tsx": "^4.4.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite-tsconfig-paths": "^4.2.2",
|
||||
"vitest": "^1.0.4"
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.485.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@fastify/cookie": "^9.2.0",
|
||||
"@fastify/cors": "^8.4.1",
|
||||
"@fastify/cors": "^8.5.0",
|
||||
"@fastify/etag": "^5.1.0",
|
||||
"@fastify/formbody": "^7.4.0",
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"@fastify/passport": "^2.4.0",
|
||||
"@fastify/rate-limit": "^9.0.0",
|
||||
"@fastify/session": "^10.7.0",
|
||||
"@fastify/swagger": "^8.12.0",
|
||||
"@fastify/swagger-ui": "^1.10.1",
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"ajv": "^8.12.0",
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1532.0",
|
||||
"axios": "^1.6.2",
|
||||
"aws-sdk": "^2.1549.0",
|
||||
"axios": "^1.6.7",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.1.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"fastify": "^4.24.3",
|
||||
"bullmq": "^5.1.6",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"ioredis": "^5.3.2",
|
||||
@ -103,10 +106,10 @@
|
||||
"knex": "^3.0.1",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"mysql2": "^3.6.5",
|
||||
"mysql2": "^3.9.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.9.7",
|
||||
"nodemailer": "^6.9.9",
|
||||
"ora": "^7.0.1",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
@ -115,12 +118,12 @@
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"posthog-node": "^3.6.0",
|
||||
"probot": "^12.3.3",
|
||||
"probot": "^13.0.0",
|
||||
"smee-client": "^2.0.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.0"
|
||||
"zod-to-json-schema": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,10 @@ import promptSync from "prompt-sync";
|
||||
const prompt = promptSync({ sigint: true });
|
||||
|
||||
const migrationName = prompt("Enter name for seedfile: ");
|
||||
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1;
|
||||
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seeds")).length || 1;
|
||||
execSync(
|
||||
`npx knex seed:make --knexfile ${path.join(
|
||||
__dirname,
|
||||
"../src/db/knexfile.ts"
|
||||
)} -x ts ${fileCounter}-${migrationName}`,
|
||||
`npx knex seed:make --knexfile ${path.join(__dirname, "../src/db/knexfile.ts")} -x ts ${
|
||||
fileCounter + 1
|
||||
}-${migrationName}`,
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
|
@ -3,13 +3,9 @@ import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
import knex from "knex";
|
||||
import { writeFileSync } from "fs";
|
||||
import promptSync from "prompt-sync";
|
||||
|
||||
const prompt = promptSync({ sigint: true });
|
||||
|
||||
dotenv.config({
|
||||
path: path.join(__dirname, "../.env"),
|
||||
debug: true
|
||||
path: path.join(__dirname, "../../.env.migration")
|
||||
});
|
||||
|
||||
const db = knex({
|
||||
@ -94,17 +90,7 @@ const main = async () => {
|
||||
.orderBy("table_name")
|
||||
).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) {
|
||||
// skip if not desired table
|
||||
if (selectedTables !== "all" && !tableNumbers.includes(i)) continue;
|
||||
|
||||
const { tableName } = tables[i];
|
||||
const columns = await db(tableName).columnInfo();
|
||||
const columnNames = Object.keys(columns);
|
||||
@ -124,16 +110,16 @@ const main = async () => {
|
||||
if (colInfo.nullable) {
|
||||
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 pascalCase = tableName
|
||||
.split("_")
|
||||
.reduce(
|
||||
(prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`,
|
||||
""
|
||||
);
|
||||
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
|
||||
|
||||
writeFileSync(
|
||||
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
||||
`// 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>>;
|
||||
`
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||
@ -51,6 +52,7 @@ declare module "fastify" {
|
||||
// used for mfa session authentication
|
||||
mfa: {
|
||||
userId: string;
|
||||
orgId?: string;
|
||||
user: TUsers;
|
||||
};
|
||||
// identity injection. depending on which kinda of token the information is filled in auth
|
||||
@ -58,6 +60,7 @@ declare module "fastify" {
|
||||
permission: {
|
||||
type: ActorType;
|
||||
id: string;
|
||||
orgId?: string;
|
||||
};
|
||||
// passport data
|
||||
passportUser: {
|
||||
@ -103,6 +106,7 @@ declare module "fastify" {
|
||||
secretRotation: TSecretRotationServiceFactory;
|
||||
snapshot: TSecretSnapshotServiceFactory;
|
||||
saml: TSamlConfigServiceFactory;
|
||||
scim: TScimServiceFactory;
|
||||
auditLog: TAuditLogServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
|
4
backend/src/@types/knex.d.ts
vendored
4
backend/src/@types/knex.d.ts
vendored
@ -83,6 +83,9 @@ import {
|
||||
TSamlConfigs,
|
||||
TSamlConfigsInsert,
|
||||
TSamlConfigsUpdate,
|
||||
TScimTokens,
|
||||
TScimTokensInsert,
|
||||
TScimTokensUpdate,
|
||||
TSecretApprovalPolicies,
|
||||
TSecretApprovalPoliciesApprovers,
|
||||
TSecretApprovalPoliciesApproversInsert,
|
||||
@ -262,6 +265,7 @@ declare module "knex/types/tables" {
|
||||
TIdentityProjectMembershipsInsert,
|
||||
TIdentityProjectMembershipsUpdate
|
||||
>;
|
||||
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||
TSecretApprovalPolicies,
|
||||
TSecretApprovalPoliciesInsert,
|
||||
|
@ -1,10 +1,18 @@
|
||||
import knex from "knex";
|
||||
|
||||
export type TDbClient = ReturnType<typeof initDbConnection>;
|
||||
export const initDbConnection = (dbConnectionUri: string) => {
|
||||
export const initDbConnection = ({ dbConnectionUri, dbRootCert }: { dbConnectionUri: string; dbRootCert?: string }) => {
|
||||
const db = knex({
|
||||
client: "pg",
|
||||
connection: dbConnectionUri
|
||||
connection: {
|
||||
connectionString: dbConnectionUri,
|
||||
ssl: dbRootCert
|
||||
? {
|
||||
rejectUnauthorized: true,
|
||||
ca: Buffer.from(dbRootCert, "base64").toString("ascii")
|
||||
}
|
||||
: false
|
||||
}
|
||||
});
|
||||
|
||||
return db;
|
||||
|
@ -5,9 +5,13 @@ import dotenv from "dotenv";
|
||||
import type { Knex } from "knex";
|
||||
import path from "path";
|
||||
|
||||
// Update with your config settings.
|
||||
// Update with your config settings. .
|
||||
dotenv.config({
|
||||
path: path.join(__dirname, "../../.env"),
|
||||
path: path.join(__dirname, "../../../.env.migration"),
|
||||
debug: true
|
||||
});
|
||||
dotenv.config({
|
||||
path: path.join(__dirname, "../../../.env"),
|
||||
debug: true
|
||||
});
|
||||
export default {
|
||||
|
25
backend/src/db/migrations/20240204171758_org-based-auth.ts
Normal file
25
backend/src/db/migrations/20240204171758_org-based-auth.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.boolean("authEnforced").defaultTo(false);
|
||||
t.index("slug");
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
|
||||
t.datetime("lastUsed");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.dropColumn("authEnforced");
|
||||
t.dropIndex("slug");
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
|
||||
t.dropColumn("lastUsed");
|
||||
});
|
||||
}
|
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");
|
||||
});
|
||||
}
|
39
backend/src/db/migrations/20240216154123_ghost_users.ts
Normal file
39
backend/src/db/migrations/20240216154123_ghost_users.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectVersion, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "isGhost");
|
||||
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
|
||||
|
||||
if (!hasGhostUserColumn) {
|
||||
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||
t.boolean("isGhost").defaultTo(false).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasProjectVersionColumn) {
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.integer("version").defaultTo(ProjectVersion.V1).notNullable();
|
||||
t.string("upgradeStatus").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "isGhost");
|
||||
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
|
||||
|
||||
if (hasGhostUserColumn) {
|
||||
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||
t.dropColumn("isGhost");
|
||||
});
|
||||
}
|
||||
|
||||
if (hasProjectVersionColumn) {
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.dropColumn("version");
|
||||
t.dropColumn("upgradeStatus");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isTablePresent = await knex.schema.hasTable(TableName.SuperAdmin);
|
||||
if (isTablePresent) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
t.string("allowedSignUpDomain");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SuperAdmin, "allowedSignUpDomain")) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
t.dropColumn("allowedSignUpDomain");
|
||||
});
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
export * from "./projects";
|
||||
export * from "./saml-configs";
|
||||
export * from "./scim-tokens";
|
||||
export * from "./secret-approval-policies";
|
||||
export * from "./secret-approval-policies-approvers";
|
||||
export * from "./secret-approval-request-secret-tags";
|
||||
|
@ -40,6 +40,7 @@ export enum TableName {
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
ScimToken = "scim_tokens",
|
||||
SecretApprovalPolicy = "secret_approval_policies",
|
||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||
SecretApprovalRequest = "secret_approval_requests",
|
||||
@ -111,6 +112,17 @@ export enum SecretType {
|
||||
Personal = "personal"
|
||||
}
|
||||
|
||||
export enum ProjectVersion {
|
||||
V1 = 1,
|
||||
V2 = 2
|
||||
}
|
||||
|
||||
export enum ProjectUpgradeStatus {
|
||||
InProgress = "IN_PROGRESS",
|
||||
// Completed -> Will be null if completed. So a completed status is not needed
|
||||
Failed = "FAILED"
|
||||
}
|
||||
|
||||
export enum IdentityAuthMethod {
|
||||
Univeral = "universal-auth"
|
||||
}
|
||||
|
@ -13,7 +13,9 @@ export const OrganizationsSchema = z.object({
|
||||
customerId: z.string().nullable().optional(),
|
||||
slug: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
authEnforced: z.boolean().default(false).nullable().optional(),
|
||||
scimEnabled: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@ -14,7 +14,9 @@ export const ProjectsSchema = z.object({
|
||||
autoCapitalization: z.boolean().default(true).nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
version: z.number().default(1),
|
||||
upgradeStatus: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||
|
@ -22,7 +22,8 @@ export const SamlConfigsSchema = z.object({
|
||||
certTag: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid()
|
||||
orgId: z.string().uuid(),
|
||||
lastUsed: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSamlConfigs = z.infer<typeof SamlConfigsSchema>;
|
||||
|
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>>;
|
@ -12,7 +12,8 @@ export const SuperAdminSchema = z.object({
|
||||
initialized: z.boolean().default(false).nullable().optional(),
|
||||
allowSignUp: z.boolean().default(true).nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
allowedSignUpDomain: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
@ -19,7 +19,8 @@ export const UsersSchema = z.object({
|
||||
mfaMethods: z.string().array().nullable().optional(),
|
||||
devices: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isGhost: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TUsers = z.infer<typeof UsersSchema>;
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/no-mutable-exports */
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import argon2, { argon2id } from "argon2";
|
||||
@ -6,17 +7,21 @@ import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import {
|
||||
decryptAsymmetric,
|
||||
// decryptAsymmetric,
|
||||
decryptSymmetric,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptAsymmetric,
|
||||
encryptSymmetric
|
||||
encryptSymmetric128BitHexKeyUTF8
|
||||
} from "@app/lib/crypto";
|
||||
|
||||
import { TUserEncryptionKeys } from "./schemas";
|
||||
import { TSecrets, TUserEncryptionKeys } from "./schemas";
|
||||
|
||||
export let userPrivateKey: string | undefined;
|
||||
export let userPublicKey: string | undefined;
|
||||
|
||||
export const seedData1 = {
|
||||
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
|
||||
email: "test@localhost.local",
|
||||
email: process.env.TEST_USER_EMAIL || "test@localhost.local",
|
||||
password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
|
||||
organization: {
|
||||
id: "180870b7-f464-4740-8ffe-9d11c9245ea7",
|
||||
@ -31,8 +36,22 @@ export const seedData1 = {
|
||||
name: "Development",
|
||||
slug: "dev"
|
||||
},
|
||||
machineIdentity: {
|
||||
id: "88fa7aed-9288-401e-a4c9-fa9430be62a0",
|
||||
name: "mac1",
|
||||
clientCredentials: {
|
||||
id: "3f6135db-f237-421d-af66-a8f4e80d443b",
|
||||
secret: "da35a5a5a7b57f977a9a73394506e878a7175d06606df43dc93e1472b10cf339"
|
||||
}
|
||||
},
|
||||
token: {
|
||||
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
|
||||
},
|
||||
|
||||
// We set these values during user creation, and later re-use them during project seeding.
|
||||
encryptionKeys: {
|
||||
publicKey: "",
|
||||
privateKey: ""
|
||||
}
|
||||
};
|
||||
|
||||
@ -73,7 +92,7 @@ export const generateUserSrpKeys = async (password: string) => {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = encryptSymmetric(privateKey, key.toString("base64"));
|
||||
} = encryptSymmetric128BitHexKeyUTF8(privateKey, key);
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
@ -81,7 +100,7 @@ export const generateUserSrpKeys = async (password: string) => {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
|
||||
} = encryptSymmetric128BitHexKeyUTF8(key.toString("hex"), derivedKey);
|
||||
|
||||
return {
|
||||
protectedKey,
|
||||
@ -107,32 +126,102 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK
|
||||
raw: true
|
||||
});
|
||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||
const key = decryptSymmetric({
|
||||
|
||||
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: user.protectedKey as string,
|
||||
iv: user.protectedKeyIV as string,
|
||||
tag: user.protectedKeyTag as string,
|
||||
key: derivedKey.toString("base64")
|
||||
key: derivedKey
|
||||
});
|
||||
const privateKey = decryptSymmetric({
|
||||
|
||||
const privateKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag,
|
||||
key
|
||||
key: Buffer.from(key, "hex")
|
||||
});
|
||||
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 { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
|
||||
return { nonce, ciphertext };
|
||||
};
|
||||
|
||||
// export const getUserProjectKey = async (privateKey: string) => {
|
||||
// const key = decryptAsymmetric({
|
||||
// ciphertext: decryptFileKey.encryptedKey,
|
||||
// nonce: decryptFileKey.nonce,
|
||||
// publicKey: decryptFileKey.sender.publicKey,
|
||||
// privateKey: PRIVATE_KEY
|
||||
// });
|
||||
// };
|
||||
export const getUserProjectKey = async (privateKey: string, ciphertext: string, nonce: string, publicKey: string) => {
|
||||
return decryptAsymmetric({
|
||||
ciphertext,
|
||||
nonce,
|
||||
publicKey,
|
||||
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)
|
||||
.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,
|
||||
email: seedData1.email,
|
||||
superAdmin: true,
|
||||
@ -48,7 +49,8 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
]);
|
||||
|
||||
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,
|
||||
userId: seedData1.id,
|
||||
ip: "151.196.220.213",
|
||||
|
@ -14,7 +14,8 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
const [org] = await knex(TableName.Organization)
|
||||
.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,
|
||||
name: "infisical",
|
||||
slug: "infisical",
|
||||
|
@ -1,7 +1,11 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, TableName } from "../schemas";
|
||||
import { seedData1 } from "../seed-data";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
|
||||
import { OrgMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
|
||||
|
||||
export const DEFAULT_PROJECT_ENVS = [
|
||||
{ name: "Development", slug: "dev" },
|
||||
@ -20,21 +24,32 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
name: seedData1.project.name,
|
||||
orgId: seedData1.organization.id,
|
||||
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
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
// await knex(TableName.ProjectKeys).insert({
|
||||
// projectId: project.id,
|
||||
// senderId: seedData1.id
|
||||
// });
|
||||
|
||||
await knex(TableName.ProjectMembership).insert({
|
||||
projectId: project.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
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)
|
||||
.insert(
|
||||
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
|
||||
@ -46,4 +61,19 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
)
|
||||
.returning("*");
|
||||
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 { registerProjectRouter } from "./project-router";
|
||||
import { registerSamlRouter } from "./saml-router";
|
||||
import { registerScimRouter } from "./scim-router";
|
||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||
@ -33,6 +34,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
prefix: "/secret-rotation-providers"
|
||||
});
|
||||
await server.register(registerSamlRouter, { prefix: "/sso" });
|
||||
await server.register(registerScimRouter, { prefix: "/scim" });
|
||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||
|
@ -22,6 +22,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgPlansTableByBillCycle({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
billingCycle: req.query.billingCycle
|
||||
});
|
||||
@ -43,6 +44,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const plan = await server.services.license.getOrgPlan({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return { plan };
|
||||
@ -85,6 +87,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.startOrgTrial({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
success_url: req.body.success_url
|
||||
});
|
||||
@ -106,6 +109,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.createOrganizationPortalSession({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -126,6 +130,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgBillingInfo({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -146,6 +151,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgPlanTable({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -166,6 +172,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgBillingDetails({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -190,6 +197,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.updateOrgBillingDetails({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
name: req.body.name,
|
||||
email: req.body.email
|
||||
@ -212,6 +220,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgPmtMethods({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -236,6 +245,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.addOrgPmtMethods({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
success_url: req.body.success_url,
|
||||
cancel_url: req.body.cancel_url
|
||||
@ -261,6 +271,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.delOrgPmtMethods({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
pmtMethodId: req.params.pmtMethodId
|
||||
});
|
||||
@ -284,6 +295,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgTaxIds({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -310,6 +322,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.addOrgTaxId({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
type: req.body.type,
|
||||
value: req.body.value
|
||||
@ -335,6 +348,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.delOrgTaxId({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
taxId: req.params.taxId
|
||||
});
|
||||
@ -358,6 +372,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgTaxInvoices({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
@ -380,6 +395,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const data = await server.services.license.getOrgLicenses({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return data;
|
||||
|
@ -26,7 +26,12 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.orgRole.createRole(req.permission.id, req.params.organizationId, req.body);
|
||||
const role = await server.services.orgRole.createRole(
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.body,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@ -57,7 +62,8 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.params.roleId,
|
||||
req.body
|
||||
req.body,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
@ -82,7 +88,8 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
const role = await server.services.orgRole.deleteRole(
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.params.roleId
|
||||
req.params.roleId,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
@ -107,7 +114,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const roles = await server.services.orgRole.listRoles(req.permission.id, req.params.organizationId);
|
||||
const roles = await server.services.orgRole.listRoles(
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { data: { roles } };
|
||||
}
|
||||
});
|
||||
@ -130,7 +141,8 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { permissions, membership } = await server.services.orgRole.getUserPermission(
|
||||
req.permission.id,
|
||||
req.params.organizationId
|
||||
req.params.organizationId,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { permissions, membership };
|
||||
}
|
||||
|
@ -30,7 +30,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.body
|
||||
req.body,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
@ -63,7 +64,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.params.roleId,
|
||||
req.body
|
||||
req.body,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
@ -89,7 +91,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.params.roleId
|
||||
req.params.roleId,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
@ -117,7 +120,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
const roles = await server.services.projectRole.listRoles(
|
||||
req.permission.type,
|
||||
req.permission.id,
|
||||
req.params.projectId
|
||||
req.params.projectId,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { data: { roles } };
|
||||
}
|
||||
@ -143,7 +147,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { permissions, membership } = await server.services.projectRole.getUserPermission(
|
||||
req.permission.id,
|
||||
req.params.projectId
|
||||
req.params.projectId,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { data: { permissions, membership } };
|
||||
}
|
||||
|
@ -11,6 +11,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
method: "GET",
|
||||
url: "/:workspaceId/secret-snapshots",
|
||||
schema: {
|
||||
description: "Return project secret snapshots ids",
|
||||
security: [
|
||||
{
|
||||
apiKeyAuth: [],
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
@ -31,6 +38,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
const secretSnapshots = await server.services.snapshot.listSnapshots({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.query
|
||||
});
|
||||
@ -60,6 +68,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
const count = await server.services.snapshot.projectSecretSnapshotCount({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
environment: req.query.environment,
|
||||
path: req.query.path
|
||||
@ -72,6 +81,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs",
|
||||
schema: {
|
||||
description: "Return audit logs",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: [],
|
||||
apiKeyAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
@ -112,6 +128,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const auditLogs = await server.services.auditLog.listProjectAuditLogs({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.query,
|
||||
auditLogActor: req.query.actor,
|
||||
|
@ -13,13 +13,12 @@ import { FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SamlConfigsSchema } from "@app/db/schemas";
|
||||
import { SamlProviders } from "@app/ee/services/saml-config/saml-config-types";
|
||||
import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
type TSAMLConfig = {
|
||||
callbackUrl: string;
|
||||
@ -28,6 +27,7 @@ type TSAMLConfig = {
|
||||
cert: string;
|
||||
audience: string;
|
||||
wantAuthnResponseSigned?: boolean;
|
||||
disableRequestedAuthnContext?: boolean;
|
||||
};
|
||||
|
||||
export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
@ -44,17 +44,30 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
// eslint-disable-next-line
|
||||
getSamlOptions: async (req, done) => {
|
||||
try {
|
||||
const { ssoIdentifier } = req.params;
|
||||
if (!ssoIdentifier) throw new BadRequestError({ message: "Missing sso identitier" });
|
||||
const { samlConfigId, orgSlug } = req.params;
|
||||
|
||||
const ssoConfig = await server.services.saml.getSaml({
|
||||
type: "ssoId",
|
||||
id: ssoIdentifier
|
||||
});
|
||||
if (!ssoConfig) throw new BadRequestError({ message: "SSO config not found" });
|
||||
let ssoLookupDetails: TGetSamlCfgDTO;
|
||||
|
||||
if (orgSlug) {
|
||||
ssoLookupDetails = {
|
||||
type: "orgSlug",
|
||||
orgSlug
|
||||
};
|
||||
} else if (samlConfigId) {
|
||||
ssoLookupDetails = {
|
||||
type: "ssoId",
|
||||
id: samlConfigId
|
||||
};
|
||||
} else {
|
||||
throw new BadRequestError({ message: "Missing sso identitier or org slug" });
|
||||
}
|
||||
|
||||
const ssoConfig = await server.services.saml.getSaml(ssoLookupDetails);
|
||||
if (!ssoConfig || !ssoConfig.isActive)
|
||||
throw new BadRequestError({ message: "Failed to authenticate with SAML SSO" });
|
||||
|
||||
const samlConfig: TSAMLConfig = {
|
||||
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoIdentifier}`,
|
||||
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`,
|
||||
entryPoint: ssoConfig.entryPoint,
|
||||
issuer: ssoConfig.issuer,
|
||||
cert: ssoConfig.cert,
|
||||
@ -64,7 +77,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
samlConfig.wantAuthnResponseSigned = false;
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
@ -79,7 +93,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
// eslint-disable-next-line
|
||||
async (req, profile, cb) => {
|
||||
try {
|
||||
const serverCfg = getServerCfg();
|
||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||
const { firstName } = profile;
|
||||
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
|
||||
@ -92,7 +105,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
email,
|
||||
firstName: profile.firstName as string,
|
||||
lastName: profile.lastName as string,
|
||||
isSignupAllowed: Boolean(serverCfg.allowSignUp),
|
||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
||||
@ -108,11 +120,11 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
|
||||
server.route({
|
||||
url: "/redirect/saml2/:ssoIdentifier",
|
||||
url: "/redirect/saml2/organizations/:orgSlug",
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
ssoIdentifier: z.string().trim()
|
||||
orgSlug: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
callback_port: z.string().optional()
|
||||
@ -134,11 +146,37 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/saml2/:ssoIdentifier",
|
||||
url: "/redirect/saml2/:samlConfigId",
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
samlConfigId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
callback_port: z.string().optional()
|
||||
})
|
||||
},
|
||||
preValidation: (req, res) =>
|
||||
(
|
||||
passport.authenticate("saml", {
|
||||
failureRedirect: "/",
|
||||
additionalParams: {
|
||||
RelayState: JSON.stringify({
|
||||
spInitiated: true,
|
||||
callbackPort: req.query.callback_port ?? ""
|
||||
})
|
||||
}
|
||||
} as any) as any
|
||||
)(req, res),
|
||||
handler: () => {}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/saml2/:samlConfigId",
|
||||
method: "POST",
|
||||
schema: {
|
||||
params: z.object({
|
||||
ssoIdentifier: z.string().trim()
|
||||
samlConfigId: z.string().trim()
|
||||
})
|
||||
},
|
||||
preValidation: passport.authenticate("saml", {
|
||||
@ -177,7 +215,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
isActive: z.boolean(),
|
||||
entryPoint: z.string(),
|
||||
issuer: z.string(),
|
||||
cert: z.string()
|
||||
cert: z.string(),
|
||||
lastUsed: z.date().nullable().optional()
|
||||
})
|
||||
.optional()
|
||||
}
|
||||
@ -186,6 +225,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
const saml = await server.services.saml.getSaml({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.query.organizationId,
|
||||
type: "org"
|
||||
});
|
||||
@ -214,6 +254,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
const saml = await server.services.saml.createSamlCfg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId,
|
||||
...req.body
|
||||
});
|
||||
@ -244,6 +285,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
const saml = await server.services.saml.updateSamlCfg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId,
|
||||
...req.body
|
||||
});
|
||||
|
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" }, (_, 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;
|
||||
}
|
||||
});
|
||||
};
|
@ -34,6 +34,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const approval = await server.services.secretApprovalPolicy.createSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.body.workspaceId,
|
||||
...req.body,
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
|
||||
@ -71,6 +72,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const approval = await server.services.secretApprovalPolicy.updateSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
secretPolicyId: req.params.sapId
|
||||
});
|
||||
@ -96,6 +98,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const approval = await server.services.secretApprovalPolicy.deleteSecretApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
secretPolicyId: req.params.sapId
|
||||
});
|
||||
return { approval };
|
||||
@ -120,6 +123,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const approvals = await server.services.secretApprovalPolicy.getSecretApprovalPolicyByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { approvals };
|
||||
@ -146,6 +150,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
const policy = await server.services.secretApprovalPolicy.getSecretApprovalPolicyOfFolder({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId,
|
||||
...req.query
|
||||
});
|
||||
|
@ -52,6 +52,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
@ -80,6 +81,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const approvals = await server.services.secretApprovalRequest.requestCount({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { approvals };
|
||||
@ -104,6 +106,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id
|
||||
});
|
||||
return { approval };
|
||||
@ -131,6 +134,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const review = await server.services.secretApprovalRequest.reviewApproval({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id,
|
||||
status: req.body.status
|
||||
});
|
||||
@ -159,6 +163,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const approval = await server.services.secretApprovalRequest.updateApprovalStatus({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id,
|
||||
status: req.body.status
|
||||
});
|
||||
@ -266,6 +271,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const approval = await server.services.secretApprovalRequest.getSecretApprovalDetails({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
return { approval };
|
||||
|
@ -30,6 +30,7 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro
|
||||
const providers = await server.services.secretRotation.getProviderTemplates({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
return providers;
|
||||
|
@ -40,6 +40,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
const secretRotation = await server.services.secretRotation.createRotation({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
projectId: req.body.workspaceId
|
||||
});
|
||||
@ -73,6 +74,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
const secretRotation = await server.services.secretRotation.restartById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
rotationId: req.body.id
|
||||
});
|
||||
return { secretRotation };
|
||||
@ -123,6 +125,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
const secretRotations = await server.services.secretRotation.getByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { secretRotations };
|
||||
@ -155,6 +158,7 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
const secretRotation = await server.services.secretRotation.deleteById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
rotationId: req.params.id
|
||||
});
|
||||
return { secretRotation };
|
||||
|
@ -22,6 +22,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const session = await server.services.secretScanning.createInstallationSession({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId
|
||||
});
|
||||
return session;
|
||||
@ -45,6 +46,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const { installatedApp } = await server.services.secretScanning.linkInstallationToOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
return installatedApp;
|
||||
@ -65,6 +67,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const appInstallationCompleted = await server.services.secretScanning.getOrgInstallationStatus({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return { appInstallationCompleted };
|
||||
@ -85,6 +88,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const { risks } = await server.services.secretScanning.getRisksByOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
});
|
||||
return { risks };
|
||||
@ -106,6 +110,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
const { risk } = await server.services.secretScanning.updateRiskStatus({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
riskId: req.params.riskId,
|
||||
...req.body
|
||||
|
@ -27,6 +27,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
||||
const secretVersions = await server.services.secret.getSecretVersions({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
secretId: req.params.secretId
|
||||
|
@ -46,6 +46,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
const secretSnapshot = await server.services.snapshot.getSnapshotData({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.secretSnapshotId
|
||||
});
|
||||
return { secretSnapshot };
|
||||
@ -56,6 +57,13 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/:secretSnapshotId/rollback",
|
||||
schema: {
|
||||
description: "Roll back project secrets to those captured in a secret snapshot version.",
|
||||
security: [
|
||||
{
|
||||
apiKeyAuth: [],
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
secretSnapshotId: z.string().trim()
|
||||
}),
|
||||
@ -70,6 +78,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
const secretSnapshot = await server.services.snapshot.rollbackSnapshot({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.secretSnapshotId
|
||||
});
|
||||
return { secretSnapshot };
|
||||
|
@ -24,7 +24,8 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
const trustedIps = await server.services.trustedIp.listIpsByProjectId({
|
||||
projectId: req.params.workspaceId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return { trustedIps };
|
||||
}
|
||||
@ -54,6 +55,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: req.params.workspaceId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@ -97,6 +99,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: req.params.workspaceId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
trustedIpId: req.params.trustedIpId,
|
||||
...req.body
|
||||
});
|
||||
@ -137,6 +140,7 @@ export const registerTrustedIpRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: req.params.workspaceId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
trustedIpId: req.params.trustedIpId
|
||||
});
|
||||
await server.services.auditLog.createAuditLog({
|
||||
|
@ -38,7 +38,8 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
})
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
.offset(offset)
|
||||
.orderBy("createdAt", "desc");
|
||||
if (startDate) {
|
||||
void sqlQuery.where("createdAt", ">=", startDate);
|
||||
}
|
||||
|
@ -30,10 +30,11 @@ export const auditLogServiceFactory = ({
|
||||
startDate,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
auditLogActor
|
||||
}: TListProjectAuditLogDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
const auditLogs = await auditLogDAL.find({
|
||||
startDate,
|
||||
@ -57,6 +58,7 @@ export const auditLogServiceFactory = ({
|
||||
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
||||
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
||||
}
|
||||
|
||||
return auditLogQueue.pushToLog(data);
|
||||
};
|
||||
|
||||
|
@ -15,7 +15,7 @@ export type TListProjectAuditLogDTO = {
|
||||
|
||||
export type TCreateAuditLogDTO = {
|
||||
event: Event;
|
||||
actor: UserActor | IdentityActor | ServiceActor;
|
||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
|
||||
orgId?: string;
|
||||
projectId?: string;
|
||||
} & BaseAuthData;
|
||||
@ -105,6 +105,8 @@ interface IdentityActorMetadata {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ScimClientActorMetadata {}
|
||||
|
||||
export interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
@ -120,7 +122,12 @@ export interface IdentityActor {
|
||||
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 {
|
||||
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,
|
||||
auditLogsRetentionDays: 0,
|
||||
samlSSO: false,
|
||||
scim: false,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
|
@ -44,7 +44,7 @@ type TLicenseServiceFactoryDep = {
|
||||
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||
|
||||
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 || ""}`;
|
||||
export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: TLicenseServiceFactoryDep) => {
|
||||
@ -92,7 +92,7 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
// else it would reach catch statement
|
||||
isValidLicense = true;
|
||||
} 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`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -175,8 +175,14 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
};
|
||||
|
||||
// below all are api calls
|
||||
const getOrgPlansTableByBillCycle = async ({ orgId, actor, actorId, billingCycle }: TOrgPlansTableDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgPlansTableByBillCycle = async ({
|
||||
orgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
billingCycle
|
||||
}: TOrgPlansTableDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||
@ -184,15 +190,15 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgPlan = async ({ orgId, actor, actorId, projectId }: TOrgPlanDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, projectId }: TOrgPlanDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
const plan = await getPlan(orgId, projectId);
|
||||
return plan;
|
||||
};
|
||||
|
||||
const startOrgTrial = async ({ orgId, actorId, actor, success_url }: TStartOrgTrialDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const startOrgTrial = async ({ orgId, actorId, actor, actorOrgId, success_url }: TStartOrgTrialDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
|
||||
@ -213,8 +219,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return { url };
|
||||
};
|
||||
|
||||
const createOrganizationPortalSession = async ({ orgId, actorId, actor }: TCreateOrgPortalSession) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const createOrganizationPortalSession = async ({ orgId, actorId, actor, actorOrgId }: TCreateOrgPortalSession) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
|
||||
@ -260,8 +266,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return { url };
|
||||
};
|
||||
|
||||
const getOrgBillingInfo = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -277,8 +283,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
};
|
||||
|
||||
// returns org current plan feature table
|
||||
const getOrgPlanTable = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgPlanTable = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -293,8 +299,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgBillingDetails = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -310,8 +316,15 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateOrgBillingDetails = async ({ actorId, actor, orgId, name, email }: TUpdateOrgBillingDetailsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const updateOrgBillingDetails = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
orgId,
|
||||
name,
|
||||
email
|
||||
}: TUpdateOrgBillingDetailsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -330,8 +343,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgPmtMethods = async ({ orgId, actor, actorId }: TOrgPmtMethodsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgPmtMethods = async ({ orgId, actor, actorId, actorOrgId }: TOrgPmtMethodsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -349,8 +362,15 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return pmtMethods;
|
||||
};
|
||||
|
||||
const addOrgPmtMethods = async ({ orgId, actor, actorId, success_url, cancel_url }: TAddOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const addOrgPmtMethods = async ({
|
||||
orgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
success_url,
|
||||
cancel_url
|
||||
}: TAddOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -371,8 +391,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return { url };
|
||||
};
|
||||
|
||||
const delOrgPmtMethods = async ({ actorId, actor, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const delOrgPmtMethods = async ({ actorId, actor, actorOrgId, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -388,8 +408,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgTaxIds = async ({ orgId, actor, actorId }: TGetOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgTaxIds = async ({ orgId, actor, actorId, actorOrgId }: TGetOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -406,8 +426,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return taxIds;
|
||||
};
|
||||
|
||||
const addOrgTaxId = async ({ actorId, actor, orgId, type, value }: TAddOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const addOrgTaxId = async ({ actorId, actor, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -427,8 +447,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return data;
|
||||
};
|
||||
|
||||
const delOrgTaxId = async ({ orgId, actor, actorId, taxId }: TDelOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const delOrgTaxId = async ({ orgId, actor, actorId, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -444,8 +464,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgTaxInvoices = async ({ actorId, actor, orgId }: TOrgInvoiceDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, orgId }: TOrgInvoiceDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
@ -461,8 +481,8 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }:
|
||||
return invoices;
|
||||
};
|
||||
|
||||
const getOrgLicenses = async ({ orgId, actor, actorId }: TOrgLicensesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgLicenses = async ({ orgId, actor, actorId, actorOrgId }: TOrgLicensesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
|
@ -25,6 +25,7 @@ export type TFeatureSet = {
|
||||
auditLogs: false;
|
||||
auditLogsRetentionDays: 0;
|
||||
samlSSO: false;
|
||||
scim: false;
|
||||
status: null;
|
||||
trial_end: null;
|
||||
has_used_trial: true;
|
||||
|
@ -16,6 +16,7 @@ export enum OrgPermissionSubjects {
|
||||
Settings = "settings",
|
||||
IncidentAccount = "incident-contact",
|
||||
Sso = "sso",
|
||||
Scim = "scim",
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity"
|
||||
@ -29,6 +30,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
||||
@ -69,6 +71,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, 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.Create, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
|
@ -10,8 +10,10 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
const membership = await db(TableName.OrgMembership)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||
.where("userId", userId)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
|
||||
.select("permissions")
|
||||
.select(selectAllTableCols(TableName.OrgMembership))
|
||||
.first();
|
||||
@ -26,9 +28,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
const membership = await db(TableName.IdentityOrgMembership)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
.join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||
.where("identityId", identityId)
|
||||
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
||||
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
|
||||
.select("permissions")
|
||||
.first();
|
||||
return membership;
|
||||
@ -41,9 +45,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
const membership = await db(TableName.ProjectMembership)
|
||||
.leftJoin(TableName.ProjectRoles, `${TableName.ProjectMembership}.roleId`, `${TableName.ProjectRoles}.id`)
|
||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||
.where("userId", userId)
|
||||
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.ProjectMembership))
|
||||
.select(
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project)
|
||||
)
|
||||
.select("permissions")
|
||||
.first();
|
||||
|
||||
|
@ -94,12 +94,15 @@ export const permissionServiceFactory = ({
|
||||
/*
|
||||
* Get user permission in an organization
|
||||
* */
|
||||
const getUserOrgPermission = async (userId: string, orgId: string) => {
|
||||
const getUserOrgPermission = async (userId: string, orgId: string, userOrgId?: string) => {
|
||||
const membership = await permissionDAL.getOrgPermission(userId, orgId);
|
||||
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
}
|
||||
if (membership.orgAuthEnforced && membership.orgId !== userOrgId) {
|
||||
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
|
||||
}
|
||||
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
||||
};
|
||||
|
||||
@ -112,10 +115,10 @@ export const permissionServiceFactory = ({
|
||||
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
||||
};
|
||||
|
||||
const getOrgPermission = async (type: ActorType, id: string, orgId: string) => {
|
||||
const getOrgPermission = async (type: ActorType, id: string, orgId: string, actorOrgId?: string) => {
|
||||
switch (type) {
|
||||
case ActorType.USER:
|
||||
return getUserOrgPermission(id, orgId);
|
||||
return getUserOrgPermission(id, orgId, actorOrgId);
|
||||
case ActorType.IDENTITY:
|
||||
return getIdentityOrgPermission(id, orgId);
|
||||
default:
|
||||
@ -142,12 +145,17 @@ export const permissionServiceFactory = ({
|
||||
};
|
||||
|
||||
// user permission for a project in an organization
|
||||
const getUserProjectPermission = async (userId: string, projectId: string) => {
|
||||
const getUserProjectPermission = async (userId: string, projectId: string, userOrgId?: string) => {
|
||||
const membership = await permissionDAL.getProjectPermission(userId, projectId);
|
||||
if (!membership) throw new UnauthorizedError({ name: "User not in project" });
|
||||
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
}
|
||||
|
||||
if (membership.orgAuthEnforced && membership.orgId !== userOrgId) {
|
||||
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
|
||||
}
|
||||
|
||||
return {
|
||||
permission: buildProjectPermission(membership.role, membership.permissions),
|
||||
membership
|
||||
@ -160,6 +168,7 @@ export const permissionServiceFactory = ({
|
||||
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
|
||||
throw new BadRequestError({ name: "Custom permission not found" });
|
||||
}
|
||||
|
||||
return {
|
||||
permission: buildProjectPermission(membership.role, membership.permissions),
|
||||
membership
|
||||
@ -168,6 +177,8 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => {
|
||||
const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
|
||||
if (!serviceToken) throw new BadRequestError({ message: "Service token not found" });
|
||||
|
||||
if (serviceToken.projectId !== projectId)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to find service authorization for given project"
|
||||
@ -184,6 +195,8 @@ export const permissionServiceFactory = ({
|
||||
: {
|
||||
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & {
|
||||
orgAuthEnforced: boolean;
|
||||
orgId: string;
|
||||
permissions?: unknown;
|
||||
};
|
||||
};
|
||||
@ -191,11 +204,12 @@ export const permissionServiceFactory = ({
|
||||
const getProjectPermission = async <T extends ActorType>(
|
||||
type: T,
|
||||
id: string,
|
||||
projectId: string
|
||||
projectId: string,
|
||||
actorOrgId?: string
|
||||
): Promise<TProjectPermissionRT<T>> => {
|
||||
switch (type) {
|
||||
case ActorType.USER:
|
||||
return getUserProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>;
|
||||
return getUserProjectPermission(id, projectId, actorOrgId) as Promise<TProjectPermissionRT<T>>;
|
||||
case ActorType.SERVICE:
|
||||
return getServiceTokenProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>;
|
||||
case ActorType.IDENTITY:
|
||||
|
@ -1,10 +1,31 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
|
||||
|
||||
export const samlConfigDALFactory = (db: TDbClient) => {
|
||||
const samlCfgOrm = ormify(db, TableName.SamlConfig);
|
||||
return samlCfgOrm;
|
||||
|
||||
const findEnforceableSamlCfg = async (orgId: string) => {
|
||||
try {
|
||||
const samlCfg = await db(TableName.SamlConfig)
|
||||
.where({
|
||||
orgId,
|
||||
isActive: true
|
||||
})
|
||||
.whereNotNull("lastUsed")
|
||||
.first();
|
||||
|
||||
return samlCfg;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org by id" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...samlCfgOrm,
|
||||
findEnforceableSamlCfg
|
||||
};
|
||||
};
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
@ -27,18 +27,15 @@ import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TSamlConfigDALFactory } from "./saml-config-dal";
|
||||
import {
|
||||
SamlProviders,
|
||||
TCreateSamlCfgDTO,
|
||||
TGetSamlCfgDTO,
|
||||
TSamlLoginDTO,
|
||||
TUpdateSamlCfgDTO
|
||||
} from "./saml-config-types";
|
||||
import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types";
|
||||
|
||||
type TSamlConfigServiceFactoryDep = {
|
||||
samlConfigDAL: TSamlConfigDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "create" | "findUserByEmail" | "transaction" | "updateById">;
|
||||
orgDAL: Pick<TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById">;
|
||||
orgDAL: Pick<
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||
>;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@ -57,6 +54,7 @@ export const samlConfigServiceFactory = ({
|
||||
const createSamlCfg = async ({
|
||||
cert,
|
||||
actor,
|
||||
actorOrgId,
|
||||
orgId,
|
||||
issuer,
|
||||
actorId,
|
||||
@ -64,7 +62,7 @@ export const samlConfigServiceFactory = ({
|
||||
entryPoint,
|
||||
authProvider
|
||||
}: TCreateSamlCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@ -140,12 +138,14 @@ export const samlConfigServiceFactory = ({
|
||||
certIV,
|
||||
certTag
|
||||
});
|
||||
|
||||
return samlConfig;
|
||||
};
|
||||
|
||||
const updateSamlCfg = async ({
|
||||
orgId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
cert,
|
||||
actorId,
|
||||
issuer,
|
||||
@ -153,7 +153,7 @@ export const samlConfigServiceFactory = ({
|
||||
entryPoint,
|
||||
authProvider
|
||||
}: TUpdateSamlCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.samlSSO)
|
||||
@ -162,7 +162,7 @@ export const samlConfigServiceFactory = ({
|
||||
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
||||
});
|
||||
|
||||
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive };
|
||||
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null };
|
||||
const orgBot = await orgBotDAL.findOne({ orgId });
|
||||
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
@ -195,6 +195,8 @@ export const samlConfigServiceFactory = ({
|
||||
updateQuery.certTag = certTag;
|
||||
}
|
||||
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
||||
await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false });
|
||||
|
||||
return ssoConfig;
|
||||
};
|
||||
|
||||
@ -203,6 +205,10 @@ export const samlConfigServiceFactory = ({
|
||||
if (dto.type === "org") {
|
||||
ssoConfig = await samlConfigDAL.findOne({ orgId: dto.orgId });
|
||||
if (!ssoConfig) return;
|
||||
} else if (dto.type === "orgSlug") {
|
||||
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||
if (!org) return;
|
||||
ssoConfig = await samlConfigDAL.findOne({ orgId: org.id });
|
||||
} else if (dto.type === "ssoId") {
|
||||
// TODO:
|
||||
// We made this change because saml config ids were not moved over during the migration
|
||||
@ -227,7 +233,12 @@ export const samlConfigServiceFactory = ({
|
||||
|
||||
// when dto is type id means it's internally used
|
||||
if (dto.type === "org") {
|
||||
const { permission } = await permissionService.getOrgPermission(dto.actor, dto.actorId, ssoConfig.orgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
ssoConfig.orgId,
|
||||
dto.actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
|
||||
}
|
||||
const {
|
||||
@ -284,35 +295,20 @@ export const samlConfigServiceFactory = ({
|
||||
isActive: ssoConfig.isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert
|
||||
cert,
|
||||
lastUsed: ssoConfig.lastUsed
|
||||
};
|
||||
};
|
||||
|
||||
const samlLogin = async ({
|
||||
firstName,
|
||||
email,
|
||||
lastName,
|
||||
authProvider,
|
||||
orgId,
|
||||
relayState,
|
||||
isSignupAllowed
|
||||
}: TSamlLoginDTO) => {
|
||||
const samlLogin = async ({ firstName, email, lastName, authProvider, orgId, relayState }: TSamlLoginDTO) => {
|
||||
const appCfg = getConfig();
|
||||
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);
|
||||
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
||||
|
||||
if (user) {
|
||||
const hasSamlEnabled = (user.authMethods || []).some((method) =>
|
||||
Object.values(SamlProviders).includes(method as SamlProviders)
|
||||
);
|
||||
await userDAL.transaction(async (tx) => {
|
||||
if (!hasSamlEnabled) {
|
||||
await userDAL.updateById(user.id, { authMethods: [authProvider] }, tx);
|
||||
}
|
||||
const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx });
|
||||
if (!orgMembership) {
|
||||
await orgDAL.createMembership(
|
||||
@ -342,7 +338,8 @@ export const samlConfigServiceFactory = ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
authMethods: [authProvider]
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -378,6 +375,9 @@ export const samlConfigServiceFactory = ({
|
||||
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
||||
}
|
||||
);
|
||||
|
||||
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||
|
||||
return { isUserCompleted, providerAuthToken };
|
||||
};
|
||||
|
||||
|
@ -25,7 +25,11 @@ export type TUpdateSamlCfgDTO = Partial<{
|
||||
TOrgPermission;
|
||||
|
||||
export type TGetSamlCfgDTO =
|
||||
| { type: "org"; orgId: string; actor: ActorType; actorId: string }
|
||||
| { type: "org"; orgId: string; actor: ActorType; actorId: string; actorOrgId?: string }
|
||||
| {
|
||||
type: "orgSlug";
|
||||
orgSlug: string;
|
||||
}
|
||||
| {
|
||||
type: "ssoId";
|
||||
id: string;
|
||||
@ -37,7 +41,6 @@ export type TSamlLoginDTO = {
|
||||
lastName?: string;
|
||||
authProvider: string;
|
||||
orgId: string;
|
||||
isSignupAllowed: boolean;
|
||||
// saml thingy
|
||||
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
|
||||
}
|
||||
};
|
||||
};
|
431
backend/src/ee/services/scim/scim-service.ts
Normal file
431
backend/src/ee/services/scim/scim-service.ts
Normal file
@ -0,0 +1,431 @@
|
||||
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],
|
||||
isGhost: false
|
||||
},
|
||||
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;
|
||||
};
|
||||
};
|
@ -44,6 +44,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
name,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
approvals,
|
||||
approvers,
|
||||
projectId,
|
||||
@ -53,7 +54,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
if (approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
@ -96,13 +97,19 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
name,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
approvals,
|
||||
secretPolicyId
|
||||
}: TUpdateSapDTO) => {
|
||||
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
||||
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, secretApprovalPolicy.projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalPolicy.projectId,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const updatedSap = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
||||
@ -145,11 +152,16 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const deleteSecretApprovalPolicy = async ({ secretPolicyId, actor, actorId }: TDeleteSapDTO) => {
|
||||
const deleteSecretApprovalPolicy = async ({ secretPolicyId, actor, actorId, actorOrgId }: TDeleteSapDTO) => {
|
||||
const sapPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
||||
if (!sapPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, sapPolicy.projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
sapPolicy.projectId,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
@ -159,8 +171,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
return sapPolicy;
|
||||
};
|
||||
|
||||
const getSecretApprovalPolicyByProjectId = async ({ actorId, actor, projectId }: TListSapDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const getSecretApprovalPolicyByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TListSapDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId });
|
||||
@ -188,10 +200,11 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
environment,
|
||||
secretPath
|
||||
}: TGetBoardSapDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { SecretApprovalRequestsSecretsSchema, TableName, TSecretTags } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
SecretApprovalRequestsSecretsSchema,
|
||||
TableName,
|
||||
TSecretApprovalRequestsSecrets,
|
||||
TSecretTags
|
||||
} from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
|
||||
@ -11,6 +16,35 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
|
||||
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
|
||||
|
||||
const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => {
|
||||
try {
|
||||
const existingApprovalSecrets = await secretApprovalRequestSecretOrm.find(
|
||||
{
|
||||
$in: {
|
||||
id: data.map((el) => el.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingApprovalSecrets.length !== data.length) {
|
||||
throw new BadRequestError({ message: "Some of the secret approvals do not exist" });
|
||||
}
|
||||
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret)
|
||||
.insert(data)
|
||||
.onConflict("id") // this will cause a conflict then merge the data
|
||||
.merge() // Merge the data with the existing data
|
||||
.returning("*");
|
||||
|
||||
return updatedApprovalSecrets;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByRequestId = async (requestId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)({
|
||||
@ -190,6 +224,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
...secretApprovalRequestSecretOrm,
|
||||
findByRequestId,
|
||||
bulkUpdateNoVersionIncrement,
|
||||
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
|
||||
};
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { groupBy, pick, unique } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TSecretQueueFactory } from "@app/services/secret/secret-queue";
|
||||
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||
@ -47,6 +48,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||
secretService: Pick<
|
||||
TSecretServiceFactory,
|
||||
| "fnSecretBulkInsert"
|
||||
@ -67,16 +69,22 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalRequestReviewerDAL,
|
||||
secretApprovalRequestSecretDAL,
|
||||
secretBlindIndexDAL,
|
||||
projectDAL,
|
||||
permissionService,
|
||||
snapshotService,
|
||||
secretService,
|
||||
secretVersionDAL,
|
||||
secretQueueService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId }: TApprovalRequestCountDTO) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(actor as ActorType.USER, actorId, projectId);
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor as ActorType.USER,
|
||||
actorId,
|
||||
projectId,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id);
|
||||
return count;
|
||||
@ -86,6 +94,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
status,
|
||||
environment,
|
||||
committer,
|
||||
@ -94,7 +103,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}: TListApprovalsDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const { membership } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
||||
projectId,
|
||||
committer,
|
||||
@ -107,7 +116,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return approvals;
|
||||
};
|
||||
|
||||
const getSecretApprovalDetails = async ({ actor, actorId, id }: TSecretApprovalDetailsDTO) => {
|
||||
const getSecretApprovalDetails = async ({ actor, actorId, actorOrgId, id }: TSecretApprovalDetailsDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
||||
@ -117,7 +126,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId
|
||||
secretApprovalRequest.projectId,
|
||||
actorOrgId
|
||||
);
|
||||
if (
|
||||
membership.role !== ProjectMembershipRole.Admin &&
|
||||
@ -134,7 +144,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets };
|
||||
};
|
||||
|
||||
const reviewApproval = async ({ approvalId, actor, status, actorId }: TReviewRequestDTO) => {
|
||||
const reviewApproval = async ({ approvalId, actor, status, actorId, actorOrgId }: TReviewRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
@ -143,7 +153,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId
|
||||
secretApprovalRequest.projectId,
|
||||
actorOrgId
|
||||
);
|
||||
if (
|
||||
membership.role !== ProjectMembershipRole.Admin &&
|
||||
@ -175,7 +186,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return reviewStatus;
|
||||
};
|
||||
|
||||
const updateApprovalStatus = async ({ actorId, status, approvalId, actor }: TStatusChangeDTO) => {
|
||||
const updateApprovalStatus = async ({ actorId, status, approvalId, actor, actorOrgId }: TStatusChangeDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
@ -184,7 +195,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId
|
||||
secretApprovalRequest.projectId,
|
||||
actorOrgId
|
||||
);
|
||||
if (
|
||||
membership.role !== ProjectMembershipRole.Admin &&
|
||||
@ -207,13 +219,18 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
return { ...secretApprovalRequest, ...updatedRequest };
|
||||
};
|
||||
|
||||
const mergeSecretApprovalRequest = async ({ approvalId, actor, actorId }: TMergeSecretApprovalRequestDTO) => {
|
||||
const mergeSecretApprovalRequest = async ({
|
||||
approvalId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId
|
||||
}: TMergeSecretApprovalRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||
|
||||
const { policy, folderId, projectId } = secretApprovalRequest;
|
||||
const { membership } = await permissionService.getProjectPermission(ActorType.USER, actorId, projectId);
|
||||
const { membership } = await permissionService.getProjectPermission(ActorType.USER, actorId, projectId, actorOrgId);
|
||||
if (
|
||||
membership.role !== ProjectMembershipRole.Admin &&
|
||||
secretApprovalRequest.committerId !== membership.id &&
|
||||
@ -401,6 +418,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
data,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
policy,
|
||||
projectId,
|
||||
secretPath,
|
||||
@ -408,12 +426,19 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}: TGenerateSecretApprovalRequestDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const { permission, membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
|
||||
const folderId = folder.id;
|
||||
|
@ -14,13 +14,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/pr
|
||||
import { TSecretRotationDALFactory } from "./secret-rotation-dal";
|
||||
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
|
||||
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
|
||||
import {
|
||||
TCreateSecretRotationDTO,
|
||||
TDeleteDTO,
|
||||
TGetByIdDTO,
|
||||
TListByProjectIdDTO,
|
||||
TRestartDTO
|
||||
} from "./secret-rotation-types";
|
||||
import { TCreateSecretRotationDTO, TDeleteDTO, TListByProjectIdDTO, TRestartDTO } from "./secret-rotation-types";
|
||||
import { rotationTemplates } from "./templates";
|
||||
|
||||
type TSecretRotationServiceFactoryDep = {
|
||||
@ -45,8 +39,8 @@ export const secretRotationServiceFactory = ({
|
||||
folderDAL,
|
||||
secretDAL
|
||||
}: TSecretRotationServiceFactoryDep) => {
|
||||
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const getProviderTemplates = async ({ actor, actorId, actorOrgId, projectId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
|
||||
return {
|
||||
@ -59,6 +53,7 @@ export const secretRotationServiceFactory = ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
inputs,
|
||||
outputs,
|
||||
interval,
|
||||
@ -66,7 +61,7 @@ export const secretRotationServiceFactory = ({
|
||||
secretPath,
|
||||
environment
|
||||
}: TCreateSecretRotationDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
@ -144,23 +139,14 @@ export const secretRotationServiceFactory = ({
|
||||
return secretRotation;
|
||||
};
|
||||
|
||||
const getById = async ({ rotationId, actor, actorId }: TGetByIdDTO) => {
|
||||
const [doc] = await secretRotationDAL.find({ id: rotationId });
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
return doc;
|
||||
};
|
||||
|
||||
const getByProjectId = async ({ actorId, projectId, actor }: TListByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const getByProjectId = async ({ actorId, projectId, actor, actorOrgId }: TListByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
const doc = await secretRotationDAL.find({ projectId });
|
||||
return doc;
|
||||
};
|
||||
|
||||
const restartById = async ({ actor, actorId, rotationId }: TRestartDTO) => {
|
||||
const restartById = async ({ actor, actorId, actorOrgId, rotationId }: TRestartDTO) => {
|
||||
const doc = await secretRotationDAL.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
@ -171,18 +157,18 @@ export const secretRotationServiceFactory = ({
|
||||
message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
|
||||
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
|
||||
await secretRotationQueue.addToQueue(doc.id, doc.interval);
|
||||
return doc;
|
||||
};
|
||||
|
||||
const deleteById = async ({ actor, actorId, rotationId }: TDeleteDTO) => {
|
||||
const deleteById = async ({ actor, actorId, actorOrgId, rotationId }: TDeleteDTO) => {
|
||||
const doc = await secretRotationDAL.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, doc.projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
@ -197,7 +183,6 @@ export const secretRotationServiceFactory = ({
|
||||
|
||||
return {
|
||||
getProviderTemplates,
|
||||
getById,
|
||||
getByProjectId,
|
||||
createRotation,
|
||||
restartById,
|
||||
|
@ -18,7 +18,3 @@ export type TDeleteDTO = {
|
||||
export type TRestartDTO = {
|
||||
rotationId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetByIdDTO = {
|
||||
rotationId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -39,8 +39,8 @@ export const secretScanningServiceFactory = ({
|
||||
permissionService,
|
||||
secretScanningQueue
|
||||
}: TSecretScanningServiceFactoryDep) => {
|
||||
const createInstallationSession = async ({ actor, orgId, actorId }: TInstallAppSessionDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const createInstallationSession = async ({ actor, orgId, actorId, actorOrgId }: TInstallAppSessionDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
@ -48,11 +48,17 @@ export const secretScanningServiceFactory = ({
|
||||
return { sessionId };
|
||||
};
|
||||
|
||||
const linkInstallationToOrg = async ({ sessionId, actorId, installationId, actor }: TLinkInstallSessionDTO) => {
|
||||
const linkInstallationToOrg = async ({
|
||||
sessionId,
|
||||
actorId,
|
||||
installationId,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TLinkInstallSessionDTO) => {
|
||||
const session = await gitAppInstallSessionDAL.findOne({ sessionId });
|
||||
if (!session) throw new UnauthorizedError({ message: "Session not found" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, session.orgId);
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, session.orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
const installatedApp = await gitAppOrgDAL.transaction(async (tx) => {
|
||||
await gitAppInstallSessionDAL.deleteById(session.id, tx);
|
||||
@ -83,23 +89,23 @@ export const secretScanningServiceFactory = ({
|
||||
return { installatedApp };
|
||||
};
|
||||
|
||||
const getOrgInstallationStatus = async ({ actorId, orgId, actor }: TGetOrgInstallStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getOrgInstallationStatus = async ({ actorId, orgId, actor, actorOrgId }: TGetOrgInstallStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const appInstallation = await gitAppOrgDAL.findOne({ orgId });
|
||||
return Boolean(appInstallation);
|
||||
};
|
||||
|
||||
const getRisksByOrg = async ({ actor, orgId, actorId }: TGetOrgRisksDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const getRisksByOrg = async ({ actor, orgId, actorId, actorOrgId }: TGetOrgRisksDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
|
||||
return { risks };
|
||||
};
|
||||
|
||||
const updateRiskStatus = async ({ actorId, orgId, actor, riskId, status }: TUpdateRiskStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
const updateRiskStatus = async ({ actorId, orgId, actor, actorOrgId, riskId, status }: TUpdateRiskStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const isRiskResolved = Boolean(
|
||||
|
@ -58,9 +58,10 @@ export const secretSnapshotServiceFactory = ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
path
|
||||
}: TProjectSnapshotCountDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
@ -75,11 +76,12 @@ export const secretSnapshotServiceFactory = ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
path,
|
||||
limit = 20,
|
||||
offset = 0
|
||||
}: TProjectSnapshotListDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
@ -89,10 +91,10 @@ export const secretSnapshotServiceFactory = ({
|
||||
return snapshots;
|
||||
};
|
||||
|
||||
const getSnapshotData = async ({ actorId, actor, id }: TGetSnapshotDataDTO) => {
|
||||
const getSnapshotData = async ({ actorId, actor, actorOrgId, id }: TGetSnapshotDataDTO) => {
|
||||
const snapshot = await snapshotDAL.findSecretSnapshotDataById(id);
|
||||
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
return snapshot;
|
||||
};
|
||||
@ -143,11 +145,11 @@ export const secretSnapshotServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackSnapshot = async ({ id: snapshotId, actor, actorId }: TRollbackSnapshotDTO) => {
|
||||
const rollbackSnapshot = async ({ id: snapshotId, actor, actorId, actorOrgId }: TRollbackSnapshotDTO) => {
|
||||
const snapshot = await snapshotDAL.findById(snapshotId);
|
||||
if (!snapshot) throw new BadRequestError({ message: "Snapshot not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId);
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, snapshot.projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
|
@ -26,8 +26,8 @@ export const trustedIpServiceFactory = ({
|
||||
licenseService,
|
||||
projectDAL
|
||||
}: TTrustedIpServiceFactoryDep) => {
|
||||
const listIpsByProjectId = async ({ projectId, actor, actorId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const listIpsByProjectId = async ({ projectId, actor, actorId, actorOrgId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
const trustedIps = await trustedIpDAL.find({
|
||||
projectId
|
||||
@ -35,8 +35,16 @@ export const trustedIpServiceFactory = ({
|
||||
return trustedIps;
|
||||
};
|
||||
|
||||
const addProjectIp = async ({ projectId, actorId, actor, ipAddress: ip, comment, isActive }: TCreateIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const addProjectIp = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
ipAddress: ip,
|
||||
comment,
|
||||
isActive
|
||||
}: TCreateIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
@ -65,8 +73,16 @@ export const trustedIpServiceFactory = ({
|
||||
return { trustedIp, project }; // for audit log
|
||||
};
|
||||
|
||||
const updateProjectIp = async ({ projectId, actorId, actor, ipAddress: ip, comment, trustedIpId }: TUpdateIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const updateProjectIp = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
ipAddress: ip,
|
||||
comment,
|
||||
trustedIpId
|
||||
}: TUpdateIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
@ -97,8 +113,8 @@ export const trustedIpServiceFactory = ({
|
||||
return { trustedIp, project }; // for audit log
|
||||
};
|
||||
|
||||
const deleteProjectIp = async ({ projectId, actorId, actor, trustedIpId }: TDeleteIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
const deleteProjectIp = async ({ projectId, actorId, actor, actorOrgId, trustedIpId }: TDeleteIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
@ -15,9 +15,11 @@ const envSchema = z
|
||||
PORT: z.coerce.number().default(4000),
|
||||
REDIS_URL: zpStr(z.string()),
|
||||
HOST: zpStr(z.string().default("localhost")),
|
||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
|
||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")),
|
||||
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
|
||||
SALT_ROUNDS: z.coerce.number().default(10),
|
||||
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),
|
||||
// TODO(akhilmhdh): will be changed to one
|
||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
@ -93,7 +95,7 @@ const envSchema = z
|
||||
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||
// 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_KEY: zpStr(z.string().optional()),
|
||||
STANDALONE_MODE: z
|
||||
|
@ -8,6 +8,9 @@ import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||
|
||||
import { getConfig } from "../config/env";
|
||||
|
||||
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
|
||||
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
|
||||
|
||||
export type TDecryptSymmetricInput = {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
@ -44,7 +47,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 cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
|
||||
|
||||
@ -58,7 +61,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"));
|
||||
|
||||
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
||||
|
@ -1,12 +1,20 @@
|
||||
export {
|
||||
buildSecretBlindIndexFromName,
|
||||
createSecretBlindIndex,
|
||||
decodeBase64,
|
||||
decryptAsymmetric,
|
||||
decryptSymmetric,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encodeBase64,
|
||||
encryptAsymmetric,
|
||||
encryptSymmetric,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
generateAsymmetricKeyPair
|
||||
} from "./encryption";
|
||||
export {
|
||||
decryptIntegrationAuths,
|
||||
decryptSecretApprovals,
|
||||
decryptSecrets,
|
||||
decryptSecretVersions
|
||||
} from "./secret-encryption";
|
||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||
|
293
backend/src/lib/crypto/secret-encryption.ts
Normal file
293
backend/src/lib/crypto/secret-encryption.ts
Normal file
@ -0,0 +1,293 @@
|
||||
import crypto from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
IntegrationAuthsSchema,
|
||||
SecretApprovalRequestsSecretsSchema,
|
||||
SecretsSchema,
|
||||
SecretVersionsSchema,
|
||||
TIntegrationAuths,
|
||||
TProjectKeys,
|
||||
TSecretApprovalRequestsSecrets,
|
||||
TSecrets,
|
||||
TSecretVersions
|
||||
} from "../../db/schemas";
|
||||
import { decryptAsymmetric } from "./encryption";
|
||||
|
||||
const DecryptedValuesSchema = z.object({
|
||||
id: z.string(),
|
||||
secretKey: z.string(),
|
||||
secretValue: z.string(),
|
||||
secretComment: z.string().optional()
|
||||
});
|
||||
|
||||
const DecryptedSecretSchema = z.object({
|
||||
decrypted: DecryptedValuesSchema,
|
||||
original: SecretsSchema
|
||||
});
|
||||
|
||||
const DecryptedIntegrationAuthsSchema = z.object({
|
||||
decrypted: z.object({
|
||||
id: z.string(),
|
||||
access: z.string(),
|
||||
accessId: z.string(),
|
||||
refresh: z.string()
|
||||
}),
|
||||
original: IntegrationAuthsSchema
|
||||
});
|
||||
|
||||
const DecryptedSecretVersionsSchema = z.object({
|
||||
decrypted: DecryptedValuesSchema,
|
||||
original: SecretVersionsSchema
|
||||
});
|
||||
|
||||
const DecryptedSecretApprovalsSchema = z.object({
|
||||
decrypted: DecryptedValuesSchema,
|
||||
original: SecretApprovalRequestsSecretsSchema
|
||||
});
|
||||
|
||||
type DecryptedSecret = z.infer<typeof DecryptedSecretSchema>;
|
||||
type DecryptedSecretVersions = z.infer<typeof DecryptedSecretVersionsSchema>;
|
||||
type DecryptedSecretApprovals = z.infer<typeof DecryptedSecretApprovalsSchema>;
|
||||
type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>;
|
||||
|
||||
type TLatestKey = TProjectKeys & {
|
||||
sender: {
|
||||
publicKey: string;
|
||||
};
|
||||
};
|
||||
|
||||
const decryptCipher = ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key
|
||||
}: {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
key: string | Buffer;
|
||||
}) => {
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "base64"));
|
||||
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, "base64", "utf8");
|
||||
cleartext += decipher.final("utf8");
|
||||
|
||||
return cleartext;
|
||||
};
|
||||
|
||||
const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: string }>, key: string | Buffer) => {
|
||||
const results: string[] = [];
|
||||
|
||||
for (const { ciphertext, iv, tag } of data) {
|
||||
if (!ciphertext || !iv || !tag) {
|
||||
results.push("");
|
||||
} else {
|
||||
results.push(decryptCipher({ ciphertext, iv, tag, key }));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
|
||||
const key = decryptAsymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const decryptedSecrets: DecryptedSecret[] = [];
|
||||
|
||||
encryptedSecrets.forEach((encSecret) => {
|
||||
const [secretKey, secretValue, secretComment] = getDecryptedValues(
|
||||
[
|
||||
{
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag
|
||||
},
|
||||
{
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag
|
||||
},
|
||||
{
|
||||
ciphertext: encSecret.secretCommentCiphertext || "",
|
||||
iv: encSecret.secretCommentIV || "",
|
||||
tag: encSecret.secretCommentTag || ""
|
||||
}
|
||||
],
|
||||
key
|
||||
);
|
||||
|
||||
const decryptedSecret: DecryptedSecret = {
|
||||
decrypted: {
|
||||
secretKey,
|
||||
secretValue,
|
||||
secretComment,
|
||||
id: encSecret.id
|
||||
},
|
||||
original: encSecret
|
||||
};
|
||||
|
||||
decryptedSecrets.push(DecryptedSecretSchema.parse(decryptedSecret));
|
||||
});
|
||||
|
||||
return decryptedSecrets;
|
||||
};
|
||||
|
||||
export const decryptSecretVersions = (
|
||||
encryptedSecretVersions: TSecretVersions[],
|
||||
privateKey: string,
|
||||
latestKey: TLatestKey
|
||||
) => {
|
||||
const key = decryptAsymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const decryptedSecrets: DecryptedSecretVersions[] = [];
|
||||
|
||||
encryptedSecretVersions.forEach((encSecret) => {
|
||||
const [secretKey, secretValue, secretComment] = getDecryptedValues(
|
||||
[
|
||||
{
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag
|
||||
},
|
||||
{
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag
|
||||
},
|
||||
{
|
||||
ciphertext: encSecret.secretCommentCiphertext || "",
|
||||
iv: encSecret.secretCommentIV || "",
|
||||
tag: encSecret.secretCommentTag || ""
|
||||
}
|
||||
],
|
||||
key
|
||||
);
|
||||
|
||||
const decryptedSecret: DecryptedSecretVersions = {
|
||||
decrypted: {
|
||||
secretKey,
|
||||
secretValue,
|
||||
secretComment,
|
||||
id: encSecret.id
|
||||
},
|
||||
original: encSecret
|
||||
};
|
||||
|
||||
decryptedSecrets.push(DecryptedSecretVersionsSchema.parse(decryptedSecret));
|
||||
});
|
||||
|
||||
return decryptedSecrets;
|
||||
};
|
||||
|
||||
export const decryptSecretApprovals = (
|
||||
encryptedSecretApprovals: TSecretApprovalRequestsSecrets[],
|
||||
privateKey: string,
|
||||
latestKey: TLatestKey
|
||||
) => {
|
||||
const key = decryptAsymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const decryptedSecrets: DecryptedSecretApprovals[] = [];
|
||||
|
||||
encryptedSecretApprovals.forEach((encApproval) => {
|
||||
const [secretKey, secretValue, secretComment] = getDecryptedValues(
|
||||
[
|
||||
{
|
||||
ciphertext: encApproval.secretKeyCiphertext,
|
||||
iv: encApproval.secretKeyIV,
|
||||
tag: encApproval.secretKeyTag
|
||||
},
|
||||
{
|
||||
ciphertext: encApproval.secretValueCiphertext,
|
||||
iv: encApproval.secretValueIV,
|
||||
tag: encApproval.secretValueTag
|
||||
},
|
||||
{
|
||||
ciphertext: encApproval.secretCommentCiphertext || "",
|
||||
iv: encApproval.secretCommentIV || "",
|
||||
tag: encApproval.secretCommentTag || ""
|
||||
}
|
||||
],
|
||||
key
|
||||
);
|
||||
|
||||
const decryptedSecret: DecryptedSecretApprovals = {
|
||||
decrypted: {
|
||||
secretKey,
|
||||
secretValue,
|
||||
secretComment,
|
||||
id: encApproval.id
|
||||
},
|
||||
original: encApproval
|
||||
};
|
||||
|
||||
decryptedSecrets.push(DecryptedSecretApprovalsSchema.parse(decryptedSecret));
|
||||
});
|
||||
|
||||
return decryptedSecrets;
|
||||
};
|
||||
|
||||
export const decryptIntegrationAuths = (
|
||||
encryptedIntegrationAuths: TIntegrationAuths[],
|
||||
privateKey: string,
|
||||
latestKey: TLatestKey
|
||||
) => {
|
||||
const key = decryptAsymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const decryptedIntegrationAuths: DecryptedIntegrationAuths[] = [];
|
||||
|
||||
encryptedIntegrationAuths.forEach((encAuth) => {
|
||||
const [access, accessId, refresh] = getDecryptedValues(
|
||||
[
|
||||
{
|
||||
ciphertext: encAuth.accessCiphertext || "",
|
||||
iv: encAuth.accessIV || "",
|
||||
tag: encAuth.accessTag || ""
|
||||
},
|
||||
{
|
||||
ciphertext: encAuth.accessIdCiphertext || "",
|
||||
iv: encAuth.accessIdIV || "",
|
||||
tag: encAuth.accessIdTag || ""
|
||||
},
|
||||
{
|
||||
ciphertext: encAuth.refreshCiphertext || "",
|
||||
iv: encAuth.refreshIV || "",
|
||||
tag: encAuth.refreshTag || ""
|
||||
}
|
||||
],
|
||||
key
|
||||
);
|
||||
|
||||
decryptedIntegrationAuths.push({
|
||||
decrypted: {
|
||||
id: encAuth.id,
|
||||
access,
|
||||
accessId,
|
||||
refresh
|
||||
},
|
||||
original: encAuth
|
||||
});
|
||||
});
|
||||
|
||||
return decryptedIntegrationAuths;
|
||||
};
|
@ -1,4 +1,12 @@
|
||||
import argon2 from "argon2";
|
||||
import crypto from "crypto";
|
||||
import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
import tweetnacl from "tweetnacl-util";
|
||||
|
||||
import { TUserEncryptionKeys } from "@app/db/schemas";
|
||||
|
||||
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
|
||||
|
||||
export const generateSrpServerKey = async (salt: string, verifier: string) => {
|
||||
// eslint-disable-next-line new-cap
|
||||
@ -24,3 +32,99 @@ export const srpCheckClientProof = async (
|
||||
server.setClientPublicKey(clientPublicKey);
|
||||
return server.checkClientProof(clientProof);
|
||||
};
|
||||
|
||||
// Ghost user related:
|
||||
// This functionality is intended for ghost user logic. This happens on the frontend when a user is being created.
|
||||
// We replicate the same functionality on the backend when creating a ghost user.
|
||||
export const generateUserSrpKeys = async (email: string, password: string) => {
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = tweetnacl.encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = tweetnacl.encodeBase64(publicKeyUint8Array);
|
||||
|
||||
// eslint-disable-next-line
|
||||
const client = new jsrp.client();
|
||||
await new Promise((resolve) => {
|
||||
client.init({ username: email, password }, () => resolve(null));
|
||||
});
|
||||
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>((resolve, reject) => {
|
||||
client.createVerifier((err, res) => {
|
||||
if (err) return reject(err);
|
||||
return resolve(res);
|
||||
});
|
||||
});
|
||||
const derivedKey = await argon2.hash(password, {
|
||||
salt: Buffer.from(salt),
|
||||
memoryCost: 65536,
|
||||
timeCost: 3,
|
||||
parallelism: 1,
|
||||
hashLength: 32,
|
||||
type: argon2.argon2id,
|
||||
raw: true
|
||||
});
|
||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = encryptSymmetric(privateKey, key.toString("base64"));
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
|
||||
|
||||
return {
|
||||
protectedKey,
|
||||
plainPrivateKey: privateKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
};
|
||||
};
|
||||
|
||||
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
|
||||
const derivedKey = await argon2.hash(password, {
|
||||
salt: Buffer.from(user.salt),
|
||||
memoryCost: 65536,
|
||||
timeCost: 3,
|
||||
parallelism: 1,
|
||||
hashLength: 32,
|
||||
type: argon2.argon2id,
|
||||
raw: true
|
||||
});
|
||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||
const key = decryptSymmetric({
|
||||
ciphertext: user.protectedKey!,
|
||||
iv: user.protectedKeyIV!,
|
||||
tag: user.protectedKeyTag!,
|
||||
key: derivedKey.toString("base64")
|
||||
});
|
||||
const privateKey = decryptSymmetric({
|
||||
ciphertext: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag,
|
||||
key
|
||||
});
|
||||
return privateKey;
|
||||
};
|
||||
|
||||
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
|
||||
return { nonce, ciphertext };
|
||||
};
|
||||
|
@ -58,3 +58,35 @@ export class BadRequestError extends 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;
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,14 @@ export type TOrgPermission = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
orgId: string;
|
||||
actorOrgId?: string;
|
||||
};
|
||||
|
||||
export type TProjectPermission = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
projectId: string;
|
||||
actorOrgId?: string;
|
||||
};
|
||||
|
||||
export type RequiredKeys<T> = {
|
||||
|
@ -12,7 +12,11 @@ dotenv.config();
|
||||
const run = async () => {
|
||||
const logger = await initLogger();
|
||||
const appCfg = initEnvConfig(logger);
|
||||
const db = initDbConnection(appCfg.DB_CONNECTION_URI);
|
||||
const db = initDbConnection({
|
||||
dbConnectionUri: appCfg.DB_CONNECTION_URI,
|
||||
dbRootCert: appCfg.DB_ROOT_CERT
|
||||
});
|
||||
|
||||
const smtp = smtpServiceFactory(formatSmtpConfig());
|
||||
const queue = queueServiceFactory(appCfg.REDIS_URL);
|
||||
|
||||
@ -36,7 +40,7 @@ const run = async () => {
|
||||
port: appCfg.PORT,
|
||||
host: appCfg.HOST,
|
||||
listenTextResolver: (address) => {
|
||||
bootstrap();
|
||||
void bootstrap();
|
||||
return address;
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
||||
import Redis from "ioredis";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
TScanFullRepoEventPayload,
|
||||
@ -15,7 +16,8 @@ export enum QueueName {
|
||||
IntegrationSync = "sync-integrations",
|
||||
SecretWebhook = "secret-webhook",
|
||||
SecretFullRepoScan = "secret-full-repo-scan",
|
||||
SecretPushEventScan = "secret-push-event-scan"
|
||||
SecretPushEventScan = "secret-push-event-scan",
|
||||
UpgradeProjectToGhost = "upgrade-project-to-ghost"
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@ -25,7 +27,8 @@ export enum QueueJobs {
|
||||
AuditLogPrune = "audit-log-prune-job",
|
||||
SecWebhook = "secret-webhook-trigger",
|
||||
IntegrationSync = "secret-integration-pull",
|
||||
SecretScan = "secret-scan"
|
||||
SecretScan = "secret-scan",
|
||||
UpgradeProjectToGhost = "upgrade-project-to-ghost-job"
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
@ -64,6 +67,20 @@ export type TQueueJobTypes = {
|
||||
payload: TScanFullRepoEventPayload;
|
||||
};
|
||||
[QueueName.SecretPushEventScan]: { name: QueueJobs.SecretScan; payload: TScanPushEventPayload };
|
||||
|
||||
[QueueName.UpgradeProjectToGhost]: {
|
||||
name: QueueJobs.UpgradeProjectToGhost;
|
||||
payload: {
|
||||
projectId: string;
|
||||
startedByUserId: string;
|
||||
encryptedPrivateKey: {
|
||||
encryptedKey: string;
|
||||
encryptedKeyIv: string;
|
||||
encryptedKeyTag: string;
|
||||
keyEncoding: SecretKeyEncoding;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||
|
@ -5,6 +5,7 @@ import type { FastifyCookieOptions } from "@fastify/cookie";
|
||||
import cookie from "@fastify/cookie";
|
||||
import type { FastifyCorsOptions } from "@fastify/cors";
|
||||
import cors from "@fastify/cors";
|
||||
import fastifyEtag from "@fastify/etag";
|
||||
import fastifyFormBody from "@fastify/formbody";
|
||||
import helmet from "@fastify/helmet";
|
||||
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
|
||||
@ -13,11 +14,10 @@ import fasitfy from "fastify";
|
||||
import { Knex } from "knex";
|
||||
import { Logger } from "pino";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { getConfig } from "@lib/config/env";
|
||||
|
||||
import { globalRateLimiterCfg } from "./config/rateLimiter";
|
||||
import { fastifyErrHandler } from "./plugins/error-handler";
|
||||
import { registerExternalNextjs } from "./plugins/external-nextjs";
|
||||
@ -37,8 +37,9 @@ type TMain = {
|
||||
export const main = async ({ db, smtp, logger, queue }: TMain) => {
|
||||
const appCfg = getConfig();
|
||||
const server = fasitfy({
|
||||
logger,
|
||||
logger: appCfg.NODE_ENV === "test" ? false : logger,
|
||||
trustProxy: true,
|
||||
connectionTimeout: 30 * 1000,
|
||||
ignoreTrailingSlash: true
|
||||
}).withTypeProvider<ZodTypeProvider>();
|
||||
|
||||
@ -50,6 +51,8 @@ export const main = async ({ db, smtp, logger, queue }: TMain) => {
|
||||
secret: appCfg.COOKIE_SECRET_SIGN_KEY
|
||||
});
|
||||
|
||||
await server.register(fastifyEtag);
|
||||
|
||||
await server.register<FastifyCorsOptions>(cors, {
|
||||
credentials: true,
|
||||
origin: appCfg.SITE_URL || true
|
||||
@ -72,7 +75,7 @@ export const main = async ({ db, smtp, logger, queue }: TMain) => {
|
||||
if (appCfg.isProductionMode) {
|
||||
await server.register(registerExternalNextjs, {
|
||||
standaloneMode: appCfg.STANDALONE_MODE,
|
||||
dir: path.join(__dirname, "../"),
|
||||
dir: path.join(__dirname, "../../"),
|
||||
port: appCfg.PORT
|
||||
});
|
||||
}
|
||||
|
@ -12,9 +12,9 @@ type BootstrapOpt = {
|
||||
db: Knex;
|
||||
};
|
||||
|
||||
const bootstrapCb = () => {
|
||||
const bootstrapCb = async () => {
|
||||
const appCfg = getConfig();
|
||||
const serverCfg = getServerCfg();
|
||||
const serverCfg = await getServerCfg();
|
||||
if (!serverCfg.initialized) {
|
||||
console.info(`Welcome to Infisical
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user