Merge remote-tracking branch 'origin/main' into misc/export-org-data-feature

This commit is contained in:
Sheen Capadngan
2024-10-21 19:04:02 +08:00
270 changed files with 6703 additions and 1751 deletions
+6
View File
@@ -36,16 +36,22 @@ CLIENT_ID_HEROKU=
CLIENT_ID_VERCEL= CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY= CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB= CLIENT_ID_GITHUB=
CLIENT_ID_GITHUB_APP=
CLIENT_SLUG_GITHUB_APP=
CLIENT_ID_GITLAB= CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET= CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU= CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL= CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY= CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB= CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITHUB_APP=
CLIENT_SECRET_GITLAB= CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET= CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL= CLIENT_SLUG_VERCEL=
CLIENT_PRIVATE_KEY_GITHUB_APP=
CLIENT_APP_ID_GITHUB_APP=
# Sentry (optional) for monitoring errors # Sentry (optional) for monitoring errors
SENTRY_DSN= SENTRY_DSN=
+1
View File
@@ -1 +1,2 @@
DB_CONNECTION_URI= DB_CONNECTION_URI=
AUDIT_LOGS_DB_CONNECTION_URI=
+1
View File
@@ -6,6 +6,7 @@
- [ ] Bug fix - [ ] Bug fix
- [ ] New feature - [ ] New feature
- [ ] Improvement
- [ ] Breaking change - [ ] Breaking change
- [ ] Documentation - [ ] Documentation
+88 -3
View File
@@ -7,7 +7,6 @@ on:
description: "Version number" description: "Version number"
required: true required: true
type: string type: string
defaults: defaults:
run: run:
working-directory: ./backend working-directory: ./backend
@@ -49,9 +48,9 @@ jobs:
- name: Package into node binary - name: Package into node binary
run: | run: |
if [ "${{ matrix.os }}" != "linux" ]; then if [ "${{ matrix.os }}" != "linux" ]; then
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} . pkg --no-bytecode --public-packages "*" --public --compress GZip --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
else else
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core . pkg --no-bytecode --public-packages "*" --public --compress GZip --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
fi fi
# Set up .deb package structure (Debian/Ubuntu only) # Set up .deb package structure (Debian/Ubuntu only)
@@ -83,6 +82,86 @@ jobs:
dpkg-deb --build infisical-core dpkg-deb --build infisical-core
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
### RPM
# Set up .rpm package structure
- name: Set up .rpm package structure
if: matrix.os == 'linux'
run: |
mkdir -p infisical-core-rpm/usr/local/bin
cp ./binary/infisical-core infisical-core-rpm/usr/local/bin/
chmod +x infisical-core-rpm/usr/local/bin/infisical-core
# Install RPM build tools
- name: Install RPM build tools
if: matrix.os == 'linux'
run: sudo apt-get update && sudo apt-get install -y rpm
# Create .spec file for RPM
- name: Create .spec file for RPM
if: matrix.os == 'linux'
run: |
cat <<EOF > infisical-core.spec
%global _enable_debug_package 0
%global debug_package %{nil}
%global __os_install_post /usr/lib/rpm/brp-compress %{nil}
Name: infisical-core
Version: ${{ github.event.inputs.version }}
Release: 1%{?dist}
Summary: Infisical Core standalone executable
License: Proprietary
URL: https://app.infisical.com
%description
Infisical Core standalone executable (app.infisical.com)
%install
mkdir -p %{buildroot}/usr/local/bin
cp %{_sourcedir}/infisical-core %{buildroot}/usr/local/bin/
%files
/usr/local/bin/infisical-core
%pre
%post
%preun
%postun
EOF
# Build .rpm file
- name: Build .rpm package
if: matrix.os == 'linux'
run: |
# Create necessary directories
mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
# Copy the binary directly to SOURCES
cp ./binary/infisical-core rpmbuild/SOURCES/
# Run rpmbuild with verbose output
rpmbuild -vv -bb \
--define "_topdir $(pwd)/rpmbuild" \
--define "_sourcedir $(pwd)/rpmbuild/SOURCES" \
--define "_rpmdir $(pwd)/rpmbuild/RPMS" \
--target ${{ matrix.arch == 'x64' && 'x86_64' || 'aarch64' }} \
infisical-core.spec
# Try to find the RPM file
find rpmbuild -name "*.rpm"
# Move the RPM file if found
if [ -n "$(find rpmbuild -name '*.rpm')" ]; then
mv $(find rpmbuild -name '*.rpm') ./binary/infisical-core-${{matrix.arch}}.rpm
else
echo "RPM file not found!"
exit 1
fi
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: "3.x" # Specify the Python version you need python-version: "3.x" # Specify the Python version you need
@@ -97,6 +176,12 @@ jobs:
working-directory: ./backend working-directory: ./backend
run: cloudsmith push deb --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.deb run: cloudsmith push deb --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.deb
# Publish .rpm file to Cloudsmith (Red Hat-based systems only)
- name: Publish .rpm to Cloudsmith
if: matrix.os == 'linux'
working-directory: ./backend
run: cloudsmith push rpm --republish --no-wait-for-sync --api-key=${{ secrets.CLOUDSMITH_API_KEY }} infisical/infisical-core/any-distro/any-version ./binary/infisical-core-${{ matrix.arch }}.rpm
# Publish .exe file to Cloudsmith (Windows only) # Publish .exe file to Cloudsmith (Windows only)
- name: Publish to Cloudsmith (Windows) - name: Publish to Cloudsmith (Windows)
if: matrix.os == 'win' if: matrix.os == 'win'
@@ -127,6 +127,7 @@ jobs:
- name: Change directory to backend and install dependencies - name: Change directory to backend and install dependencies
env: env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }} DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
AUDIT_LOGS_DB_CONNECTION_URI: ${{ secrets.AUDIT_LOGS_DB_CONNECTION_URI }}
run: | run: |
cd backend cd backend
npm install npm install
+4
View File
@@ -95,6 +95,10 @@ RUN mkdir frontend-build
# Production stage # Production stage
FROM base AS production FROM base AS production
RUN apk add --upgrade --no-cache ca-certificates RUN apk add --upgrade --no-cache ca-certificates
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.31.1 && apk add --no-cache git
RUN addgroup --system --gid 1001 nodejs \ RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user && adduser --system --uid 1001 non-root-user
+1 -4
View File
@@ -135,9 +135,7 @@ 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. This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo): If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo).
<a href="[https://infisical.cal.com/vlad/infisical-demo](https://infisical.cal.com/vlad/infisical-demo)"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
## Security ## Security
@@ -163,4 +161,3 @@ Not sure where to get started? You can:
- [Twitter](https://twitter.com/infisical) for fast news - [Twitter](https://twitter.com/infisical) for fast news
- [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management - [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management
- [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates - [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates
- [Roadmap](https://www.notion.so/infisical/be2d2585a6694e40889b03aef96ea36b?v=5b19a8127d1a4060b54769567a8785fa) for planned features
@@ -123,7 +123,7 @@ describe("Project Environment Router", async () => {
id: deletedProjectEnvironment.id, id: deletedProjectEnvironment.id,
name: mockProjectEnv.name, name: mockProjectEnv.name,
slug: mockProjectEnv.slug, slug: mockProjectEnv.slug,
position: 4, position: 5,
createdAt: expect.any(String), createdAt: expect.any(String),
updatedAt: expect.any(String) updatedAt: expect.any(String)
}) })
+541 -67
View File
@@ -21,12 +21,14 @@
"@fastify/etag": "^5.1.0", "@fastify/etag": "^5.1.0",
"@fastify/formbody": "^7.4.0", "@fastify/formbody": "^7.4.0",
"@fastify/helmet": "^11.1.1", "@fastify/helmet": "^11.1.1",
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0", "@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0", "@fastify/rate-limit": "^9.0.0",
"@fastify/session": "^10.7.0", "@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-retry": "^5.0.5", "@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
@@ -61,7 +63,7 @@
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",
"knex": "^3.0.1", "knex": "^3.0.1",
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"ldif": "^0.5.1", "ldif": "0.5.1",
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"mongodb": "^6.8.1", "mongodb": "^6.8.1",
@@ -4311,6 +4313,15 @@
"fast-uri": "^2.0.0" "fast-uri": "^2.0.0"
} }
}, },
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@fastify/cookie": { "node_modules/@fastify/cookie": {
"version": "9.3.1", "version": "9.3.1",
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.1.tgz", "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.1.tgz",
@@ -4381,6 +4392,20 @@
"helmet": "^7.0.0" "helmet": "^7.0.0"
} }
}, },
"node_modules/@fastify/multipart": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.3.0.tgz",
"integrity": "sha512-A8h80TTyqUzaMVH0Cr9Qcm6RxSkVqmhK/MVBYHYeRRSUbUYv08WecjWKSlG2aSnD4aGI841pVxAjC+G1GafUeQ==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.1.0",
"@fastify/deepmerge": "^1.0.0",
"@fastify/error": "^3.0.0",
"fastify-plugin": "^4.0.0",
"secure-json-parse": "^2.4.0",
"stream-wormhole": "^1.1.0"
}
},
"node_modules/@fastify/passport": { "node_modules/@fastify/passport": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-2.4.0.tgz", "resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-2.4.0.tgz",
@@ -4976,24 +5001,73 @@
} }
}, },
"node_modules/@octokit/auth-app": { "node_modules/@octokit/auth-app": {
"version": "6.0.3", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.1.tgz",
"integrity": "sha512-9N7IlBAKEJR3tJgPSubCxIDYGXSdc+2xbkjYpk9nCyqREnH8qEMoMhiEB1WgoA9yTFp91El92XNXAi+AjuKnfw==", "integrity": "sha512-kRAd6yelV9OgvlEJE88H0VLlQdZcag9UlLr7dV0YYP37X8PPDvhgiTy66QVhDXdyoT0AleFN2w/qXkPdrSzINg==",
"dependencies": { "dependencies": {
"@octokit/auth-oauth-app": "^7.0.0", "@octokit/auth-oauth-app": "^8.1.0",
"@octokit/auth-oauth-user": "^4.0.0", "@octokit/auth-oauth-user": "^5.1.0",
"@octokit/request": "^8.0.2", "@octokit/request": "^9.1.1",
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^6.1.1",
"@octokit/types": "^12.0.0", "@octokit/types": "^13.4.1",
"deprecation": "^2.3.1",
"lru-cache": "^10.0.0", "lru-cache": "^10.0.0",
"universal-github-app-jwt": "^1.1.2", "universal-github-app-jwt": "^2.2.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^7.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
},
"node_modules/@octokit/auth-app/node_modules/@octokit/request": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
"dependencies": {
"@octokit/endpoint": "^10.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-app/node_modules/@octokit/request-error": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
"dependencies": {
"@octokit/types": "^13.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-app/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/auth-app/node_modules/lru-cache": { "node_modules/@octokit/auth-app/node_modules/lru-cache": {
"version": "10.2.0", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
@@ -5002,53 +5076,220 @@
"node": "14 || >=16.14" "node": "14 || >=16.14"
} }
}, },
"node_modules/@octokit/auth-app/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/auth-oauth-app": { "node_modules/@octokit/auth-oauth-app": {
"version": "7.0.1", "version": "8.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz",
"integrity": "sha512-RE0KK0DCjCHXHlQBoubwlLijXEKfhMhKm9gO56xYvFmP1QTMb+vvwRPmQLLx0V+5AvV9N9I3lr1WyTzwL3rMDg==", "integrity": "sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==",
"dependencies": { "dependencies": {
"@octokit/auth-oauth-device": "^6.0.0", "@octokit/auth-oauth-device": "^7.0.0",
"@octokit/auth-oauth-user": "^4.0.0", "@octokit/auth-oauth-user": "^5.0.1",
"@octokit/request": "^8.0.2", "@octokit/request": "^9.0.0",
"@octokit/types": "^12.0.0", "@octokit/types": "^13.0.0",
"@types/btoa-lite": "^1.0.0", "universal-user-agent": "^7.0.0"
"btoa-lite": "^1.0.0",
"universal-user-agent": "^6.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
"dependencies": {
"@octokit/endpoint": "^10.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
"dependencies": {
"@octokit/types": "^13.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/auth-oauth-device": { "node_modules/@octokit/auth-oauth-device": {
"version": "6.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz",
"integrity": "sha512-yxU0rkL65QkjbqQedgVx3gmW7YM5fF+r5uaSj9tM/cQGVqloXcqP2xK90eTyYvl29arFVCW8Vz4H/t47mL0ELw==", "integrity": "sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==",
"dependencies": { "dependencies": {
"@octokit/oauth-methods": "^4.0.0", "@octokit/oauth-methods": "^5.0.0",
"@octokit/request": "^8.0.0", "@octokit/request": "^9.0.0",
"@octokit/types": "^12.0.0", "@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^7.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/auth-oauth-user": { "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": {
"version": "4.0.1", "version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-N94wWW09d0hleCnrO5wt5MxekatqEJ4zf+1vSe8MKMrhZ7gAXKFOKrDEZW2INltvBWJCyDUELgGRv8gfErH1Iw==", "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": { "dependencies": {
"@octokit/auth-oauth-device": "^6.0.0", "@octokit/types": "^13.0.0",
"@octokit/oauth-methods": "^4.0.0", "universal-user-agent": "^7.0.2"
"@octokit/request": "^8.0.2",
"@octokit/types": "^12.0.0",
"btoa-lite": "^1.0.0",
"universal-user-agent": "^6.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
"dependencies": {
"@octokit/endpoint": "^10.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
"dependencies": {
"@octokit/types": "^13.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/auth-oauth-user": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz",
"integrity": "sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==",
"dependencies": {
"@octokit/auth-oauth-device": "^7.0.1",
"@octokit/oauth-methods": "^5.0.0",
"@octokit/request": "^9.0.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
"dependencies": {
"@octokit/endpoint": "^10.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
"dependencies": {
"@octokit/types": "^13.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/auth-token": { "node_modules/@octokit/auth-token": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
@@ -5112,28 +5353,82 @@
} }
}, },
"node_modules/@octokit/oauth-authorization-url": { "node_modules/@octokit/oauth-authorization-url": {
"version": "6.0.2", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz",
"integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==",
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/oauth-methods": { "node_modules/@octokit/oauth-methods": {
"version": "4.0.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.2.tgz",
"integrity": "sha512-1NdTGCoBHyD6J0n2WGXg9+yDLZrRNZ0moTEex/LSPr49m530WNKcCfXDghofYptr3st3eTii+EHoG5k/o+vbtw==", "integrity": "sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==",
"dependencies": { "dependencies": {
"@octokit/oauth-authorization-url": "^6.0.2", "@octokit/oauth-authorization-url": "^7.0.0",
"@octokit/request": "^8.0.2", "@octokit/request": "^9.1.0",
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^6.1.0",
"@octokit/types": "^12.0.0", "@octokit/types": "^13.0.0"
"btoa-lite": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/request": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
"dependencies": {
"@octokit/endpoint": "^10.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
"dependencies": {
"@octokit/types": "^13.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/oauth-methods/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/openapi-types": { "node_modules/@octokit/openapi-types": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz",
@@ -5248,13 +5543,13 @@
} }
}, },
"node_modules/@octokit/request": { "node_modules/@octokit/request": {
"version": "8.1.6", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz",
"integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==",
"dependencies": { "dependencies": {
"@octokit/endpoint": "^9.0.0", "@octokit/endpoint": "^9.0.1",
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^5.1.0",
"@octokit/types": "^12.0.0", "@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^6.0.0"
}, },
"engines": { "engines": {
@@ -5262,11 +5557,11 @@
} }
}, },
"node_modules/@octokit/request-error": { "node_modules/@octokit/request-error": {
"version": "5.0.1", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz",
"integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==",
"dependencies": { "dependencies": {
"@octokit/types": "^12.0.0", "@octokit/types": "^13.1.0",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
"once": "^1.4.0" "once": "^1.4.0"
}, },
@@ -5274,6 +5569,32 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
},
"node_modules/@octokit/request-error/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/request/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
},
"node_modules/@octokit/request/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/@octokit/rest": { "node_modules/@octokit/rest": {
"version": "20.0.2", "version": "20.0.2",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz",
@@ -14160,6 +14481,154 @@
"@octokit/core": ">=5" "@octokit/core": ">=5"
} }
}, },
"node_modules/octokit-auth-probot/node_modules/@octokit/auth-app": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.2.tgz",
"integrity": "sha512-fWjIOpxnL8/YFY3kqquciFQ4o99aCqHw5kMFoGPYbz/h5HNZ11dJlV9zag5wS2nt0X1wJ5cs9BUo+CsAPfW4jQ==",
"dependencies": {
"@octokit/auth-oauth-app": "^7.1.0",
"@octokit/auth-oauth-user": "^4.1.0",
"@octokit/request": "^8.3.1",
"@octokit/request-error": "^5.1.0",
"@octokit/types": "^13.1.0",
"deprecation": "^2.3.1",
"lru-cache": "^10.0.0",
"universal-github-app-jwt": "^1.1.2",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/auth-app/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz",
"integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==",
"dependencies": {
"@octokit/auth-oauth-device": "^6.1.0",
"@octokit/auth-oauth-user": "^4.1.0",
"@octokit/request": "^8.3.1",
"@octokit/types": "^13.0.0",
"@types/btoa-lite": "^1.0.0",
"btoa-lite": "^1.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz",
"integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==",
"dependencies": {
"@octokit/oauth-methods": "^4.1.0",
"@octokit/request": "^8.3.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz",
"integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==",
"dependencies": {
"@octokit/auth-oauth-device": "^6.1.0",
"@octokit/oauth-methods": "^4.1.0",
"@octokit/request": "^8.3.1",
"@octokit/types": "^13.0.0",
"btoa-lite": "^1.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/oauth-authorization-url": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz",
"integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==",
"engines": {
"node": ">= 18"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz",
"integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==",
"dependencies": {
"@octokit/oauth-authorization-url": "^6.0.2",
"@octokit/request": "^8.3.1",
"@octokit/request-error": "^5.1.0",
"@octokit/types": "^13.0.0",
"btoa-lite": "^1.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods/node_modules/@octokit/types": {
"version": "13.6.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
}
},
"node_modules/octokit-auth-probot/node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg=="
},
"node_modules/octokit-auth-probot/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
},
"node_modules/octokit-auth-probot/node_modules/universal-github-app-jwt": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz",
"integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==",
"dependencies": {
"@types/jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.2"
}
},
"node_modules/oidc-token-hash": { "node_modules/oidc-token-hash": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
@@ -16604,6 +17073,15 @@
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="
}, },
"node_modules/stream-wormhole": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz",
"integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -18143,13 +18621,9 @@
} }
}, },
"node_modules/universal-github-app-jwt": { "node_modules/universal-github-app-jwt": {
"version": "1.1.2", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.1.2.tgz", "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz",
"integrity": "sha512-t1iB2FmLFE+yyJY9+3wMx0ejB+MQpEVkH0gQv7dR6FZyltyq+ZZO0uDpbopxhrZ3SLEO4dCEkIujOMldEQ2iOA==", "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ=="
"dependencies": {
"@types/jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.2"
}
}, },
"node_modules/universal-user-agent": { "node_modules/universal-user-agent": {
"version": "6.0.1", "version": "6.0.1",
+14 -6
View File
@@ -45,13 +45,19 @@
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts", "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
"generate:component": "tsx ./scripts/create-backend-file.ts", "generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts",
"auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest",
"auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up",
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
"auditlog-migration:list": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:list",
"auditlog-migration:status": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:status",
"auditlog-migration:rollback": "knex --knexfile ./src/db/auditlog-knexfile.ts migrate:rollback",
"migration:new": "tsx ./scripts/create-migration.ts", "migration:new": "tsx ./scripts/create-migration.ts",
"migration:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up", "migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down", "migration:down": "npm run auditlog-migration:down && knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list", "migration:list": "npm run auditlog-migration:list && knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest", "migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status", "migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback", "migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"migrate:org": "tsx ./scripts/migrate-organization.ts", "migrate:org": "tsx ./scripts/migrate-organization.ts",
"seed:new": "tsx ./scripts/create-seed-file.ts", "seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
@@ -120,12 +126,14 @@
"@fastify/etag": "^5.1.0", "@fastify/etag": "^5.1.0",
"@fastify/formbody": "^7.4.0", "@fastify/formbody": "^7.4.0",
"@fastify/helmet": "^11.1.1", "@fastify/helmet": "^11.1.1",
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0", "@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0", "@fastify/rate-limit": "^9.0.0",
"@fastify/session": "^10.7.0", "@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-retry": "^5.0.5", "@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
+6 -1
View File
@@ -90,7 +90,12 @@ const main = async () => {
.whereRaw("table_schema = current_schema()") .whereRaw("table_schema = current_schema()")
.select<{ tableName: string }[]>("table_name as tableName") .select<{ tableName: string }[]>("table_name as tableName")
.orderBy("table_name") .orderBy("table_name")
).filter((el) => !el.tableName.includes("_migrations")); ).filter(
(el) =>
!el.tableName.includes("_migrations") &&
!el.tableName.includes("audit_logs_") &&
el.tableName !== "intermediate_audit_logs"
);
for (let i = 0; i < tables.length; i += 1) { for (let i = 0; i < tables.length; i += 1) {
const { tableName } = tables[i]; const { tableName } = tables[i];
+2
View File
@@ -39,6 +39,7 @@ import { TCertificateServiceFactory } from "@app/services/certificate/certificat
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
@@ -185,6 +186,7 @@ declare module "fastify" {
workflowIntegration: TWorkflowIntegrationServiceFactory; workflowIntegration: TWorkflowIntegrationServiceFactory;
cmek: TCmekServiceFactory; cmek: TCmekServiceFactory;
migration: TExternalMigrationServiceFactory; migration: TExternalMigrationServiceFactory;
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer
+10
View File
@@ -336,6 +336,11 @@ import {
TWorkflowIntegrationsInsert, TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate TWorkflowIntegrationsUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import {
TExternalGroupOrgRoleMappings,
TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate
} from "@app/db/schemas/external-group-org-role-mappings";
import { import {
TSecretV2TagJunction, TSecretV2TagJunction,
TSecretV2TagJunctionInsert, TSecretV2TagJunctionInsert,
@@ -808,5 +813,10 @@ declare module "knex/types/tables" {
TWorkflowIntegrationsInsert, TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate TWorkflowIntegrationsUpdate
>; >;
[TableName.ExternalGroupOrgRoleMapping]: KnexOriginal.CompositeTableType<
TExternalGroupOrgRoleMappings,
TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate
>;
} }
} }
+75
View File
@@ -0,0 +1,75 @@
// eslint-disable-next-line
import "ts-node/register";
import dotenv from "dotenv";
import type { Knex } from "knex";
import path from "path";
// Update with your config settings. .
dotenv.config({
path: path.join(__dirname, "../../../.env.migration")
});
dotenv.config({
path: path.join(__dirname, "../../../.env")
});
if (!process.env.AUDIT_LOGS_DB_CONNECTION_URI && !process.env.AUDIT_LOGS_DB_HOST) {
console.info("Dedicated audit log database not found. No further migrations necessary");
process.exit(0);
}
console.info("Executing migration on audit log database...");
export default {
development: {
client: "postgres",
connection: {
connectionString: process.env.AUDIT_LOGS_DB_CONNECTION_URI,
host: process.env.AUDIT_LOGS_DB_HOST,
port: process.env.AUDIT_LOGS_DB_PORT,
user: process.env.AUDIT_LOGS_DB_USER,
database: process.env.AUDIT_LOGS_DB_NAME,
password: process.env.AUDIT_LOGS_DB_PASSWORD,
ssl: process.env.AUDIT_LOGS_DB_ROOT_CERT
? {
rejectUnauthorized: true,
ca: Buffer.from(process.env.AUDIT_LOGS_DB_ROOT_CERT, "base64").toString("ascii")
}
: false
},
pool: {
min: 2,
max: 10
},
seeds: {
directory: "./seeds"
},
migrations: {
tableName: "infisical_migrations"
}
},
production: {
client: "postgres",
connection: {
connectionString: process.env.AUDIT_LOGS_DB_CONNECTION_URI,
host: process.env.AUDIT_LOGS_DB_HOST,
port: process.env.AUDIT_LOGS_DB_PORT,
user: process.env.AUDIT_LOGS_DB_USER,
database: process.env.AUDIT_LOGS_DB_NAME,
password: process.env.AUDIT_LOGS_DB_PASSWORD,
ssl: process.env.AUDIT_LOGS_DB_ROOT_CERT
? {
rejectUnauthorized: true,
ca: Buffer.from(process.env.AUDIT_LOGS_DB_ROOT_CERT, "base64").toString("ascii")
}
: false
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: "infisical_migrations"
}
}
} as Knex.Config;
+1 -1
View File
@@ -1,2 +1,2 @@
export type { TDbClient } from "./instance"; export type { TDbClient } from "./instance";
export { initDbConnection } from "./instance"; export { initAuditLogDbConnection, initDbConnection } from "./instance";
+42
View File
@@ -70,3 +70,45 @@ export const initDbConnection = ({
return db; return db;
}; };
export const initAuditLogDbConnection = ({
dbConnectionUri,
dbRootCert
}: {
dbConnectionUri: string;
dbRootCert?: string;
}) => {
// akhilmhdh: the default Knex is knex.Knex<any, any[]>. but when assigned with knex({<config>}) the value is knex.Knex<any, unknown[]>
// this was causing issue with files like `snapshot-dal` `findRecursivelySnapshots` this i am explicitly putting the any and unknown[]
// eslint-disable-next-line
const db: Knex<any, unknown[]> = knex({
client: "pg",
connection: {
connectionString: dbConnectionUri,
host: process.env.AUDIT_LOGS_DB_HOST,
// @ts-expect-error I have no clue why only for the port there is a type error
// eslint-disable-next-line
port: process.env.AUDIT_LOGS_DB_PORT,
user: process.env.AUDIT_LOGS_DB_USER,
database: process.env.AUDIT_LOGS_DB_NAME,
password: process.env.AUDIT_LOGS_DB_PASSWORD,
ssl: dbRootCert
? {
rejectUnauthorized: true,
ca: Buffer.from(dbRootCert, "base64").toString("ascii")
}
: false
}
});
// we add these overrides so that auditLogDb and the primary DB are interchangeable
db.primaryNode = () => {
return db;
};
db.replicaNode = () => {
return db;
};
return db;
};
@@ -0,0 +1,161 @@
import kx, { Knex } from "knex";
import { TableName } from "../schemas";
const INTERMEDIATE_AUDIT_LOG_TABLE = "intermediate_audit_logs";
const formatPartitionDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Date) => {
const startDateStr = formatPartitionDate(startDate);
const endDateStr = formatPartitionDate(endDate);
const partitionName = `${TableName.AuditLog}_${startDateStr.replace(/-/g, "")}_${endDateStr.replace(/-/g, "")}`;
await knex.schema.raw(
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')`
);
};
const up = async (knex: Knex): Promise<void> => {
console.info("Dropping primary key of audit log table...");
await knex.schema.alterTable(TableName.AuditLog, (t) => {
// remove existing keys
t.dropPrimary();
});
// Get all indices of the audit log table and drop them
const indexNames: { rows: { indexname: string }[] } = await knex.raw(
`
SELECT indexname
FROM pg_indexes
WHERE tablename = '${TableName.AuditLog}'
`
);
console.log(
"Deleting existing audit log indices:",
indexNames.rows.map((e) => e.indexname)
);
for await (const row of indexNames.rows) {
await knex.raw(`DROP INDEX IF EXISTS ${row.indexname}`);
}
// renaming audit log to intermediate table
console.log("Renaming audit log table to the intermediate name");
await knex.schema.renameTable(TableName.AuditLog, INTERMEDIATE_AUDIT_LOG_TABLE);
if (!(await knex.schema.hasTable(TableName.AuditLog))) {
const createTableSql = knex.schema
.createTable(TableName.AuditLog, (t) => {
t.uuid("id").defaultTo(knex.fn.uuid());
t.string("actor").notNullable();
t.jsonb("actorMetadata").notNullable();
t.string("ipAddress");
t.string("eventType").notNullable();
t.jsonb("eventMetadata");
t.string("userAgent");
t.string("userAgentType");
t.datetime("expiresAt");
t.timestamps(true, true, true);
t.uuid("orgId");
t.string("projectId");
t.string("projectName");
t.primary(["id", "createdAt"]);
})
.toString();
console.info("Creating partition table...");
await knex.schema.raw(`
${createTableSql} PARTITION BY RANGE ("createdAt");
`);
console.log("Adding indices...");
await knex.schema.alterTable(TableName.AuditLog, (t) => {
t.index(["projectId", "createdAt"]);
t.index(["orgId", "createdAt"]);
t.index("expiresAt");
t.index("orgId");
t.index("projectId");
});
console.log("Adding GIN indices...");
await knex.raw(
`CREATE INDEX IF NOT EXISTS "audit_logs_actorMetadata_idx" ON ${TableName.AuditLog} USING gin("actorMetadata" jsonb_path_ops)`
);
console.log("GIN index for actorMetadata done");
await knex.raw(
`CREATE INDEX IF NOT EXISTS "audit_logs_eventMetadata_idx" ON ${TableName.AuditLog} USING gin("eventMetadata" jsonb_path_ops)`
);
console.log("GIN index for eventMetadata done");
// create default partition
console.log("Creating default partition...");
await knex.schema.raw(`CREATE TABLE ${TableName.AuditLog}_default PARTITION OF ${TableName.AuditLog} DEFAULT`);
const nextDate = new Date();
nextDate.setDate(nextDate.getDate() + 1);
const nextDateStr = formatPartitionDate(nextDate);
console.log("Attaching existing audit log table as a partition...");
await knex.schema.raw(`
ALTER TABLE ${INTERMEDIATE_AUDIT_LOG_TABLE} ADD CONSTRAINT audit_log_old
CHECK ( "createdAt" < DATE '${nextDateStr}' );
ALTER TABLE ${TableName.AuditLog} ATTACH PARTITION ${INTERMEDIATE_AUDIT_LOG_TABLE}
FOR VALUES FROM (MINVALUE) TO ('${nextDateStr}' );
`);
// create partition from now until end of month
console.log("Creating audit log partitions ahead of time... next date:", nextDateStr);
await createAuditLogPartition(knex, nextDate, new Date(nextDate.getFullYear(), nextDate.getMonth() + 1));
// create partitions 4 years ahead
const partitionMonths = 4 * 12;
const partitionPromises: Promise<void>[] = [];
for (let x = 1; x <= partitionMonths; x += 1) {
partitionPromises.push(
createAuditLogPartition(
knex,
new Date(nextDate.getFullYear(), nextDate.getMonth() + x, 1),
new Date(nextDate.getFullYear(), nextDate.getMonth() + (x + 1), 1)
)
);
}
await Promise.all(partitionPromises);
console.log("Partition migration complete");
}
};
export const executeMigration = async (url: string) => {
console.log("Executing migration...");
const knex = kx({
client: "pg",
connection: url
});
await knex.transaction(async (tx) => {
await up(tx);
});
};
const dbUrl = process.env.AUDIT_LOGS_DB_CONNECTION_URI;
if (!dbUrl) {
console.error("Please provide a DB connection URL to the AUDIT_LOGS_DB_CONNECTION_URI env");
process.exit(1);
}
void executeMigration(dbUrl).then(() => {
console.log("Migration: partition-audit-logs DONE");
process.exit(0);
});
@@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.KmsKey)) {
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
if (!hasSlug) {
// add slug back temporarily and set value equal to name
await knex.schema
.alterTable(TableName.KmsKey, (table) => {
table.string("slug", 32);
})
.then(() => knex(TableName.KmsKey).update("slug", knex.ref("name")));
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.KmsKey)) {
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
if (hasSlug) {
await knex.schema.alterTable(TableName.KmsKey, (table) => {
table.dropColumn("slug");
});
}
}
}
@@ -0,0 +1,48 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.AuditLog)) {
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
const doesProjectNameExist = await knex.schema.hasColumn(TableName.AuditLog, "projectName");
await knex.schema.alterTable(TableName.AuditLog, (t) => {
if (doesOrgIdExist) {
t.dropForeign("orgId");
}
if (doesProjectIdExist) {
t.dropForeign("projectId");
}
// add normalized field
if (!doesProjectNameExist) {
t.string("projectName");
}
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesProjectIdExist = await knex.schema.hasColumn(TableName.AuditLog, "projectId");
const doesOrgIdExist = await knex.schema.hasColumn(TableName.AuditLog, "orgId");
const doesProjectNameExist = await knex.schema.hasColumn(TableName.AuditLog, "projectName");
if (await knex.schema.hasTable(TableName.AuditLog)) {
await knex.schema.alterTable(TableName.AuditLog, (t) => {
if (doesOrgIdExist) {
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
}
if (doesProjectIdExist) {
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
}
// remove normalized field
if (doesProjectNameExist) {
t.dropColumn("projectName");
}
});
}
}
@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
// org default role
if (await knex.schema.hasTable(TableName.Organization)) {
const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole");
if (!hasDefaultRoleCol) {
await knex.schema.alterTable(TableName.Organization, (tb) => {
tb.string("defaultMembershipRole").notNullable().defaultTo("member");
});
}
}
}
export async function down(knex: Knex): Promise<void> {
// org default role
if (await knex.schema.hasTable(TableName.Organization)) {
const hasDefaultRoleCol = await knex.schema.hasColumn(TableName.Organization, "defaultMembershipRole");
if (hasDefaultRoleCol) {
await knex.schema.alterTable(TableName.Organization, (tb) => {
tb.dropColumn("defaultMembershipRole");
});
}
}
}
@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
t.string("value", 1020).alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
t.string("value", 255).alter();
});
}
}
@@ -0,0 +1,32 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
export async function up(knex: Knex): Promise<void> {
// add external group to org role mapping table
if (!(await knex.schema.hasTable(TableName.ExternalGroupOrgRoleMapping))) {
await knex.schema.createTable(TableName.ExternalGroupOrgRoleMapping, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("groupName").notNullable();
t.index("groupName");
t.string("role").notNullable();
t.uuid("roleId");
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
t.unique(["orgId", "groupName"]);
});
await createOnUpdateTrigger(knex, TableName.ExternalGroupOrgRoleMapping);
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.ExternalGroupOrgRoleMapping)) {
await dropOnUpdateTrigger(knex, TableName.ExternalGroupOrgRoleMapping);
await knex.schema.dropTable(TableName.ExternalGroupOrgRoleMapping);
}
}
+2 -1
View File
@@ -20,7 +20,8 @@ export const AuditLogsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
orgId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(),
projectId: z.string().nullable().optional() projectId: z.string().nullable().optional(),
projectName: z.string().nullable().optional()
}); });
export type TAuditLogs = z.infer<typeof AuditLogsSchema>; export type TAuditLogs = z.infer<typeof AuditLogsSchema>;
@@ -0,0 +1,27 @@
// 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 ExternalGroupOrgRoleMappingsSchema = z.object({
id: z.string().uuid(),
groupName: z.string(),
role: z.string(),
roleId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TExternalGroupOrgRoleMappings = z.infer<typeof ExternalGroupOrgRoleMappingsSchema>;
export type TExternalGroupOrgRoleMappingsInsert = Omit<
z.input<typeof ExternalGroupOrgRoleMappingsSchema>,
TImmutableDBKeys
>;
export type TExternalGroupOrgRoleMappingsUpdate = Partial<
Omit<z.input<typeof ExternalGroupOrgRoleMappingsSchema>, TImmutableDBKeys>
>;
+2 -1
View File
@@ -16,7 +16,8 @@ export const KmsKeysSchema = z.object({
name: z.string(), name: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
projectId: z.string().nullable().optional() projectId: z.string().nullable().optional(),
slug: z.string().nullable().optional()
}); });
export type TKmsKeys = z.infer<typeof KmsKeysSchema>; export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
+1
View File
@@ -17,6 +17,7 @@ export enum TableName {
Groups = "groups", Groups = "groups",
GroupProjectMembership = "group_project_memberships", GroupProjectMembership = "group_project_memberships",
GroupProjectMembershipRole = "group_project_membership_roles", GroupProjectMembershipRole = "group_project_membership_roles",
ExternalGroupOrgRoleMapping = "external_group_org_role_mappings",
UserGroupMembership = "user_group_membership", UserGroupMembership = "user_group_membership",
UserAliases = "user_aliases", UserAliases = "user_aliases",
UserEncryptionKey = "user_encryption_keys", UserEncryptionKey = "user_encryption_keys",
+2 -1
View File
@@ -19,7 +19,8 @@ export const OrganizationsSchema = z.object({
authEnforced: z.boolean().default(false).nullable().optional(), authEnforced: z.boolean().default(false).nullable().optional(),
scimEnabled: z.boolean().default(false).nullable().optional(), scimEnabled: z.boolean().default(false).nullable().optional(),
kmsDefaultKeyId: z.string().uuid().nullable().optional(), kmsDefaultKeyId: z.string().uuid().nullable().optional(),
kmsEncryptedDataKey: zodBuffer.nullable().optional() kmsEncryptedDataKey: zodBuffer.nullable().optional(),
defaultMembershipRole: z.string().default("member")
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;
+4 -1
View File
@@ -128,7 +128,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
.map((key) => { .map((key) => {
// for the ones like in format: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email // for the ones like in format: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email
const formatedKey = key.startsWith("http") ? key.split("/").at(-1) || "" : key; const formatedKey = key.startsWith("http") ? key.split("/").at(-1) || "" : key;
return { key: formatedKey, value: String((profile.attributes as Record<string, string>)[key]) }; return {
key: formatedKey,
value: String((profile.attributes as Record<string, string>)[key]).substring(0, 1020)
};
}) })
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key)); .filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
+4 -6
View File
@@ -20,7 +20,7 @@ const ScimUserSchema = z.object({
z.object({ z.object({
primary: z.boolean(), primary: z.boolean(),
value: z.string().email(), value: z.string().email(),
type: z.string().trim() type: z.string().trim().default("work")
}) })
) )
.optional(), .optional(),
@@ -210,8 +210,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
.array( .array(
z.object({ z.object({
primary: z.boolean(), primary: z.boolean(),
value: z.string().email(), value: z.string().email()
type: z.string().trim()
}) })
) )
.optional(), .optional(),
@@ -281,8 +280,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
.array( .array(
z.object({ z.object({
primary: z.boolean(), primary: z.boolean(),
value: z.string().email(), value: z.string().email()
type: z.string().trim()
}) })
) )
.optional(), .optional(),
@@ -301,7 +299,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
z.object({ z.object({
primary: z.boolean(), primary: z.boolean(),
value: z.string().email(), value: z.string().email(),
type: z.string().trim() type: z.string().trim().default("work")
}) })
), ),
displayName: z.string().trim(), displayName: z.string().trim(),
@@ -2,6 +2,8 @@ import { z } from "zod";
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas"; import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types"; import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -23,6 +25,13 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const appCfg = getConfig();
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
throw new BadRequestError({
message: "Secret scanning is temporarily unavailable."
});
}
const session = await server.services.secretScanning.createInstallationSession({ const session = await server.services.secretScanning.createInstallationSession({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -30,6 +39,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
orgId: req.body.organizationId orgId: req.body.organizationId
}); });
return session; return session;
} }
}); });
@@ -1,8 +1,9 @@
import { Knex } from "knex"; // weird commonjs-related error in the CI requires us to do the import like this
import knex from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { AuditLogsSchema, TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -46,7 +47,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
eventType?: EventType[]; eventType?: EventType[];
eventMetadata?: Record<string, string>; eventMetadata?: Record<string, string>;
}, },
tx?: Knex tx?: knex.Knex
) => { ) => {
if (!orgId && !projectId) { if (!orgId && !projectId) {
throw new Error("Either orgId or projectId must be provided"); throw new Error("Either orgId or projectId must be provided");
@@ -55,11 +56,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
try { try {
// Find statements // Find statements
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog) const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
.where(function () { .where(function () {
if (orgId) { if (orgId) {
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId); void this.where(`${TableName.AuditLog}.orgId`, orgId);
} else if (projectId) { } else if (projectId) {
void this.where(`${TableName.AuditLog}.projectId`, projectId); void this.where(`${TableName.AuditLog}.projectId`, projectId);
} }
@@ -72,23 +72,19 @@ export const auditLogDALFactory = (db: TDbClient) => {
// Select statements // Select statements
void sqlQuery void sqlQuery
.select(selectAllTableCols(TableName.AuditLog)) .select(selectAllTableCols(TableName.AuditLog))
.select(
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
)
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
.orderBy(`${TableName.AuditLog}.createdAt`, "desc"); .orderBy(`${TableName.AuditLog}.createdAt`, "desc");
// Special case: Filter by actor ID // Special case: Filter by actor ID
if (actorId) { if (actorId) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]); void sqlQuery.whereRaw(`"actorMetadata" @> jsonb_build_object('userId', ?::text)`, [actorId]);
} }
// Special case: Filter by key/value pairs in eventMetadata field // Special case: Filter by key/value pairs in eventMetadata field
if (eventMetadata && Object.keys(eventMetadata).length) { if (eventMetadata && Object.keys(eventMetadata).length) {
Object.entries(eventMetadata).forEach(([key, value]) => { Object.entries(eventMetadata).forEach(([key, value]) => {
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]); void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object(?::text, ?::text)`, [key, value]);
}); });
} }
@@ -109,30 +105,25 @@ export const auditLogDALFactory = (db: TDbClient) => {
if (endDate) { if (endDate) {
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate); void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
} }
const docs = await sqlQuery;
return docs.map((doc) => { // we timeout long running queries to prevent DB resource issues (2 minutes)
// Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above. const docs = await sqlQuery.timeout(1000 * 120);
// This is a quick and dirty way to get around the types.
const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
return { return docs;
...AuditLogsSchema.parse(doc),
...(projectDoc?.projectSlug && {
project: {
name: projectDoc.projectName,
slug: projectDoc.projectSlug
}
})
};
});
} catch (error) { } catch (error) {
if (error instanceof knex.KnexTimeoutError) {
throw new GatewayTimeoutError({
error,
message: "Failed to fetch audit logs due to timeout. Add more search filters."
});
}
throw new DatabaseError({ error }); throw new DatabaseError({ error });
} }
}; };
// delete all audit log that have expired // delete all audit log that have expired
const pruneAuditLog = async (tx?: Knex) => { const pruneAuditLog = async (tx?: knex.Knex) => {
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000; const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3; const MAX_RETRY_ON_FAILURE = 3;
@@ -148,6 +139,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
.where("expiresAt", "<", today) .where("expiresAt", "<", today)
.select("id") .select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE); .limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
deletedAuditLogIds = await (tx || db)(TableName.AuditLog) deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
.whereIn("id", findExpiredLogSubQuery) .whereIn("id", findExpiredLogSubQuery)
@@ -74,6 +74,7 @@ export const auditLogQueueServiceFactory = ({
actorMetadata: actor.metadata, actorMetadata: actor.metadata,
userAgent, userAgent,
projectId, projectId,
projectName: project?.name,
ipAddress, ipAddress,
orgId, orgId,
eventType: event.type, eventType: event.type,
@@ -123,6 +123,7 @@ export enum EventType {
UPDATE_WEBHOOK_STATUS = "update-webhook-status", UPDATE_WEBHOOK_STATUS = "update-webhook-status",
DELETE_WEBHOOK = "delete-webhook", DELETE_WEBHOOK = "delete-webhook",
GET_SECRET_IMPORTS = "get-secret-imports", GET_SECRET_IMPORTS = "get-secret-imports",
GET_SECRET_IMPORT = "get-secret-import",
CREATE_SECRET_IMPORT = "create-secret-import", CREATE_SECRET_IMPORT = "create-secret-import",
UPDATE_SECRET_IMPORT = "update-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import",
DELETE_SECRET_IMPORT = "delete-secret-import", DELETE_SECRET_IMPORT = "delete-secret-import",
@@ -189,7 +190,9 @@ export enum EventType {
DELETE_CMEK = "delete-cmek", DELETE_CMEK = "delete-cmek",
GET_CMEKS = "get-cmeks", GET_CMEKS = "get-cmeks",
CMEK_ENCRYPT = "cmek-encrypt", CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt" CMEK_DECRYPT = "cmek-decrypt",
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping"
} }
interface UserActorMetadata { interface UserActorMetadata {
@@ -1011,6 +1014,14 @@ interface GetSecretImportsEvent {
}; };
} }
interface GetSecretImportEvent {
type: EventType.GET_SECRET_IMPORT;
metadata: {
secretImportId: string;
folderId: string;
};
}
interface CreateSecretImportEvent { interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT; type: EventType.CREATE_SECRET_IMPORT;
metadata: { metadata: {
@@ -1595,6 +1606,18 @@ interface CmekDecryptEvent {
}; };
} }
interface GetExternalGroupOrgRoleMappingsEvent {
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
metadata?: Record<string, never>; // not needed, based off orgId
}
interface UpdateExternalGroupOrgRoleMappingsEvent {
type: EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
metadata: {
mappings: { groupName: string; roleSlug: string }[];
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -1674,6 +1697,7 @@ export type Event =
| UpdateWebhookStatusEvent | UpdateWebhookStatusEvent
| DeleteWebhookEvent | DeleteWebhookEvent
| GetSecretImportsEvent | GetSecretImportsEvent
| GetSecretImportEvent
| CreateSecretImportEvent | CreateSecretImportEvent
| UpdateSecretImportEvent | UpdateSecretImportEvent
| DeleteSecretImportEvent | DeleteSecretImportEvent
@@ -1740,4 +1764,6 @@ export type Event =
| DeleteCmekEvent | DeleteCmekEvent
| GetCmeksEvent | GetCmeksEvent
| CmekEncryptEvent | CmekEncryptEvent
| CmekDecryptEvent; | CmekDecryptEvent
| GetExternalGroupOrgRoleMappingsEvent
| UpdateExternalGroupOrgRoleMappingsEvent;
@@ -1,4 +1,4 @@
import { compile } from "handlebars"; import handlebars from "handlebars";
import ldapjs from "ldapjs"; import ldapjs from "ldapjs";
import ldif from "ldif"; import ldif from "ldif";
import { customAlphabet } from "nanoid"; import { customAlphabet } from "nanoid";
@@ -40,7 +40,7 @@ const generateLDIF = ({
EncodedPassword: encodePassword(password) EncodedPassword: encodePassword(password)
}; };
const renderTemplate = compile(ldifTemplate); const renderTemplate = handlebars.compile(ldifTemplate);
const renderedLdif = renderTemplate(data); const renderedLdif = renderTemplate(data);
return renderedLdif; return renderedLdif;
@@ -1,14 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas";
OrgMembershipRole,
OrgMembershipStatus,
SecretKeyEncoding,
TableName,
TLdapConfigsUpdate,
TUsers
} from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -28,6 +21,7 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -444,11 +438,14 @@ export const ldapConfigServiceFactory = ({
{ tx } { tx }
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgDAL.createMembership( await orgDAL.createMembership(
{ {
userId: userAlias.userId, userId: userAlias.userId,
orgId, orgId,
role: OrgMembershipRole.Member, role,
roleId,
status: OrgMembershipStatus.Accepted, status: OrgMembershipStatus.Accepted,
isActive: true isActive: true
}, },
@@ -529,12 +526,15 @@ export const ldapConfigServiceFactory = ({
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
@@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
@@ -23,6 +23,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -187,12 +188,15 @@ export const oidcConfigServiceFactory = ({
{ tx } { tx }
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, role,
roleId,
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
@@ -261,12 +265,15 @@ export const oidcConfigServiceFactory = ({
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
@@ -2,7 +2,6 @@ import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { import {
OrgMembershipRole,
OrgMembershipStatus, OrgMembershipStatus,
SecretKeyEncoding, SecretKeyEncoding,
TableName, TableName,
@@ -26,6 +25,7 @@ import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -369,12 +369,15 @@ export const samlConfigServiceFactory = ({
{ tx } { tx }
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, role,
roleId,
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
@@ -472,12 +475,15 @@ export const samlConfigServiceFactory = ({
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
+93 -20
View File
@@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { scimPatch } from "scim-patch"; import { scimPatch } from "scim-patch";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -13,9 +13,11 @@ import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } f
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembershipFn } from "@app/services/org/org-fns"; import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -70,7 +72,10 @@ type TScimServiceFactoryDep = {
| "transaction" | "transaction"
| "updateMembershipById" | "updateMembershipById"
>; >;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">; orgMembershipDAL: Pick<
TOrgMembershipDALFactory,
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
>;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick< groupDAL: Pick<
@@ -101,6 +106,7 @@ type TScimServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
}; };
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>; export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
@@ -121,7 +127,8 @@ export const scimServiceFactory = ({
projectBotDAL, projectBotDAL,
permissionService, permissionService,
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
smtpService smtpService,
externalGroupOrgRoleMappingDAL
}: TScimServiceFactoryDep) => { }: TScimServiceFactoryDep) => {
const createScimToken = async ({ const createScimToken = async ({
actor, actor,
@@ -318,12 +325,15 @@ export const scimServiceFactory = ({
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
orgMembership = await orgMembershipDAL.create( orgMembership = await orgMembershipDAL.create(
{ {
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.NoAccess, role,
roleId,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
@@ -391,12 +401,15 @@ export const scimServiceFactory = ({
orgMembership = foundOrgMembership; orgMembership = foundOrgMembership;
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
orgMembership = await orgMembershipDAL.create( orgMembership = await orgMembershipDAL.create(
{ {
userId: user.id, userId: user.id,
inviteEmail: email, inviteEmail: email,
orgId, orgId,
role: OrgMembershipRole.Member, role,
roleId,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
@@ -685,6 +698,43 @@ export const scimServiceFactory = ({
}); });
}; };
const $syncNewMembersRoles = async (group: TGroups, members: TScimGroup["members"]) => {
// this function handles configuring newly provisioned users org membership if an external group mapping exists
if (!members.length) return;
const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({
orgId: group.orgId,
groupName: group.name
});
// no mapping, user will have default org membership
if (!externalGroupMapping) return;
// only get org memberships that are new (invites)
const newOrgMemberships = await orgMembershipDAL.find({
status: "invited",
$in: {
id: members.map((member) => member.value)
}
});
if (!newOrgMemberships.length) return;
// set new membership roles to group mapping value
await orgMembershipDAL.update(
{
$in: {
id: newOrgMemberships.map((membership) => membership.id)
}
},
{
role: externalGroupMapping.role,
roleId: externalGroupMapping.roleId
}
);
};
const createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => { const createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (!plan.groups) if (!plan.groups)
@@ -738,6 +788,8 @@ export const scimServiceFactory = ({
tx tx
}); });
await $syncNewMembersRoles(group, members);
return { group, newMembers }; return { group, newMembers };
} }
@@ -813,22 +865,41 @@ export const scimServiceFactory = ({
orgId: string, orgId: string,
{ displayName, members = [] }: { displayName: string; members: { value: string }[] } { displayName, members = [] }: { displayName: string; members: { value: string }[] }
) => { ) => {
const updatedGroup = await groupDAL.transaction(async (tx) => { let group = await groupDAL.findOne({
const [group] = await groupDAL.update( id: groupId,
{ orgId
id: groupId, });
orgId
},
{
name: displayName
}
);
if (!group) { if (!group) {
throw new ScimRequestError({ throw new ScimRequestError({
detail: "Group Not Found", detail: "Group Not Found",
status: 404 status: 404
}); });
}
const updatedGroup = await groupDAL.transaction(async (tx) => {
if (group.name !== displayName) {
await externalGroupOrgRoleMappingDAL.update(
{
groupName: group.name,
orgId
},
{
groupName: displayName
}
);
const [modifiedGroup] = await groupDAL.update(
{
id: groupId,
orgId
},
{
name: displayName
}
);
group = modifiedGroup;
} }
const orgMemberships = members.length const orgMemberships = members.length
@@ -885,6 +956,8 @@ export const scimServiceFactory = ({
return group; return group;
}); });
await $syncNewMembersRoles(group, members);
return updatedGroup; return updatedGroup;
}; };
@@ -1,6 +1,6 @@
import { ProbotOctokit } from "probot"; import { ProbotOctokit } from "probot";
import { OrgMembershipRole } from "@app/db/schemas"; import { OrgMembershipRole, TableName } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
@@ -61,7 +61,7 @@ export const secretScanningQueueFactory = ({
const getOrgAdminEmails = async (organizationId: string) => { const getOrgAdminEmails = async (organizationId: string) => {
// get emails of admins // get emails of admins
const adminsOfWork = await orgMemberDAL.findMembership({ const adminsOfWork = await orgMemberDAL.findMembership({
orgId: organizationId, [`${TableName.Organization}.id` as string]: organizationId,
role: OrgMembershipRole.Admin role: OrgMembershipRole.Admin
}); });
return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string); return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string);
@@ -90,7 +90,7 @@ export const secretScanningServiceFactory = ({
const { const {
data: { repositories } data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation(); } = await octokit.apps.listReposAccessibleToInstallation();
if (!appCfg.DISABLE_SECRET_SCANNING) { if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
await Promise.all( await Promise.all(
repositories.map(({ id, full_name }) => repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({ secretScanningQueue.startFullRepoScan({
@@ -164,7 +164,7 @@ export const secretScanningServiceFactory = ({
}); });
if (!installationLink) return; if (!installationLink) return;
if (!appCfg.DISABLE_SECRET_SCANNING) { if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
await secretScanningQueue.startPushEventScan({ await secretScanningQueue.startPushEventScan({
commits, commits,
pusher: { name: pusher.name, email: pusher.email }, pusher: { name: pusher.name, email: pusher.email },
@@ -240,7 +240,8 @@ export const secretSnapshotServiceFactory = ({
}, },
tx tx
); );
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
const snapshotSecrets = await snapshotSecretV2BridgeDAL.batchInsert(
secretVersions.map(({ id }) => ({ secretVersions.map(({ id }) => ({
secretVersionId: id, secretVersionId: id,
envId: folder.environment.envId, envId: folder.environment.envId,
@@ -248,7 +249,8 @@ export const secretSnapshotServiceFactory = ({
})), })),
tx tx
); );
const snapshotFolders = await snapshotFolderDAL.insertMany(
const snapshotFolders = await snapshotFolderDAL.batchInsert(
folderVersions.map(({ id }) => ({ folderVersions.map(({ id }) => ({
folderVersionId: id, folderVersionId: id,
envId: folder.environment.envId, envId: folder.environment.envId,
+5 -1
View File
@@ -533,7 +533,8 @@ export const ENVIRONMENTS = {
CREATE: { CREATE: {
workspaceId: "The ID of the project to create the environment in.", workspaceId: "The ID of the project to create the environment in.",
name: "The name of the environment to create.", name: "The name of the environment to create.",
slug: "The slug of the environment to create." slug: "The slug of the environment to create.",
position: "The position of the environment. The lowest number will be displayed as the first environment."
}, },
UPDATE: { UPDATE: {
workspaceId: "The ID of the project to update the environment in.", workspaceId: "The ID of the project to update the environment in.",
@@ -675,6 +676,9 @@ export const SECRET_IMPORTS = {
environment: "The slug of the environment to list secret imports from.", environment: "The slug of the environment to list secret imports from.",
path: "The path to list secret imports from." path: "The path to list secret imports from."
}, },
GET: {
secretImportId: "The ID of the secret import to fetch."
},
CREATE: { CREATE: {
environment: "The slug of the environment to import into.", environment: "The slug of the environment to import into.",
path: "The path to import into.", path: "The path to import into.",
+17 -2
View File
@@ -34,6 +34,12 @@ const envSchema = z
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default( DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
`postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`
), ),
AUDIT_LOGS_DB_CONNECTION_URI: zpStr(
z.string().describe("Postgres database connection string for Audit logs").optional()
),
AUDIT_LOGS_DB_ROOT_CERT: zpStr(
z.string().describe("Postgres database base64-encoded CA cert for Audit logs").optional()
),
MAX_LEASE_LIMIT: z.coerce.number().default(10000), MAX_LEASE_LIMIT: z.coerce.number().default(10000),
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()), DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()), DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
@@ -111,9 +117,16 @@ const envSchema = z
// gcp secret manager // gcp secret manager
CLIENT_ID_GCP_SECRET_MANAGER: zpStr(z.string().optional()), CLIENT_ID_GCP_SECRET_MANAGER: zpStr(z.string().optional()),
CLIENT_SECRET_GCP_SECRET_MANAGER: zpStr(z.string().optional()), CLIENT_SECRET_GCP_SECRET_MANAGER: zpStr(z.string().optional()),
// github // github oauth
CLIENT_ID_GITHUB: zpStr(z.string().optional()), CLIENT_ID_GITHUB: zpStr(z.string().optional()),
CLIENT_SECRET_GITHUB: zpStr(z.string().optional()), CLIENT_SECRET_GITHUB: zpStr(z.string().optional()),
// github app
CLIENT_ID_GITHUB_APP: zpStr(z.string().optional()),
CLIENT_SECRET_GITHUB_APP: zpStr(z.string().optional()),
CLIENT_PRIVATE_KEY_GITHUB_APP: zpStr(z.string().optional()),
CLIENT_APP_ID_GITHUB_APP: z.coerce.number().optional(),
CLIENT_SLUG_GITHUB_APP: zpStr(z.string().optional()),
// azure // azure
CLIENT_ID_AZURE: zpStr(z.string().optional()), CLIENT_ID_AZURE: zpStr(z.string().optional()),
CLIENT_SECRET_AZURE: zpStr(z.string().optional()), CLIENT_SECRET_AZURE: zpStr(z.string().optional()),
@@ -129,6 +142,7 @@ const envSchema = z
SECRET_SCANNING_WEBHOOK_SECRET: zpStr(z.string().optional()), SECRET_SCANNING_WEBHOOK_SECRET: zpStr(z.string().optional()),
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()), SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()), SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()),
// LICENSE // LICENSE
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")), LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
LICENSE_SERVER_KEY: zpStr(z.string().optional()), LICENSE_SERVER_KEY: zpStr(z.string().optional()),
@@ -164,7 +178,8 @@ const envSchema = z
Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET), Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
})); }));
let envCfg: Readonly<z.infer<typeof envSchema>>; let envCfg: Readonly<z.infer<typeof envSchema>>;
+19
View File
@@ -23,6 +23,18 @@ export class InternalServerError extends Error {
} }
} }
export class GatewayTimeoutError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message || "Timeout error");
this.name = name || "GatewayTimeoutError";
this.error = error;
}
}
export class UnauthorizedError extends Error { export class UnauthorizedError extends Error {
name: string; name: string;
@@ -59,6 +71,13 @@ export class BadRequestError extends Error {
} }
} }
export class RateLimitError extends Error {
constructor({ message }: { message?: string }) {
super(message || "Rate limit exceeded");
this.name = "RateLimitExceeded";
}
}
export class NotFoundError extends Error { export class NotFoundError extends Error {
name: string; name: string;
+11
View File
@@ -70,3 +70,14 @@ export const objectify = <T, Key extends string | number | symbol, Value = T>(
{} as Record<Key, Value> {} as Record<Key, Value>
); );
}; };
/**
* Chunks an array into smaller arrays of the given size.
*/
export const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
};
+29 -25
View File
@@ -8,12 +8,14 @@ const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
return filter; return filter;
}; };
export const generateKnexQueryFromScim = ( const processDynamicQuery = (
rootQuery: Knex.QueryBuilder, rootQuery: Knex.QueryBuilder,
rootScimFilter: string, scimRootFilterAst: Filter,
getAttributeField: (attr: string) => string | null getAttributeField: (attr: string) => string | null,
depth = 0
) => { ) => {
const scimRootFilterAst = parse(rootScimFilter); if (depth > 20) return;
const stack = [ const stack = [
{ {
scimFilterAst: scimRootFilterAst, scimFilterAst: scimRootFilterAst,
@@ -75,42 +77,35 @@ export const generateKnexQueryFromScim = (
break; break;
} }
case "and": { case "and": {
void query.andWhere((subQueryBuilder) => { scimFilterAst.filters.forEach((el) => {
scimFilterAst.filters.forEach((el) => { void query.andWhere((subQueryBuilder) => {
stack.push({ processDynamicQuery(subQueryBuilder, el, getAttributeField, depth + 1);
query: subQueryBuilder,
scimFilterAst: el
});
}); });
}); });
break; break;
} }
case "or": { case "or": {
void query.orWhere((subQueryBuilder) => { scimFilterAst.filters.forEach((el) => {
scimFilterAst.filters.forEach((el) => { void query.orWhere((subQueryBuilder) => {
stack.push({ processDynamicQuery(subQueryBuilder, el, getAttributeField, depth + 1);
query: subQueryBuilder,
scimFilterAst: el
});
}); });
}); });
break; break;
} }
case "not": { case "not": {
void query.whereNot((subQueryBuilder) => { void query.whereNot((subQueryBuilder) => {
stack.push({ processDynamicQuery(subQueryBuilder, scimFilterAst.filter, getAttributeField, depth + 1);
query: subQueryBuilder,
scimFilterAst: scimFilterAst.filter
});
}); });
break; break;
} }
case "[]": { case "[]": {
void query.whereNot((subQueryBuilder) => { void query.where((subQueryBuilder) => {
stack.push({ processDynamicQuery(
query: subQueryBuilder, subQueryBuilder,
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter) appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter),
}); getAttributeField,
depth + 1
);
}); });
break; break;
} }
@@ -119,3 +114,12 @@ export const generateKnexQueryFromScim = (
} }
} }
}; };
export const generateKnexQueryFromScim = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
) => {
const scimRootFilterAst = parse(rootScimFilter);
return processDynamicQuery(rootQuery, scimRootFilterAst, getAttributeField);
};
+9 -2
View File
@@ -1,7 +1,7 @@
import dotenv from "dotenv"; import dotenv from "dotenv";
import path from "path"; import path from "path";
import { initDbConnection } from "./db"; import { initAuditLogDbConnection, initDbConnection } from "./db";
import { keyStoreFactory } from "./keystore/keystore"; import { keyStoreFactory } from "./keystore/keystore";
import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env"; import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env";
import { isMigrationMode } from "./lib/fn"; import { isMigrationMode } from "./lib/fn";
@@ -25,6 +25,13 @@ const run = async () => {
})) }))
}); });
const auditLogDb = appCfg.AUDIT_LOGS_DB_CONNECTION_URI
? initAuditLogDbConnection({
dbConnectionUri: appCfg.AUDIT_LOGS_DB_CONNECTION_URI,
dbRootCert: appCfg.AUDIT_LOGS_DB_ROOT_CERT
})
: undefined;
// Case: App is running in packaged mode (binary), and migration mode is enabled. // Case: App is running in packaged mode (binary), and migration mode is enabled.
// Run the migrations and exit the process after completion. // Run the migrations and exit the process after completion.
if (IS_PACKAGED && isMigrationMode()) { if (IS_PACKAGED && isMigrationMode()) {
@@ -46,7 +53,7 @@ const run = async () => {
const queue = queueServiceFactory(appCfg.REDIS_URL); const queue = queueServiceFactory(appCfg.REDIS_URL);
const keyStore = keyStoreFactory(appCfg.REDIS_URL); const keyStore = keyStoreFactory(appCfg.REDIS_URL);
const server = await main({ db, smtp, logger, queue, keyStore }); const server = await main({ db, auditLogDb, smtp, logger, queue, keyStore });
const bootstrap = await bootstrapCheck({ db }); const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line // eslint-disable-next-line
+18 -3
View File
@@ -1,7 +1,7 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq"; import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis"; import Redis from "ioredis";
import { SecretKeyEncoding } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { import {
TScanFullRepoEventPayload, TScanFullRepoEventPayload,
@@ -32,7 +32,8 @@ export enum QueueName {
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration", ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update" AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
} }
export enum QueueJobs { export enum QueueJobs {
@@ -56,7 +57,8 @@ export enum QueueJobs {
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration", ProjectV3Migration = "project-v3-migration",
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update", IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
ServiceTokenStatusUpdate = "service-token-status-update" ServiceTokenStatusUpdate = "service-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@@ -166,6 +168,19 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration; name: QueueJobs.ProjectV3Migration;
payload: { projectId: string }; payload: { projectId: string };
}; };
[QueueName.ImportSecretsFromExternalSource]: {
name: QueueJobs.ImportSecretsFromExternalSource;
payload: {
actorEmail: string;
data: {
iv: string;
tag: string;
ciphertext: string;
algorithm: SecretEncryptionAlgo;
encoding: SecretKeyEncoding;
};
};
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
+3 -2
View File
@@ -30,6 +30,7 @@ import { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes"; import { registerRoutes } from "./routes";
type TMain = { type TMain = {
auditLogDb?: Knex;
db: Knex; db: Knex;
smtp: TSmtpService; smtp: TSmtpService;
logger?: Logger; logger?: Logger;
@@ -38,7 +39,7 @@ type TMain = {
}; };
// Run the server! // Run the server!
export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => { export const main = async ({ db, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
const appCfg = getConfig(); const appCfg = getConfig();
const server = fastify({ const server = fastify({
logger: appCfg.NODE_ENV === "test" ? false : logger, logger: appCfg.NODE_ENV === "test" ? false : logger,
@@ -94,7 +95,7 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
await server.register(maintenanceMode); await server.register(maintenanceMode);
await server.register(registerRoutes, { smtp, queue, db, keyStore }); await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore });
if (appCfg.isProductionMode) { if (appCfg.isProductionMode) {
await server.register(registerExternalNextjs, { await server.register(registerExternalNextjs, {
+6
View File
@@ -2,6 +2,7 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-lim
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { RateLimitError } from "@app/lib/errors";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => { export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -10,6 +11,11 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
: null; : null;
return { return {
errorResponseBuilder: (_, context) => {
throw new RateLimitError({
message: `Rate limit exceeded. Please try again in ${context.after}`
});
},
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: 600, max: 600,
redis, redis,
@@ -3,9 +3,12 @@ import fp from "fastify-plugin";
import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas"; import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas";
const isScimRoutes = (pathname: string) =>
pathname.startsWith("/api/v1/scim/Users") || pathname.startsWith("/api/v1/scim/Groups");
export const addErrorsToResponseSchemas = fp(async (server) => { export const addErrorsToResponseSchemas = fp(async (server) => {
server.addHook("onRoute", (routeOptions) => { server.addHook("onRoute", (routeOptions) => {
if (routeOptions.schema && routeOptions.schema.response) { if (routeOptions.schema && routeOptions.schema.response && !isScimRoutes(routeOptions.path)) {
routeOptions.schema.response = { routeOptions.schema.response = {
...DefaultResponseErrorsSchema, ...DefaultResponseErrorsSchema,
...routeOptions.schema.response ...routeOptions.schema.response
+20 -2
View File
@@ -7,8 +7,10 @@ import {
BadRequestError, BadRequestError,
DatabaseError, DatabaseError,
ForbiddenRequestError, ForbiddenRequestError,
GatewayTimeoutError,
InternalServerError, InternalServerError,
NotFoundError, NotFoundError,
RateLimitError,
ScimRequestError, ScimRequestError,
UnauthorizedError UnauthorizedError
} from "@app/lib/errors"; } from "@app/lib/errors";
@@ -25,7 +27,9 @@ enum HttpStatusCodes {
Unauthorized = 401, Unauthorized = 401,
Forbidden = 403, Forbidden = 403,
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
InternalServerError = 500 InternalServerError = 500,
GatewayTimeout = 504,
TooManyRequests = 429
} }
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => { export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
@@ -47,6 +51,10 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
void res void res
.status(HttpStatusCodes.InternalServerError) .status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name }); .send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name });
} else if (error instanceof GatewayTimeoutError) {
void res
.status(HttpStatusCodes.GatewayTimeout)
.send({ statusCode: HttpStatusCodes.GatewayTimeout, message: error.message, error: error.name });
} else if (error instanceof ZodError) { } else if (error instanceof ZodError) {
void res void res
.status(HttpStatusCodes.Unauthorized) .status(HttpStatusCodes.Unauthorized)
@@ -63,6 +71,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
message: error.message, message: error.message,
error: error.name error: error.name
}); });
} else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({
statusCode: HttpStatusCodes.TooManyRequests,
message: error.message,
error: error.name
});
} else if (error instanceof ScimRequestError) { } else if (error instanceof ScimRequestError) {
void res.status(error.status).send({ void res.status(error.status).send({
schemas: error.schemas, schemas: error.schemas,
@@ -91,7 +105,11 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
message message
}); });
} else { } else {
void res.send(error); void res.status(HttpStatusCodes.InternalServerError).send({
statusCode: HttpStatusCodes.InternalServerError,
error: "InternalServerError",
message: "Something went wrong"
});
} }
}); });
}); });
+53 -15
View File
@@ -97,6 +97,9 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal"; import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
@@ -214,16 +217,15 @@ import { registerV3Routes } from "./v3";
export const registerRoutes = async ( export const registerRoutes = async (
server: FastifyZodProvider, server: FastifyZodProvider,
{ {
auditLogDb,
db, db,
smtp: smtpService, smtp: smtpService,
queue: queueService, queue: queueService,
keyStore keyStore
}: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory } }: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
) => { ) => {
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.DISABLE_SECRET_SCANNING) { await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
}
// db layers // db layers
const userDAL = userDALFactory(db); const userDAL = userDALFactory(db);
@@ -283,7 +285,7 @@ export const registerRoutes = async (
const identityOidcAuthDAL = identityOidcAuthDALFactory(db); const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(db); const auditLogDAL = auditLogDALFactory(auditLogDb ?? db);
const auditLogStreamDAL = auditLogStreamDALFactory(db); const auditLogStreamDAL = auditLogStreamDALFactory(db);
const trustedIpDAL = trustedIpDALFactory(db); const trustedIpDAL = trustedIpDALFactory(db);
const telemetryDAL = telemetryDALFactory(db); const telemetryDAL = telemetryDALFactory(db);
@@ -334,6 +336,8 @@ export const registerRoutes = async (
const projectSlackConfigDAL = projectSlackConfigDALFactory(db); const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
const workflowIntegrationDAL = workflowIntegrationDALFactory(db); const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
orgRoleDAL, orgRoleDAL,
@@ -440,7 +444,8 @@ export const registerRoutes = async (
projectKeyDAL, projectKeyDAL,
projectBotDAL, projectBotDAL,
permissionService, permissionService,
smtpService smtpService,
externalGroupOrgRoleMappingDAL
}); });
const ldapService = ldapConfigServiceFactory({ const ldapService = ldapConfigServiceFactory({
@@ -491,6 +496,9 @@ export const registerRoutes = async (
authDAL, authDAL,
userDAL userDAL
}); });
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
const orgService = orgServiceFactory({ const orgService = orgServiceFactory({
userAliasDAL, userAliasDAL,
identityMetadataDAL, identityMetadataDAL,
@@ -513,7 +521,8 @@ export const registerRoutes = async (
userDAL, userDAL,
groupDAL, groupDAL,
orgBotDAL, orgBotDAL,
oidcConfigDAL oidcConfigDAL,
projectBotService
}); });
const signupService = authSignupServiceFactory({ const signupService = authSignupServiceFactory({
tokenService, tokenService,
@@ -531,7 +540,12 @@ export const registerRoutes = async (
orgService, orgService,
licenseService licenseService
}); });
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL }); const orgRoleService = orgRoleServiceFactory({
permissionService,
orgRoleDAL,
orgDAL,
externalGroupOrgRoleMappingDAL
});
const superAdminService = superAdminServiceFactory({ const superAdminService = superAdminServiceFactory({
userDAL, userDAL,
authService: loginService, authService: loginService,
@@ -572,7 +586,6 @@ export const registerRoutes = async (
secretScanningDAL, secretScanningDAL,
secretScanningQueue secretScanningQueue
}); });
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
const projectMembershipService = projectMembershipServiceFactory({ const projectMembershipService = projectMembershipServiceFactory({
projectMembershipDAL, projectMembershipDAL,
@@ -836,7 +849,10 @@ export const registerRoutes = async (
integrationAuthDAL, integrationAuthDAL,
snapshotDAL, snapshotDAL,
snapshotSecretV2BridgeDAL, snapshotSecretV2BridgeDAL,
secretApprovalRequestDAL secretApprovalRequestDAL,
projectKeyDAL,
projectUserMembershipRoleDAL,
orgService
}); });
const secretImportService = secretImportServiceFactory({ const secretImportService = secretImportServiceFactory({
licenseService, licenseService,
@@ -1201,12 +1217,33 @@ export const registerRoutes = async (
permissionService permissionService
}); });
const migrationService = externalMigrationServiceFactory({ const externalMigrationQueue = externalMigrationQueueFactory({
projectService,
orgService,
projectEnvService, projectEnvService,
projectDAL,
projectService,
smtpService,
kmsService,
projectEnvDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderDAL,
secretDAL: secretV2BridgeDAL,
queueService,
secretV2BridgeService
});
const migrationService = externalMigrationServiceFactory({
externalMigrationQueue,
userDAL,
permissionService
});
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
permissionService, permissionService,
secretService licenseService,
orgRoleDAL,
externalGroupOrgRoleMappingDAL
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
@@ -1294,7 +1331,8 @@ export const registerRoutes = async (
orgAdmin: orgAdminService, orgAdmin: orgAdminService,
slack: slackService, slack: slackService,
workflowIntegration: workflowIntegrationService, workflowIntegration: workflowIntegrationService,
migration: migrationService migration: migrationService,
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService
}); });
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];
+2 -1
View File
@@ -109,7 +109,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
firstName: true, firstName: true,
lastName: true, lastName: true,
email: true, email: true,
id: true id: true,
superAdmin: true
}).array() }).array()
}) })
} }
@@ -0,0 +1,83 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerExternalGroupOrgRoleMappingRouter = async (server: FastifyZodProvider) => {
// get mappings for current org
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: ExternalGroupOrgRoleMappingsSchema.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const mappings = server.services.externalGroupOrgRoleMapping.listExternalGroupOrgRoleMappings(req.permission);
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS
}
});
return mappings;
}
});
// update mappings for current org
server.route({
method: "PUT", // using put since this endpoint creates, updates and deletes mappings
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
mappings: z
.object({
groupName: z.string().trim().min(1),
roleSlug: z
.string()
.min(1)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Role must be a valid slug"
})
})
.array()
}),
response: {
200: ExternalGroupOrgRoleMappingsSchema.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { body, permission } = req;
const mappings = server.services.externalGroupOrgRoleMapping.updateExternalGroupOrgRoleMappings(body, permission);
await server.services.auditLog.createAuditLog({
orgId: permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS,
metadata: body
}
});
return mappings;
}
});
};
@@ -22,7 +22,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
description: "Login with AWS Auth", description: "Login with AWS Auth",
body: z.object({ body: z.object({
identityId: z.string().describe(AWS_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId),
iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod), iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod),
iamRequestBody: z.string().describe(AWS_AUTH.LOGIN.iamRequestBody), iamRequestBody: z.string().describe(AWS_AUTH.LOGIN.iamRequestBody),
iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders) iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders)
@@ -21,7 +21,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
description: "Login with Azure Auth", description: "Login with Azure Auth",
body: z.object({ body: z.object({
identityId: z.string().describe(AZURE_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId),
jwt: z.string() jwt: z.string()
}), }),
response: { response: {
@@ -19,7 +19,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
description: "Login with GCP Auth", description: "Login with GCP Auth",
body: z.object({ body: z.object({
identityId: z.string().describe(GCP_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId).trim(),
jwt: z.string() jwt: z.string()
}), }),
response: { response: {
+2
View File
@@ -7,6 +7,7 @@ import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { registerCertRouter } from "./certificate-router"; import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
@@ -106,4 +107,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
await server.register(registerDashboardRouter, { prefix: "/dashboard" }); await server.register(registerDashboardRouter, { prefix: "/dashboard" });
await server.register(registerCmekRouter, { prefix: "/kms" }); await server.register(registerCmekRouter, { prefix: "/kms" });
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
}; };
@@ -189,6 +189,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
code: z.string().trim(), code: z.string().trim(),
integration: z.string().trim(), integration: z.string().trim(),
installationId: z.string().trim().optional(),
url: z.string().trim().url().optional() url: z.string().trim().url().optional()
}), }),
response: { response: {
@@ -452,6 +453,40 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
} }
}); });
server.route({
method: "POST",
url: "/:integrationAuthId/duplicate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
body: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
integrationAuth: integrationAuthPubSchema
})
}
},
handler: async (req) => {
const integrationAuth = await server.services.integrationAuth.duplicateIntegrationAuth({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.integrationAuthId,
projectId: req.body.projectId
});
return { integrationAuth };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:integrationAuthId/github/envs", url: "/:integrationAuthId/github/envs",
@@ -52,7 +52,13 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
integration: IntegrationsSchema integration: IntegrationsSchema.extend({
environment: z.object({
slug: z.string().trim(),
name: z.string().trim(),
id: z.string().trim()
})
})
}) })
} }
}, },
@@ -138,7 +144,13 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
integration: IntegrationsSchema integration: IntegrationsSchema.extend({
environment: z.object({
slug: z.string().trim(),
name: z.string().trim(),
id: z.string().trim()
})
})
}) })
} }
}, },
@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { import {
@@ -11,13 +12,13 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getLastMidnightDateISO } from "@app/lib/fn"; import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { integrationAuthPubSchema } from "../sanitizedSchemas";
export const registerOrgRouter = async (server: FastifyZodProvider) => { export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
@@ -69,6 +70,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/:organizationId/integration-authorizations",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
organizationId: z.string().trim()
}),
response: {
200: z.object({
authorizations: integrationAuthPubSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const authorizations = await server.services.integrationAuth.listOrgIntegrationAuth({
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
return { authorizations };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/audit-logs", url: "/audit-logs",
@@ -125,12 +155,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}) })
.merge( .merge(
z.object({ z.object({
project: z
.object({
name: z.string(),
slug: z.string()
})
.optional(),
event: z.object({ event: z.object({
type: z.string(), type: z.string(),
metadata: z.any() metadata: z.any()
@@ -145,13 +169,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const appCfg = getConfig();
if (appCfg.isCloud) {
throw new BadRequestError({ message: "Infisical cloud audit log is in maintenance mode." });
}
const auditLogs = await server.services.auditLog.listAuditLogs({ const auditLogs = await server.services.auditLog.listAuditLogs({
filter: { filter: {
...req.query, ...req.query,
@@ -168,6 +187,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type actor: req.permission.type
}); });
return { auditLogs }; return { auditLogs };
} }
}); });
@@ -191,7 +211,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
email: true, email: true,
firstName: true, firstName: true,
lastName: true, lastName: true,
id: true id: true,
superAdmin: true
}).merge(z.object({ publicKey: z.string().nullable() })) }).merge(z.object({ publicKey: z.string().nullable() }))
}) })
) )
@@ -229,7 +250,15 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens") .regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens")
.optional(), .optional(),
authEnforced: z.boolean().optional(), authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional() scimEnabled: z.boolean().optional(),
defaultMembershipRoleSlug: z
.string()
.min(1)
.trim()
.refine((v) => slugify(v) === v, {
message: "Membership role must be a valid slug"
})
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -65,7 +65,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig(); const appCfg = getConfig();
await server.services.password.changePassword({ ...req.body, userId: req.permission.id }); await server.services.password.changePassword({ ...req.body, userId: req.permission.id });
void res.cookie("jid", appCfg.COOKIE_SECRET_SIGN_KEY, { void res.cookie("jid", "", {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
@@ -4,7 +4,7 @@ import { z } from "zod";
import { ProjectEnvironmentsSchema } from "@app/db/schemas"; import { ProjectEnvironmentsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ENVIRONMENTS } from "@app/lib/api-docs"; import { ENVIRONMENTS } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -23,6 +23,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
// NOTE(daniel): workspaceId isn't used, but we need to keep it for backwards compatibility. The endpoint defined below, uses no project ID, and is takes a pure environment ID.
workspaceId: z.string().trim().describe(ENVIRONMENTS.GET.workspaceId), workspaceId: z.string().trim().describe(ENVIRONMENTS.GET.workspaceId),
envId: z.string().trim().describe(ENVIRONMENTS.GET.id) envId: z.string().trim().describe(ENVIRONMENTS.GET.id)
}), }),
@@ -39,7 +40,53 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId, id: req.params.envId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: environment.projectId,
event: {
type: EventType.GET_ENVIRONMENT,
metadata: {
id: environment.id
}
}
});
return { environment };
}
});
server.route({
method: "GET",
url: "/environments/:envId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get Environment by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
envId: z.string().trim().describe(ENVIRONMENTS.GET.id)
}),
response: {
200: z.object({
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const environment = await server.services.projectEnv.getEnvironmentById({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
id: req.params.envId id: req.params.envId
}); });
@@ -76,6 +123,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
}), }),
body: z.object({ body: z.object({
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name), name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position),
slug: z slug: z
.string() .string()
.trim() .trim()
@@ -365,7 +365,15 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
}), }),
response: { response: {
200: z.object({ 200: z.object({
folder: SecretFoldersSchema folder: SecretFoldersSchema.extend({
environment: z.object({
envId: z.string(),
envName: z.string(),
envSlug: z.string()
}),
path: z.string(),
projectId: z.string()
})
}) })
} }
}, },
@@ -312,6 +312,64 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
} }
}); });
server.route({
url: "/:secretImportId",
method: "GET",
config: {
rateLimit: readLimit
},
schema: {
description: "Get single secret import",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretImportId: z.string().trim().describe(SECRET_IMPORTS.GET.secretImportId)
}),
response: {
200: z.object({
secretImport: SecretImportsSchema.omit({ importEnv: true }).extend({
environment: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
projectId: z.string(),
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }),
secretPath: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secretImport = await server.services.secretImport.getImportById({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.secretImportId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretImport.projectId,
event: {
type: EventType.GET_SECRET_IMPORT,
metadata: {
secretImportId: secretImport.id,
folderId: secretImport.folderId
}
}
});
return { secretImport };
}
});
server.route({ server.route({
url: "/secrets", url: "/secrets",
method: "GET", method: "GET",
@@ -1,30 +1,50 @@
import { z } from "zod"; import fastifyMultipart from "@fastify/multipart";
import { BadRequestError } from "@app/lib/errors";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
const MB25_IN_BYTES = 26214400;
export const registerExternalMigrationRouter = async (server: FastifyZodProvider) => { export const registerExternalMigrationRouter = async (server: FastifyZodProvider) => {
await server.register(fastifyMultipart);
server.route({ server.route({
method: "POST", method: "POST",
bodyLimit: MB25_IN_BYTES,
url: "/env-key", url: "/env-key",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: {
body: z.object({
decryptionKey: z.string().trim().min(1),
encryptedJson: z.object({
nonce: z.string().trim().min(1),
data: z.string().trim().min(1)
})
})
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const data = await req.file({
limits: {
fileSize: MB25_IN_BYTES
}
});
if (!data) {
throw new BadRequestError({ message: "No file provided" });
}
const fullFile = Buffer.from(await data.toBuffer()).toString("utf8");
const parsedJsonFile = JSON.parse(fullFile) as { nonce: string; data: string };
const decryptionKey = (data.fields.decryptionKey as { value: string }).value;
if (!parsedJsonFile.nonce || !parsedJsonFile.data) {
throw new BadRequestError({ message: "Invalid file format. Nonce or data missing." });
}
if (!decryptionKey) {
throw new BadRequestError({ message: "Decryption key is required" });
}
await server.services.migration.importEnvKeyData({ await server.services.migration.importEnvKeyData({
decryptionKey: req.body.decryptionKey, decryptionKey,
encryptedJson: req.body.encryptedJson, encryptedJson: parsedJsonFile,
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
@@ -0,0 +1,46 @@
import { Tables } from "knex/types/tables";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TExternalGroupOrgRoleMappings } from "@app/db/schemas/external-group-org-role-mappings";
import { ormify } from "@app/lib/knex";
export type TExternalGroupOrgRoleMappingDALFactory = ReturnType<typeof externalGroupOrgRoleMappingDALFactory>;
export const externalGroupOrgRoleMappingDALFactory = (db: TDbClient) => {
const externalGroupOrgRoleMappingOrm = ormify(db, TableName.ExternalGroupOrgRoleMapping);
const updateExternalGroupOrgRoleMappingForOrg = async (
orgId: string,
newMappings: readonly Tables[TableName.ExternalGroupOrgRoleMapping]["insert"][]
) => {
const currentMappings = await externalGroupOrgRoleMappingOrm.find({ orgId });
const newMap = new Map(newMappings.map((mapping) => [mapping.groupName, mapping]));
const currentMap = new Map(currentMappings.map((mapping) => [mapping.groupName, mapping]));
const mappingsToDelete = currentMappings.filter((mapping) => !newMap.has(mapping.groupName));
const mappingsToUpdate = currentMappings
.filter((mapping) => newMap.has(mapping.groupName))
.map((mapping) => ({ id: mapping.id, ...newMap.get(mapping.groupName) }));
const mappingsToInsert = newMappings.filter((mapping) => !currentMap.has(mapping.groupName));
const mappings = await externalGroupOrgRoleMappingOrm.transaction(async (tx) => {
await externalGroupOrgRoleMappingOrm.delete({ $in: { id: mappingsToDelete.map((mapping) => mapping.id) } }, tx);
const updatedMappings: TExternalGroupOrgRoleMappings[] = [];
for await (const { id, ...mappingData } of mappingsToUpdate) {
const updatedMapping = await externalGroupOrgRoleMappingOrm.update({ id }, mappingData, tx);
updatedMappings.push(updatedMapping[0]);
}
const insertedMappings = await externalGroupOrgRoleMappingOrm.insertMany(mappingsToInsert, tx);
return [...updatedMappings, ...insertedMappings];
});
return mappings;
};
return { ...externalGroupOrgRoleMappingOrm, updateExternalGroupOrgRoleMappingForOrg };
};
@@ -0,0 +1,67 @@
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { isCustomOrgRole } from "@app/services/org/org-role-fns";
import { TExternalGroupOrgMembershipRoleMappingDTO } from "./external-group-org-role-mapping-types";
export const constructGroupOrgMembershipRoleMappings = async ({
mappingsDTO,
orgId,
orgRoleDAL,
licenseService
}: {
mappingsDTO: TExternalGroupOrgMembershipRoleMappingDTO[];
orgRoleDAL: TOrgRoleDALFactory;
licenseService: TLicenseServiceFactory;
orgId: string;
}) => {
const plan = await licenseService.getPlan(orgId);
// prevent setting custom values if not in plan
if (mappingsDTO.some((map) => isCustomOrgRole(map.roleSlug)) && !plan?.rbac)
throw new BadRequestError({
message:
"Failed to set group organization role mapping due to plan RBAC restriction. Upgrade plan to set custom role mapping."
});
const customRoleSlugs = mappingsDTO
.filter((mapping) => isCustomOrgRole(mapping.roleSlug))
.map((mapping) => mapping.roleSlug);
let customRolesMap: Map<string, TOrgRoles> = new Map();
if (customRoleSlugs.length > 0) {
const customRoles = await orgRoleDAL.find({
$in: {
slug: customRoleSlugs
}
});
customRolesMap = new Map(customRoles.map((role) => [role.slug, role]));
}
const mappings = mappingsDTO.map(({ roleSlug, groupName }) => {
if (isCustomOrgRole(roleSlug)) {
const customRole = customRolesMap.get(roleSlug);
if (!customRole) throw new NotFoundError({ message: `Custom role ${roleSlug} not found.` });
return {
groupName,
role: OrgMembershipRole.Custom,
roleId: customRole.id,
orgId
};
}
return {
groupName,
role: roleSlug,
roleId: null, // need to set explicitly null for updates
orgId
};
});
return mappings;
};
@@ -0,0 +1,78 @@
import { ForbiddenError } from "@casl/ability";
import { FastifyRequest } from "fastify";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns";
import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { TExternalGroupOrgRoleMappingDALFactory } from "./external-group-org-role-mapping-dal";
type TExternalGroupOrgRoleMappingServiceFactoryDep = {
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
permissionService: TPermissionServiceFactory;
licenseService: TLicenseServiceFactory;
orgRoleDAL: TOrgRoleDALFactory;
};
export type TExternalGroupOrgRoleMappingServiceFactory = ReturnType<typeof externalGroupOrgRoleMappingServiceFactory>;
export const externalGroupOrgRoleMappingServiceFactory = ({
externalGroupOrgRoleMappingDAL,
licenseService,
permissionService,
orgRoleDAL
}: TExternalGroupOrgRoleMappingServiceFactoryDep) => {
const listExternalGroupOrgRoleMappings = async (actor: FastifyRequest["permission"]) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
// TODO: will need to change if we add support for ldap, oidc, etc.
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
const mappings = await externalGroupOrgRoleMappingDAL.find({
orgId: actor.orgId
});
return mappings;
};
const updateExternalGroupOrgRoleMappings = async (
dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO,
actor: FastifyRequest["permission"]
) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
// TODO: will need to change if we add support for ldap, oidc, etc.
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
const mappings = await constructGroupOrgMembershipRoleMappings({
mappingsDTO: dto.mappings,
orgRoleDAL,
licenseService,
orgId: actor.orgId
});
const data = await externalGroupOrgRoleMappingDAL.updateExternalGroupOrgRoleMappingForOrg(actor.orgId, mappings);
return data;
};
return {
updateExternalGroupOrgRoleMappings,
listExternalGroupOrgRoleMappings
};
};
@@ -0,0 +1,8 @@
export type TExternalGroupOrgMembershipRoleMappingDTO = {
groupName: string;
roleSlug: string;
};
export type TSyncExternalGroupOrgMembershipRoleMappingsDTO = {
mappings: TExternalGroupOrgMembershipRoleMappingDTO[];
};
@@ -4,22 +4,41 @@ import sjcl from "sjcl";
import tweetnacl from "tweetnacl"; import tweetnacl from "tweetnacl";
import tweetnaclUtil from "tweetnacl-util"; import tweetnaclUtil from "tweetnacl-util";
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas"; import { SecretType } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { chunkArray } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgServiceFactory } from "../org/org-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service"; import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretServiceFactory } from "../secret/secret-service"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { fnSecretBulkInsert, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types"; import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";
export type TImportDataIntoInfisicalDTO = { export type TImportDataIntoInfisicalDTO = {
projectService: TProjectServiceFactory; projectDAL: Pick<TProjectDALFactory, "transaction">;
orgService: TOrgServiceFactory; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
projectEnvService: TProjectEnvServiceFactory; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretService: TSecretServiceFactory;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
input: TImportInfisicalDataCreate; input: TImportInfisicalDataCreate;
}; };
@@ -46,13 +65,13 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON; const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;
const infisicalImportData: InfisicalImportData = { const infisicalImportData: InfisicalImportData = {
projects: new Map<string, { name: string; id: string }>(), projects: [],
environments: new Map<string, { name: string; id: string; projectId: string }>(), environments: [],
secrets: new Map<string, { name: string; id: string; projectId: string; environmentId: string; value: string }>() secrets: []
}; };
parsedJson.apps.forEach((app: { name: string; id: string }) => { parsedJson.apps.forEach((app: { name: string; id: string }) => {
infisicalImportData.projects.set(app.id, { name: app.name, id: app.id }); infisicalImportData.projects.push({ name: app.name, id: app.id });
}); });
// string to string map for env templates // string to string map for env templates
@@ -63,7 +82,7 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
// environments // environments
for (const env of parsedJson.baseEnvironments) { for (const env of parsedJson.baseEnvironments) {
infisicalImportData.environments?.set(env.id, { infisicalImportData.environments.push({
id: env.id, id: env.id,
name: envTemplates.get(env.environmentRoleId)!, name: envTemplates.get(env.environmentRoleId)!,
projectId: env.envParentId projectId: env.envParentId
@@ -75,9 +94,8 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
if (!env.includes("|")) { if (!env.includes("|")) {
const envData = parsedJson.envs[env]; const envData = parsedJson.envs[env];
for (const secret of Object.keys(envData.variables)) { for (const secret of Object.keys(envData.variables)) {
const id = randomUUID(); infisicalImportData.secrets.push({
infisicalImportData.secrets?.set(id, { id: randomUUID(),
id,
name: secret, name: secret,
environmentId: env, environmentId: env,
value: envData.variables[secret].val value: envData.variables[secret].val
@@ -91,9 +109,14 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
export const importDataIntoInfisicalFn = async ({ export const importDataIntoInfisicalFn = async ({
projectService, projectService,
orgService, projectEnvDAL,
projectEnvService, projectDAL,
secretService, secretDAL,
kmsService,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
input: { data, actor, actorId, actorOrgId, actorAuthMethod } input: { data, actor, actorId, actorOrgId, actorAuthMethod }
}: TImportDataIntoInfisicalDTO) => { }: TImportDataIntoInfisicalDTO) => {
// Import data to infisical // Import data to infisical
@@ -103,95 +126,145 @@ export const importDataIntoInfisicalFn = async ({
const originalToNewProjectId = new Map<string, string>(); const originalToNewProjectId = new Map<string, string>();
const originalToNewEnvironmentId = new Map<string, string>(); const originalToNewEnvironmentId = new Map<string, string>();
const projectsNotImported: string[] = [];
for await (const [id, project] of data.projects) { await projectDAL.transaction(async (tx) => {
const newProject = await projectService for await (const project of data.projects) {
.createProject({ const newProject = await projectService
actor, .createProject({
actorId,
actorOrgId,
actorAuthMethod,
workspaceName: project.name,
createDefaultEnvs: false
})
.catch(() => {
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}] [id:${id}]` });
});
originalToNewProjectId.set(project.id, newProject.id);
}
// Invite user importing projects
const invites = await orgService.inviteUserToOrganization({
actorAuthMethod,
actorId,
actorOrgId,
actor,
inviteeEmails: [],
orgId: actorOrgId,
organizationRoleSlug: OrgMembershipRole.NoAccess,
projects: Array.from(originalToNewProjectId.values()).map((project) => ({
id: project,
projectRoleSlug: [ProjectMembershipRole.Member]
}))
});
if (!invites) {
throw new BadRequestError({ message: `Failed to invite user to projects: [userId:${actorId}]` });
}
// Import environments
if (data.environments) {
for await (const [id, environment] of data.environments) {
try {
const newEnvironment = await projectEnvService.createEnvironment({
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
name: environment.name, workspaceName: project.name,
projectId: originalToNewProjectId.get(environment.projectId)!, createDefaultEnvs: false,
slug: slugify(`${environment.name}-${alphaNumericNanoId(4)}`) tx
})
.catch((e) => {
logger.error(e, `Failed to import to project [name:${project.name}]`);
throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` });
}); });
originalToNewProjectId.set(project.id, newProject.id);
}
if (!newEnvironment) { // Import environments
logger.error(`Failed to import environment: [name:${environment.name}] [id:${id}]`); if (data.environments) {
for await (const environment of data.environments) {
const projectId = originalToNewProjectId.get(environment.projectId);
const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`);
if (!projectId) {
projectsNotImported.push(environment.projectId);
// eslint-disable-next-line no-continue
continue;
}
const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx);
if (existingEnv) {
throw new BadRequestError({ throw new BadRequestError({
message: `Failed to import environment: [name:${environment.name}] [id:${id}]` message: `Environment with slug '${slug}' already exist`,
name: "CreateEnvironment"
}); });
} }
originalToNewEnvironmentId.set(id, newEnvironment.slug);
} catch (error) { const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
throw new BadRequestError({ const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
message: `Failed to import environment: ${environment.name}]`, await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
name: "EnvKeyMigrationImportEnvironment"
originalToNewEnvironmentId.set(environment.id, doc.slug);
}
}
if (data.secrets && data.secrets.length > 0) {
const mappedToEnvironmentId = new Map<
string,
{
secretKey: string;
secretValue: string;
}[]
>();
for (const secret of data.secrets) {
if (!originalToNewEnvironmentId.get(secret.environmentId)) {
// eslint-disable-next-line no-continue
continue;
}
if (!mappedToEnvironmentId.has(secret.environmentId)) {
mappedToEnvironmentId.set(secret.environmentId, []);
}
mappedToEnvironmentId.get(secret.environmentId)!.push({
secretKey: secret.name,
secretValue: secret.value || ""
}); });
} }
}
}
// Import secrets // for each of the mappedEnvironmentId
if (data.secrets) { for await (const [envId, secrets] of mappedToEnvironmentId) {
for await (const [id, secret] of data.secrets) { const environment = data.environments.find((env) => env.id === envId);
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId; const projectId = originalToNewProjectId.get(environment?.projectId as string)!;
if (!dataProjectId) {
throw new BadRequestError({ message: `Failed to import secret "${secret.name}", project not found` }); if (!projectId) {
} throw new BadRequestError({ message: `Failed to import secret, project not found` });
const projectId = originalToNewProjectId.get(dataProjectId); }
const newSecret = await secretService.createSecretRaw({
actorId, const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
actor, {
actorOrgId, type: KmsDataKey.SecretManager,
environment: originalToNewEnvironmentId.get(secret.environmentId)!, projectId
actorAuthMethod, },
projectId: projectId!, tx
secretPath: "/", );
secretName: secret.name,
type: SecretType.Shared, const envSlug = originalToNewEnvironmentId.get(envId)!;
secretValue: secret.value const folder = await folderDAL.findBySecretPath(projectId, envSlug, "/", tx);
}); if (!folder)
if (!newSecret) { throw new NotFoundError({
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` }); message: `Folder not found for the given environment slug (${envSlug}) & secret path (/)`,
name: "Create secret"
});
const secretBatches = chunkArray(secrets, 2500);
for await (const secretBatch of secretBatches) {
const secretsByKeys = await secretDAL.findBySecretKeys(
folder.id,
secretBatch.map((el) => ({
key: el.secretKey,
type: SecretType.Shared
})),
tx
);
if (secretsByKeys.length) {
throw new BadRequestError({
message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
});
}
await fnSecretBulkInsert({
inputSecrets: secretBatch.map((el) => {
const references = getAllNestedSecretReferences(el.secretValue);
return {
version: 1,
encryptedValue: el.secretValue
? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
: undefined,
key: el.secretKey,
references,
type: SecretType.Shared
};
}),
folderId: folder.id,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
});
}
} }
} }
} });
return { projectsNotImported };
}; };
@@ -0,0 +1,152 @@
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { importDataIntoInfisicalFn } from "./external-migration-fns";
import { ExternalPlatforms, TImportInfisicalDataCreate } from "./external-migration-types";
export type TExternalMigrationQueueFactoryDep = {
smtpService: TSmtpService;
queueService: TQueueServiceFactory;
projectDAL: Pick<TProjectDALFactory, "transaction">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
};
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
export const externalMigrationQueueFactory = ({
queueService,
projectService,
smtpService,
projectDAL,
projectEnvService,
secretV2BridgeService,
kmsService,
projectEnvDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
}: TExternalMigrationQueueFactoryDep) => {
const startImport = async (dto: {
actorEmail: string;
data: {
iv: string;
tag: string;
ciphertext: string;
algorithm: SecretEncryptionAlgo;
encoding: SecretKeyEncoding;
};
}) => {
await queueService.queue(
QueueName.ImportSecretsFromExternalSource,
QueueJobs.ImportSecretsFromExternalSource,
dto,
{
removeOnComplete: true,
removeOnFail: true
}
);
};
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
try {
const { data, actorEmail } = job.data;
await smtpService.sendMail({
recipients: [actorEmail],
subjectLine: "Infisical import started",
substitutions: {
provider: ExternalPlatforms.EnvKey
},
template: SmtpTemplates.ExternalImportStarted
});
const decrypted = infisicalSymmetricDecrypt({
ciphertext: data.ciphertext,
iv: data.iv,
keyEncoding: data.encoding,
tag: data.tag
});
const decryptedJson = JSON.parse(decrypted) as TImportInfisicalDataCreate;
const { projectsNotImported } = await importDataIntoInfisicalFn({
input: decryptedJson,
projectDAL,
projectEnvDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
kmsService,
projectService,
projectEnvService,
secretV2BridgeService
});
if (projectsNotImported.length) {
logger.info(
{
actorEmail,
actorOrgId: decryptedJson.actorOrgId,
projectsNotImported
},
"One or more projects were not imported during import from external source"
);
}
await smtpService.sendMail({
recipients: [actorEmail],
subjectLine: "Infisical import successful",
substitutions: {
provider: ExternalPlatforms.EnvKey
},
template: SmtpTemplates.ExternalImportSuccessful
});
} catch (err) {
await smtpService.sendMail({
recipients: [job.data.actorEmail],
subjectLine: "Infisical import failed",
substitutions: {
provider: ExternalPlatforms.EnvKey,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
error: (err as any)?.message || "Unknown error"
},
template: SmtpTemplates.ExternalImportFailed
});
logger.error(err, "Failed to import data from external source");
}
});
return {
startImport
};
};
@@ -1,30 +1,25 @@
import { OrgMembershipRole } from "@app/db/schemas"; import { OrgMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { ForbiddenRequestError } from "@app/lib/errors"; import { ForbiddenRequestError } from "@app/lib/errors";
import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectServiceFactory } from "../project/project-service"; import { decryptEnvKeyDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; import { TExternalMigrationQueueFactory } from "./external-migration-queue";
import { TSecretServiceFactory } from "../secret/secret-service";
import { decryptEnvKeyDataFn, importDataIntoInfisicalFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TImportEnvKeyDataCreate } from "./external-migration-types"; import { TImportEnvKeyDataCreate } from "./external-migration-types";
type TExternalMigrationServiceFactoryDep = { type TExternalMigrationServiceFactoryDep = {
projectService: TProjectServiceFactory;
orgService: TOrgServiceFactory;
projectEnvService: TProjectEnvServiceFactory;
secretService: TSecretServiceFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
externalMigrationQueue: TExternalMigrationQueueFactory;
userDAL: Pick<TUserDALFactory, "findById">;
}; };
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>; export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
export const externalMigrationServiceFactory = ({ export const externalMigrationServiceFactory = ({
projectService,
orgService,
projectEnvService,
permissionService, permissionService,
secretService externalMigrationQueue,
userDAL
}: TExternalMigrationServiceFactoryDep) => { }: TExternalMigrationServiceFactoryDep) => {
const importEnvKeyData = async ({ const importEnvKeyData = async ({
decryptionKey, decryptionKey,
@@ -41,21 +36,28 @@ export const externalMigrationServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (membership.role !== OrgMembershipRole.Admin) { if (membership.role !== OrgMembershipRole.Admin) {
throw new ForbiddenRequestError({ message: "Only admins can import data" }); throw new ForbiddenRequestError({ message: "Only admins can import data" });
} }
const user = await userDAL.findById(actorId);
const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson); const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson);
const envKeyData = await parseEnvKeyDataFn(json); const envKeyData = await parseEnvKeyDataFn(json);
const response = await importDataIntoInfisicalFn({
input: { data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod }, const stringifiedJson = JSON.stringify({
projectService, data: envKeyData,
orgService, actor,
projectEnvService, actorId,
secretService actorOrgId,
actorAuthMethod
});
const encrypted = infisicalSymmetricEncypt(stringifiedJson);
await externalMigrationQueue.startImport({
actorEmail: user.email!,
data: encrypted
}); });
return response;
}; };
return { return {
@@ -1,26 +1,9 @@
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
export type InfisicalImportData = { export type InfisicalImportData = {
projects: Map<string, { name: string; id: string }>; projects: Array<{ name: string; id: string }>;
environments: Array<{ name: string; id: string; projectId: string }>;
environments?: Map< secrets: Array<{ name: string; id: string; environmentId: string; value: string }>;
string,
{
name: string;
id: string;
projectId: string;
}
>;
secrets?: Map<
string,
{
name: string;
id: string;
environmentId: string;
value: string;
}
>;
}; };
export type TImportEnvKeyDataCreate = { export type TImportEnvKeyDataCreate = {
@@ -104,3 +87,7 @@ export type TEnvKeyExportJSON = {
} }
>; >;
}; };
export enum ExternalPlatforms {
EnvKey = "EnvKey"
}
@@ -1,7 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas"; import { TableName, TIdentityOrgMemberships, TOrgRoles } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
@@ -42,30 +42,50 @@ export const identityOrgDALFactory = (db: TDbClient) => {
tx?: Knex tx?: Knex
) => { ) => {
try { try {
const paginatedFetchIdentity = (tx || db.replicaNode())(TableName.Identity) const paginatedIdentity = (tx || db.replicaNode())(TableName.Identity)
.as(TableName.Identity) .join(
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection); TableName.IdentityOrgMembership,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.Identity}.id`
)
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("authMethod").withSchema(TableName.Identity).as("identityAuthMethod")
)
.where(filter)
.as("paginatedIdentity");
if (search?.length) { if (search?.length) {
void paginatedFetchIdentity.whereILike(`${TableName.Identity}.name`, `%${search}%`); void paginatedIdentity.whereILike(`${TableName.Identity}.name`, `%${search}%`);
} }
if (limit) { if (limit) {
void paginatedFetchIdentity.offset(offset).limit(limit); void paginatedIdentity.offset(offset).limit(limit);
} }
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) // akhilmhdh: refer this for pagination with multiple left queries
.where(filter) type TSubquery = Awaited<typeof paginatedIdentity>;
.join<Awaited<typeof paginatedFetchIdentity>>(paginatedFetchIdentity, (queryBuilder) => { const query = (tx || db.replicaNode())
queryBuilder.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`); .from<TSubquery[number], TSubquery>(paginatedIdentity)
}) .leftJoin<TOrgRoles>(TableName.OrgRoles, `paginatedIdentity.roleId`, `${TableName.OrgRoles}.id`)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder
.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`) .on(`paginatedIdentity.identityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`); .andOn(`paginatedIdentity.orgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.select(selectAllTableCols(TableName.IdentityOrgMembership)) .select(
db.ref("id").withSchema("paginatedIdentity"),
db.ref("role").withSchema("paginatedIdentity"),
db.ref("roleId").withSchema("paginatedIdentity"),
db.ref("orgId").withSchema("paginatedIdentity"),
db.ref("createdAt").withSchema("paginatedIdentity"),
db.ref("updatedAt").withSchema("paginatedIdentity"),
db.ref("identityId").withSchema("paginatedIdentity"),
db.ref("identityName").withSchema("paginatedIdentity"),
db.ref("identityAuthMethod").withSchema("paginatedIdentity")
)
// cr stands for custom role // cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
.select(db.ref("name").as("crName").withSchema(TableName.OrgRoles)) .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
@@ -73,17 +93,14 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles)) .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
.select(
db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)
)
.select( .select(
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
) );
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection); if (orderBy === OrgIdentityOrderBy.Name) {
void query.orderBy("identityName", orderDirection);
}
const docs = await query; const docs = await query;
const formattedDocs = sqlNestRelationships({ const formattedDocs = sqlNestRelationships({
@@ -1,9 +1,13 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { TIntegrationAuths } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import { Integrations, IntegrationUrls } from "./integration-list"; import { Integrations, IntegrationUrls } from "./integration-list";
// akhilmhdh: check this part later. Copied from old base // akhilmhdh: check this part later. Copied from old base
@@ -230,7 +234,13 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
/** /**
* Return list of repositories for Github integration * Return list of repositories for Github integration
*/ */
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { const getAppsGithub = async ({
accessToken,
authMetadata
}: {
accessToken: string;
authMetadata?: TIntegrationAuthMetadata;
}) => {
interface GitHubApp { interface GitHubApp {
id: string; id: string;
name: string; name: string;
@@ -242,6 +252,29 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
}; };
} }
if (authMetadata?.installationId) {
const appCfg = getConfig();
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.CLIENT_APP_ID_GITHUB_APP,
privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP,
installationId: authMetadata.installationId
}
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const repos = await octokit.paginate("GET /installation/repositories", {
per_page: 100
});
return repos.map((a) => ({
appId: String(a.id),
name: a.name,
owner: a.owner.login
}));
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const repos = (await new Octokit({ const repos = (await new Octokit({
auth: accessToken auth: accessToken
@@ -455,6 +488,31 @@ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
return apps; return apps;
}; };
/**
* Return list of projects for Databricks integration
*/
const getAppsDatabricks = async ({ url, accessToken }: { url?: string | null; accessToken: string }) => {
const databricksApiUrl = `${url}/api`;
const res = await request.get<{ scopes: { name: string; backend_type: string }[] }>(
`${databricksApiUrl}/2.0/secrets/scopes/list`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
const scopes =
res.data?.scopes?.map((a) => ({
name: a.name, // name maps to unique scope name in Databricks
backend_type: a.backend_type
})) ?? [];
return scopes;
};
const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
const res = ( const res = (
await request.get<{ id: string; slug: string }[]>(`${IntegrationUrls.TRAVISCI_API_URL}/repos`, { await request.get<{ id: string; slug: string }[]>(`${IntegrationUrls.TRAVISCI_API_URL}/repos`, {
@@ -1031,6 +1089,7 @@ const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: strin
export const getApps = async ({ export const getApps = async ({
integration, integration,
integrationAuth,
accessToken, accessToken,
accessId, accessId,
teamId, teamId,
@@ -1041,6 +1100,7 @@ export const getApps = async ({
integration: string; integration: string;
accessToken: string; accessToken: string;
accessId?: string; accessId?: string;
integrationAuth: TIntegrationAuths;
teamId?: string | null; teamId?: string | null;
azureDevOpsOrgName?: string | null; azureDevOpsOrgName?: string | null;
workspaceSlug?: string; workspaceSlug?: string;
@@ -1074,7 +1134,8 @@ export const getApps = async ({
case Integrations.GITHUB: case Integrations.GITHUB:
return getAppsGithub({ return getAppsGithub({
accessToken accessToken,
authMetadata: IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {})
}); });
case Integrations.GITLAB: case Integrations.GITLAB:
@@ -1104,6 +1165,12 @@ export const getApps = async ({
accessToken accessToken
}); });
case Integrations.DATABRICKS:
return getAppsDatabricks({
url,
accessToken
});
case Integrations.LARAVELFORGE: case Integrations.LARAVELFORGE:
return getAppsLaravelForge({ return getAppsLaravelForge({
accessToken, accessToken,
@@ -3,7 +3,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TIntegrationAuths, TIntegrationAuthsUpdate } from "@app/db/schemas"; import { TableName, TIntegrationAuths, TIntegrationAuthsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TIntegrationAuthDALFactory = ReturnType<typeof integrationAuthDALFactory>; export type TIntegrationAuthDALFactory = ReturnType<typeof integrationAuthDALFactory>;
@@ -28,8 +28,23 @@ export const integrationAuthDALFactory = (db: TDbClient) => {
} }
}; };
const getByOrg = async (orgId: string, tx?: Knex) => {
try {
const integrationAuths = await (tx || db)(TableName.IntegrationAuth)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.IntegrationAuth}.projectId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
.where(`${TableName.Organization}.id`, "=", orgId)
.select(selectAllTableCols(TableName.IntegrationAuth));
return integrationAuths;
} catch (error) {
throw new DatabaseError({ error, name: "get by org" });
}
};
return { return {
...integrationAuthOrm, ...integrationAuthOrm,
bulkUpdate bulkUpdate,
getByOrg
}; };
}; };
@@ -0,0 +1,7 @@
import { z } from "zod";
export const IntegrationAuthMetadataSchema = z.object({
installationId: z.string().optional()
});
export type TIntegrationAuthMetadata = z.infer<typeof IntegrationAuthMetadataSchema>;
@@ -1,14 +1,16 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import AWS from "aws-sdk"; import AWS from "aws-sdk";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TGenericPermission, TProjectPermission } from "@app/lib/types";
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
@@ -16,11 +18,13 @@ import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { getApps } from "./integration-app-list"; import { getApps } from "./integration-app-list";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import { import {
TBitbucketWorkspace, TBitbucketWorkspace,
TChecklyGroups, TChecklyGroups,
TDeleteIntegrationAuthByIdDTO, TDeleteIntegrationAuthByIdDTO,
TDeleteIntegrationAuthsDTO, TDeleteIntegrationAuthsDTO,
TDuplicateGithubIntegrationAuthDTO,
TGetIntegrationAuthDTO, TGetIntegrationAuthDTO,
TGetIntegrationAuthTeamCityBuildConfigDTO, TGetIntegrationAuthTeamCityBuildConfigDTO,
THerokuPipelineCoupling, THerokuPipelineCoupling,
@@ -86,6 +90,24 @@ export const integrationAuthServiceFactory = ({
return authorizations; return authorizations;
}; };
const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => {
const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string);
return Promise.all(
authorizations.filter(async (auth) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
auth.projectId,
actorAuthMethod,
actorOrgId
);
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
})
);
};
const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => { const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id); const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: "Failed to find integration" }); if (!integrationAuth) throw new NotFoundError({ message: "Failed to find integration" });
@@ -109,7 +131,8 @@ export const integrationAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
integration, integration,
url, url,
code code,
installationId
}: TOauthExchangeDTO) => { }: TOauthExchangeDTO) => {
if (!Object.values(Integrations).includes(integration as Integrations)) if (!Object.values(Integrations).includes(integration as Integrations))
throw new BadRequestError({ message: "Invalid integration" }); throw new BadRequestError({ message: "Invalid integration" });
@@ -123,7 +146,7 @@ export const integrationAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
const tokenExchange = await exchangeCode({ integration, code, url }); const tokenExchange = await exchangeCode({ integration, code, url, installationId });
const updateDoc: TIntegrationAuthsInsert = { const updateDoc: TIntegrationAuthsInsert = {
projectId, projectId,
integration, integration,
@@ -141,6 +164,16 @@ export const integrationAuthServiceFactory = ({
updateDoc.metadata = { updateDoc.metadata = {
authMethod: "oauth2" authMethod: "oauth2"
}; };
} else if (integration === Integrations.GITHUB && installationId) {
updateDoc.metadata = {
installationId,
installationName: tokenExchange.installationName,
authMethod: "app"
};
}
if (installationId && integration === Integrations.GITHUB) {
return integrationAuthDAL.create(updateDoc);
} }
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
@@ -176,12 +209,23 @@ export const integrationAuthServiceFactory = ({
updateDoc.accessCiphertext = accessEncToken.ciphertext; updateDoc.accessCiphertext = accessEncToken.ciphertext;
} }
} }
return integrationAuthDAL.transaction(async (tx) => { return integrationAuthDAL.transaction(async (tx) => {
const doc = await integrationAuthDAL.findOne({ projectId, integration }, tx); const integrationAuths = await integrationAuthDAL.find({ projectId, integration }, { tx });
if (!doc) { let existingIntegrationAuth: TIntegrationAuths | undefined;
// we need to ensure that the integration auth that we use for Github is actually Oauth
if (integration === Integrations.GITHUB) {
existingIntegrationAuth = integrationAuths.find((integAuth) => !integAuth.metadata);
} else {
[existingIntegrationAuth] = integrationAuths;
}
if (!existingIntegrationAuth) {
return integrationAuthDAL.create(updateDoc, tx); return integrationAuthDAL.create(updateDoc, tx);
} }
return integrationAuthDAL.updateById(doc.id, updateDoc, tx);
return integrationAuthDAL.updateById(existingIntegrationAuth.id, updateDoc, tx);
}); });
}; };
@@ -334,6 +378,13 @@ export const integrationAuthServiceFactory = ({
) { ) {
return { accessToken: "", accessId: "" }; return { accessToken: "", accessId: "" };
} }
if (
integrationAuth.integration === Integrations.GITHUB &&
IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}).installationId
) {
return { accessToken: "", accessId: "" };
}
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } = const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } =
await kmsService.createCipherPairWithDataKey({ await kmsService.createCipherPairWithDataKey({
@@ -460,6 +511,7 @@ export const integrationAuthServiceFactory = ({
const { accessToken, accessId } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); const { accessToken, accessId } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const apps = await getApps({ const apps = await getApps({
integration: integrationAuth.integration, integration: integrationAuth.integration,
integrationAuth,
accessToken, accessToken,
accessId, accessId,
teamId, teamId,
@@ -575,6 +627,7 @@ export const integrationAuthServiceFactory = ({
}; };
const getGithubOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthGithubOrgsDTO) => { const getGithubOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthGithubOrgsDTO) => {
const appCfg = getConfig();
const integrationAuth = await integrationAuthDAL.findById(id); const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: "Failed to find integration" }); if (!integrationAuth) throw new NotFoundError({ message: "Failed to find integration" });
@@ -587,9 +640,44 @@ export const integrationAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const octokit = new Octokit({ let octokit: Octokit;
const { installationId } = (integrationAuth.metadata as TIntegrationAuthMetadata) || {};
if (installationId) {
octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.CLIENT_APP_ID_GITHUB_APP,
privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP,
installationId
}
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const repos = await octokit.paginate("GET /installation/repositories", {
per_page: 100
});
const orgSet: Set<string> = new Set();
return repos
.filter((repo) => repo.owner.type === "Organization")
.map((repo) => ({
name: repo.owner.login,
orgId: String(repo.owner.id)
}))
.filter((org) => {
const isOrgProcessed = orgSet.has(org.orgId);
if (!isOrgProcessed) {
orgSet.add(org.orgId);
}
return !isOrgProcessed;
});
}
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
octokit = new Octokit({
auth: accessToken auth: accessToken
}); });
@@ -598,7 +686,9 @@ export const integrationAuthServiceFactory = ({
"X-GitHub-Api-Version": "2022-11-28" "X-GitHub-Api-Version": "2022-11-28"
} }
}); });
if (!data) return []; if (!data) {
return [];
}
return data.map(({ login: name, id: orgId }) => ({ name, orgId: String(orgId) })); return data.map(({ login: name, id: orgId }) => ({ name, orgId: String(orgId) }));
}; };
@@ -626,9 +716,24 @@ export const integrationAuthServiceFactory = ({
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const octokit = new Octokit({ let octokit: Octokit;
auth: accessToken const appCfg = getConfig();
});
const authMetadata = IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {});
if (authMetadata.installationId) {
octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.CLIENT_APP_ID_GITHUB_APP,
privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP,
installationId: authMetadata.installationId
}
});
} else {
octokit = new Octokit({
auth: accessToken
});
}
const { const {
data: { environments } data: { environments }
@@ -1315,8 +1420,58 @@ export const integrationAuthServiceFactory = ({
return delIntegrationAuth; return delIntegrationAuth;
}; };
// At the moment, we only use this for Github App integration as it's a special case
const duplicateIntegrationAuth = async ({
id,
actorId,
actor,
actorAuthMethod,
actorOrgId,
projectId
}: TDuplicateGithubIntegrationAuthDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) {
throw new NotFoundError({ message: "Failed to find integration" });
}
const { permission: sourcePermission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(sourcePermission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const { permission: targetPermission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(targetPermission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const newIntegrationAuth: Omit<typeof integrationAuth, "id"> & { id?: string } = {
...integrationAuth,
id: undefined,
projectId
};
return integrationAuthDAL.create(newIntegrationAuth);
};
return { return {
listIntegrationAuthByProjectId, listIntegrationAuthByProjectId,
listOrgIntegrationAuth,
getIntegrationOptions, getIntegrationOptions,
getIntegrationAuth, getIntegrationAuth,
oauthExchange, oauthExchange,
@@ -1343,6 +1498,7 @@ export const integrationAuthServiceFactory = ({
getNorthFlankSecretGroups, getNorthFlankSecretGroups,
getTeamcityBuildConfigs, getTeamcityBuildConfigs,
getBitbucketWorkspaces, getBitbucketWorkspaces,
getIntegrationAccessToken getIntegrationAccessToken,
duplicateIntegrationAuth
}; };
}; };
@@ -9,6 +9,7 @@ export type TOauthExchangeDTO = {
integration: string; integration: string;
code: string; code: string;
url?: string; url?: string;
installationId?: string;
} & TProjectPermission; } & TProjectPermission;
export type TSaveIntegrationAccessTokenDTO = { export type TSaveIntegrationAccessTokenDTO = {
@@ -107,6 +108,10 @@ export type TDeleteIntegrationAuthByIdDTO = {
id: string; id: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDuplicateGithubIntegrationAuthDTO = {
id: string;
} & TProjectPermission;
export type TGetIntegrationAuthTeamCityBuildConfigDTO = { export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
id: string; id: string;
appId: string; appId: string;
@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { createAppAuth } from "@octokit/auth-app";
import { retry } from "@octokit/plugin-retry"; import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas"; import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -15,6 +18,7 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import { TIntegrationAuthServiceFactory } from "./integration-auth-service"; import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
import { Integrations } from "./integration-list"; import { Integrations } from "./integration-list";
@@ -154,10 +158,12 @@ const getIntegrationSecretsV1 = async (
export const deleteGithubSecrets = async ({ export const deleteGithubSecrets = async ({
integration, integration,
authMetadata,
secrets, secrets,
accessToken accessToken
}: { }: {
integration: Omit<TIntegrations, "envId">; integration: Omit<TIntegrations, "envId">;
authMetadata: TIntegrationAuthMetadata;
secrets: Record<string, boolean>; secrets: Record<string, boolean>;
accessToken: string; accessToken: string;
}) => { }) => {
@@ -170,9 +176,23 @@ export const deleteGithubSecrets = async ({
} }
const OctokitWithRetry = Octokit.plugin(retry); const OctokitWithRetry = Octokit.plugin(retry);
const octokit = new OctokitWithRetry({ let octokit: Octokit;
auth: accessToken const appCfg = getConfig();
});
if (authMetadata.installationId) {
octokit = new OctokitWithRetry({
authStrategy: createAppAuth,
auth: {
appId: appCfg.CLIENT_APP_ID_GITHUB_APP,
privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP,
installationId: authMetadata.installationId
}
});
} else {
octokit = new OctokitWithRetry({
auth: accessToken
});
}
enum GithubScope { enum GithubScope {
Repo = "github-repo", Repo = "github-repo",
@@ -192,6 +212,7 @@ export const deleteGithubSecrets = async ({
break; break;
} }
case GithubScope.Env: { case GithubScope.Env: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
encryptedGithubSecrets = ( encryptedGithubSecrets = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", { await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
repository_id: Number(integration.appId), repository_id: Number(integration.appId),
@@ -346,6 +367,7 @@ export const deleteIntegrationSecrets = async ({
case Integrations.GITHUB: { case Integrations.GITHUB: {
await deleteGithubSecrets({ await deleteGithubSecrets({
integration, integration,
authMetadata: IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}),
accessToken, accessToken,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
}); });
@@ -15,6 +15,7 @@ export enum Integrations {
FLYIO = "flyio", FLYIO = "flyio",
LARAVELFORGE = "laravel-forge", LARAVELFORGE = "laravel-forge",
CIRCLECI = "circleci", CIRCLECI = "circleci",
DATABRICKS = "databricks",
TRAVISCI = "travisci", TRAVISCI = "travisci",
TEAMCITY = "teamcity", TEAMCITY = "teamcity",
SUPABASE = "supabase", SUPABASE = "supabase",
@@ -73,6 +74,7 @@ export enum IntegrationUrls {
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2", RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
FLYIO_API_URL = "https://api.fly.io/graphql", FLYIO_API_URL = "https://api.fly.io/graphql",
CIRCLECI_API_URL = "https://circleci.com/api", CIRCLECI_API_URL = "https://circleci.com/api",
DATABRICKS_API_URL = "https:/xxxx.com/api",
TRAVISCI_API_URL = "https://api.travis-ci.com", TRAVISCI_API_URL = "https://api.travis-ci.com",
SUPABASE_API_URL = "https://api.supabase.com", SUPABASE_API_URL = "https://api.supabase.com",
LARAVELFORGE_API_URL = "https://forge.laravel.com", LARAVELFORGE_API_URL = "https://forge.laravel.com",
@@ -94,7 +96,9 @@ export enum IntegrationUrls {
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com", GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com", GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform" GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations"
} }
export const getIntegrationOptions = async () => { export const getIntegrationOptions = async () => {
@@ -136,6 +140,7 @@ export const getIntegrationOptions = async () => {
isAvailable: true, isAvailable: true,
type: "oauth", type: "oauth",
clientId: appCfg.CLIENT_ID_GITHUB, clientId: appCfg.CLIENT_ID_GITHUB,
clientSlug: appCfg.CLIENT_SLUG_GITHUB_APP,
docsLink: "" docsLink: ""
}, },
{ {
@@ -210,6 +215,15 @@ export const getIntegrationOptions = async () => {
clientId: "", clientId: "",
docsLink: "" docsLink: ""
}, },
{
name: "Databricks",
slug: "databricks",
image: "Databricks.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{ {
name: "GitLab", name: "GitLab",
slug: "gitlab", slug: "gitlab",
@@ -9,6 +9,7 @@
import { import {
CreateSecretCommand, CreateSecretCommand,
DeleteSecretCommand,
DescribeSecretCommand, DescribeSecretCommand,
GetSecretValueCommand, GetSecretValueCommand,
ResourceNotFoundException, ResourceNotFoundException,
@@ -18,6 +19,7 @@ import {
UpdateSecretCommand UpdateSecretCommand
} from "@aws-sdk/client-secrets-manager"; } from "@aws-sdk/client-secrets-manager";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import AWS, { AWSError } from "aws-sdk"; import AWS, { AWSError } from "aws-sdk";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
@@ -35,6 +37,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema"; import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
import { TIntegrationsWithEnvironment } from "./integration-auth-types"; import { TIntegrationsWithEnvironment } from "./integration-auth-types";
import { import {
IntegrationInitialSyncBehavior, IntegrationInitialSyncBehavior,
@@ -727,7 +730,7 @@ const syncSecretsAWSParameterStore = async ({
awsParameterStoreSecretsObj[key].KeyId !== metadata.kmsKeyId; awsParameterStoreSecretsObj[key].KeyId !== metadata.kmsKeyId;
// we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS // we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS
if (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { if (secrets[key].value && (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value)) {
await ssm await ssm
.putParameter({ .putParameter({
Name: `${integration.path}${key}`, Name: `${integration.path}${key}`,
@@ -788,7 +791,7 @@ const syncSecretsAWSParameterStore = async ({
logger.info( logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=2]` `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=2]`
); );
if (!(key in secrets)) { if (!(key in secrets) || !secrets[key].value) {
logger.info( logger.info(
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=3]` `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=3]`
); );
@@ -899,12 +902,21 @@ const syncSecretsAWSSecretManager = async ({
} }
if (!isEqual(secretToCompare, secretValue)) { if (!isEqual(secretToCompare, secretValue)) {
await secretsManager.send( if (secretValue) {
new UpdateSecretCommand({ await secretsManager.send(
SecretId: secretId, new UpdateSecretCommand({
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue) SecretId: secretId,
}) SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue)
); })
);
// delete it
} else {
await secretsManager.send(
new DeleteSecretCommand({
SecretId: secretId
})
);
}
} }
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
@@ -989,16 +1001,21 @@ const syncSecretsAWSSecretManager = async ({
} catch (err) { } catch (err) {
// case 1: when AWS manager can't find the specified secret // case 1: when AWS manager can't find the specified secret
if (err instanceof ResourceNotFoundException && secretsManager) { if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send( if (secretValue) {
new CreateSecretCommand({ await secretsManager.send(
Name: secretId, new CreateSecretCommand({
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue), Name: secretId,
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }), SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
Tags: metadata.secretAWSTag ...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) Tags: metadata.secretAWSTag
: [] ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
}) Key: tag.key,
); Value: tag.value
}))
: []
})
);
}
// case 2: something unexpected went wrong, so we'll throw the error to reflect the error in the integration sync status // case 2: something unexpected went wrong, so we'll throw the error to reflect the error in the integration sync status
} else { } else {
throw err; throw err;
@@ -1527,11 +1544,13 @@ const syncSecretsNetlify = async ({
*/ */
const syncSecretsGitHub = async ({ const syncSecretsGitHub = async ({
integration, integration,
integrationAuth,
secrets, secrets,
accessToken, accessToken,
appendices appendices
}: { }: {
integration: TIntegrations; integration: TIntegrations;
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessToken: string; accessToken: string;
appendices?: { prefix: string; suffix: string }; appendices?: { prefix: string; suffix: string };
@@ -1553,9 +1572,24 @@ const syncSecretsGitHub = async ({
selected_repositories_url?: string | undefined; selected_repositories_url?: string | undefined;
} }
const octokit = new Octokit({ const authMetadata = IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {});
auth: accessToken let octokit: Octokit;
}); const appCfg = getConfig();
if (authMetadata.installationId) {
octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.CLIENT_APP_ID_GITHUB_APP,
privateKey: appCfg.CLIENT_PRIVATE_KEY_GITHUB_APP,
installationId: authMetadata.installationId
}
});
} else {
octokit = new Octokit({
auth: accessToken
});
}
enum GithubScope { enum GithubScope {
Repo = "github-repo", Repo = "github-repo",
@@ -2085,6 +2119,80 @@ const syncSecretsCircleCI = async ({
); );
}; };
/**
* Sync/push [secrets] to Databricks project
*/
const syncSecretsDatabricks = async ({
integration,
integrationAuth,
secrets,
accessToken
}: {
integration: TIntegrations;
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const databricksApiUrl = `${integrationAuth.url}/api`;
// sync secrets to Databricks
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.post(
`${databricksApiUrl}/2.0/secrets/put`,
{
scope: integration.app,
key,
string_value: secrets[key].value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
)
)
);
// get secrets from Databricks
const getSecretsRes = (
await request.get<{ secrets: { key: string; last_updated_timestamp: number }[] }>(
`${databricksApiUrl}/2.0/secrets/list`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
params: {
scope: integration.app
}
}
)
).data.secrets;
// delete secrets from Databricks
await Promise.all(
getSecretsRes.map(async (sec) => {
if (!(sec.key in secrets)) {
return request.post(
`${databricksApiUrl}/2.0/secrets/delete`,
{
scope: integration.app,
key: sec.key
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
}
})
);
};
/** /**
* Sync/push [secrets] to TravisCI project * Sync/push [secrets] to TravisCI project
*/ */
@@ -3980,6 +4088,7 @@ export const syncIntegrationSecrets = async ({
case Integrations.GITHUB: case Integrations.GITHUB:
await syncSecretsGitHub({ await syncSecretsGitHub({
integration, integration,
integrationAuth,
secrets, secrets,
accessToken, accessToken,
appendices appendices
@@ -4021,6 +4130,14 @@ export const syncIntegrationSecrets = async ({
accessToken accessToken
}); });
break; break;
case Integrations.DATABRICKS:
await syncSecretsDatabricks({
integration,
integrationAuth,
secrets,
accessToken
});
break;
case Integrations.LARAVELFORGE: case Integrations.LARAVELFORGE:
await syncSecretsLaravelForge({ await syncSecretsLaravelForge({
integration, integration,
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { Integrations, IntegrationUrls } from "./integration-list"; import { Integrations, IntegrationUrls } from "./integration-list";
@@ -234,12 +234,73 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
}; };
}; };
const exchangeCodeGithub = async ({ code }: { code: string }) => { const exchangeCodeGithub = async ({ code, installationId }: { code: string; installationId?: string }) => {
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.CLIENT_ID_GITHUB || !appCfg.CLIENT_SECRET_GITHUB) {
throw new BadRequestError({ message: "Missing client id and client secret" }); if (!installationId && (!appCfg.CLIENT_ID_GITHUB || !appCfg.CLIENT_SECRET_GITHUB)) {
throw new InternalServerError({ message: "Missing client id and client secret" });
} }
if (installationId && (!appCfg.CLIENT_ID_GITHUB_APP || !appCfg.CLIENT_SECRET_GITHUB_APP)) {
throw new InternalServerError({
message: "Missing Github app client ID and client secret"
});
}
if (installationId) {
// handle app installations
const oauthRes = (
await request.get<ExchangeCodeGithubResponse>(IntegrationUrls.GITHUB_TOKEN_URL, {
params: {
client_id: appCfg.CLIENT_ID_GITHUB_APP,
client_secret: appCfg.CLIENT_SECRET_GITHUB_APP,
code,
redirect_uri: `${appCfg.SITE_URL}/integrations/github/oauth2/callback`
},
headers: {
Accept: "application/json",
"Accept-Encoding": "application/json"
}
})
).data;
// use access token to validate installation ID
const installationsRes = (
await request.get<{
installations: {
id: number;
account: {
login: string;
};
}[];
}>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${oauthRes.access_token}`,
"Accept-Encoding": "application/json"
}
})
).data;
const matchingInstallation = installationsRes.installations.find(
(installation) => installation.id === +installationId
);
if (!matchingInstallation) {
throw new ForbiddenRequestError({
message: "User has no access to the provided installation"
});
}
return {
accessToken: "", // for github app integrations, we only need the installationID from the metadata
refreshToken: null,
accessExpiresAt: null,
installationName: matchingInstallation.account.login
};
}
// handle oauth github integration
const res = ( const res = (
await request.get<ExchangeCodeGithubResponse>(IntegrationUrls.GITHUB_TOKEN_URL, { await request.get<ExchangeCodeGithubResponse>(IntegrationUrls.GITHUB_TOKEN_URL, {
params: { params: {
@@ -346,6 +407,7 @@ type TExchangeReturn = {
url?: string; url?: string;
teamId?: string; teamId?: string;
accountId?: string; accountId?: string;
installationName?: string;
}; };
/** /**
@@ -355,11 +417,13 @@ type TExchangeReturn = {
export const exchangeCode = async ({ export const exchangeCode = async ({
integration, integration,
code, code,
url url,
installationId
}: { }: {
integration: string; integration: string;
code: string; code: string;
url?: string; url?: string;
installationId?: string;
}): Promise<TExchangeReturn> => { }): Promise<TExchangeReturn> => {
switch (integration) { switch (integration) {
case Integrations.GCP_SECRET_MANAGER: case Integrations.GCP_SECRET_MANAGER:
@@ -384,7 +448,8 @@ export const exchangeCode = async ({
}); });
case Integrations.GITHUB: case Integrations.GITHUB:
return exchangeCodeGithub({ return exchangeCodeGithub({
code code,
installationId
}); });
case Integrations.GITLAB: case Integrations.GITLAB:
return exchangeCodeGitlab({ return exchangeCodeGitlab({
@@ -120,7 +120,13 @@ export const integrationServiceFactory = ({
secretPath, secretPath,
projectId: integrationAuth.projectId projectId: integrationAuth.projectId
}); });
return { integration, integrationAuth }; return {
integration: {
...integration,
environment: folder.environment
},
integrationAuth
};
}; };
const updateIntegration = async ({ const updateIntegration = async ({
@@ -183,7 +189,10 @@ export const integrationServiceFactory = ({
projectId: folder.projectId projectId: folder.projectId
}); });
return updatedIntegration; return {
...updatedIntegration,
environment: folder.environment
};
}; };
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => { const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
@@ -249,27 +258,7 @@ export const integrationServiceFactory = ({
}); });
} }
const deletedIntegration = await integrationDAL.transaction(async (tx) => { const deletedIntegration = await integrationDAL.deleteById(id);
// delete integration
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
// check if there are other integrations that share the same integration auth
const integrations = await integrationDAL.find(
{
integrationAuthId: integration.integrationAuthId
},
tx
);
if (integrations.length === 0) {
// no other integration shares the same integration auth
// -> delete the integration auth
await integrationAuthDAL.deleteById(integration.integrationAuthId, tx);
}
return deletedIntegrationResult;
});
return { ...integration, ...deletedIntegration }; return { ...integration, ...deletedIntegration };
}; };
+37 -30
View File
@@ -160,8 +160,8 @@ export const kmsServiceFactory = ({
* In mean time the rest of the request will wait until creation is finished followed by getting the created on * In mean time the rest of the request will wait until creation is finished followed by getting the created on
* In real time this would be milliseconds * In real time this would be milliseconds
*/ */
const getOrgKmsKeyId = async (orgId: string) => { const getOrgKmsKeyId = async (orgId: string, trx?: Knex) => {
let org = await orgDAL.findById(orgId); let org = await orgDAL.findById(orgId, trx);
if (!org) { if (!org) {
throw new NotFoundError({ message: "Org not found" }); throw new NotFoundError({ message: "Org not found" });
@@ -180,9 +180,9 @@ export const kmsServiceFactory = ({
waitingCb: () => logger.info("KMS. Waiting for org key to be created") waitingCb: () => logger.info("KMS. Waiting for org key to be created")
}); });
org = await orgDAL.findById(orgId); org = await orgDAL.findById(orgId, trx);
} else { } else {
const keyId = await orgDAL.transaction(async (tx) => { const keyId = await (trx || orgDAL).transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx); org = await orgDAL.findById(orgId, tx);
if (org.kmsDefaultKeyId) { if (org.kmsDefaultKeyId) {
return org.kmsDefaultKeyId; return org.kmsDefaultKeyId;
@@ -240,11 +240,12 @@ export const kmsServiceFactory = ({
const decryptWithKmsKey = async ({ const decryptWithKmsKey = async ({
kmsId, kmsId,
depth = 0 depth = 0,
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number }) => { tx
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number; tx?: Knex }) => {
if (depth > 2) throw new BadRequestError({ message: "KMS depth max limit" }); if (depth > 2) throw new BadRequestError({ message: "KMS depth max limit" });
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) { if (!kmsDoc) {
throw new NotFoundError({ message: "KMS ID not found" }); throw new NotFoundError({ message: "KMS ID not found" });
} }
@@ -261,7 +262,8 @@ export const kmsServiceFactory = ({
// we put a limit of depth to avoid too many cycles // we put a limit of depth to avoid too many cycles
const orgKmsDecryptor = await decryptWithKmsKey({ const orgKmsDecryptor = await decryptWithKmsKey({
kmsId: kmsDoc.orgKms.id, kmsId: kmsDoc.orgKms.id,
depth: depth + 1 depth: depth + 1,
tx
}); });
const orgKmsDataKey = await orgKmsDecryptor({ const orgKmsDataKey = await orgKmsDecryptor({
@@ -375,9 +377,9 @@ export const kmsServiceFactory = ({
}; };
}; };
const $getOrgKmsDataKey = async (orgId: string) => { const $getOrgKmsDataKey = async (orgId: string, trx?: Knex) => {
const kmsKeyId = await getOrgKmsKeyId(orgId); const kmsKeyId = await getOrgKmsKeyId(orgId, trx);
let org = await orgDAL.findById(orgId); let org = await orgDAL.findById(orgId, trx);
if (!org) { if (!org) {
throw new NotFoundError({ message: "Org not found" }); throw new NotFoundError({ message: "Org not found" });
@@ -396,9 +398,9 @@ export const kmsServiceFactory = ({
waitingCb: () => logger.info("KMS. Waiting for org data key to be created") waitingCb: () => logger.info("KMS. Waiting for org data key to be created")
}); });
org = await orgDAL.findById(orgId); org = await orgDAL.findById(orgId, trx);
} else { } else {
const orgDataKey = await orgDAL.transaction(async (tx) => { const orgDataKey = await (trx || orgDAL).transaction(async (tx) => {
org = await orgDAL.findById(orgId, tx); org = await orgDAL.findById(orgId, tx);
if (org.kmsEncryptedDataKey) { if (org.kmsEncryptedDataKey) {
return; return;
@@ -455,8 +457,8 @@ export const kmsServiceFactory = ({
}); });
}; };
const getProjectSecretManagerKmsKeyId = async (projectId: string) => { const getProjectSecretManagerKmsKeyId = async (projectId: string, trx?: Knex) => {
let project = await projectDAL.findById(projectId); let project = await projectDAL.findById(projectId, trx);
if (!project) { if (!project) {
throw new NotFoundError({ message: "Project not found" }); throw new NotFoundError({ message: "Project not found" });
} }
@@ -477,7 +479,7 @@ export const kmsServiceFactory = ({
project = await projectDAL.findById(projectId); project = await projectDAL.findById(projectId);
} else { } else {
const kmsKeyId = await projectDAL.transaction(async (tx) => { const kmsKeyId = await (trx || projectDAL).transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx); project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerKeyId) { if (project.kmsSecretManagerKeyId) {
return project.kmsSecretManagerKeyId; return project.kmsSecretManagerKeyId;
@@ -520,9 +522,9 @@ export const kmsServiceFactory = ({
return project.kmsSecretManagerKeyId; return project.kmsSecretManagerKeyId;
}; };
const $getProjectSecretManagerKmsDataKey = async (projectId: string) => { const $getProjectSecretManagerKmsDataKey = async (projectId: string, trx?: Knex) => {
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId); const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId, trx);
let project = await projectDAL.findById(projectId); let project = await projectDAL.findById(projectId, trx);
if (!project.kmsSecretManagerEncryptedDataKey) { if (!project.kmsSecretManagerEncryptedDataKey) {
const lock = await keyStore const lock = await keyStore
@@ -538,18 +540,21 @@ export const kmsServiceFactory = ({
delay: 500 delay: 500
}); });
project = await projectDAL.findById(projectId); project = await projectDAL.findById(projectId, trx);
} else { } else {
const projectDataKey = await projectDAL.transaction(async (tx) => { const projectDataKey = await (trx || projectDAL).transaction(async (tx) => {
project = await projectDAL.findById(projectId, tx); project = await projectDAL.findById(projectId, tx);
if (project.kmsSecretManagerEncryptedDataKey) { if (project.kmsSecretManagerEncryptedDataKey) {
return; return;
} }
const dataKey = randomSecureBytes(); const dataKey = randomSecureBytes();
const kmsEncryptor = await encryptWithKmsKey({ const kmsEncryptor = await encryptWithKmsKey(
kmsId: kmsKeyId {
}); kmsId: kmsKeyId
},
tx
);
const { cipherTextBlob } = await kmsEncryptor({ const { cipherTextBlob } = await kmsEncryptor({
plainText: dataKey plainText: dataKey
@@ -585,7 +590,8 @@ export const kmsServiceFactory = ({
} }
const kmsDecryptor = await decryptWithKmsKey({ const kmsDecryptor = await decryptWithKmsKey({
kmsId: kmsKeyId kmsId: kmsKeyId,
tx: trx
}); });
return kmsDecryptor({ return kmsDecryptor({
@@ -593,13 +599,13 @@ export const kmsServiceFactory = ({
}); });
}; };
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => { const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
switch (dto.type) { switch (dto.type) {
case KmsDataKey.SecretManager: { case KmsDataKey.SecretManager: {
return $getProjectSecretManagerKmsDataKey(dto.projectId); return $getProjectSecretManagerKmsDataKey(dto.projectId, trx);
} }
default: { default: {
return $getOrgKmsDataKey(dto.orgId); return $getOrgKmsDataKey(dto.orgId, trx);
} }
} }
}; };
@@ -607,8 +613,9 @@ export const kmsServiceFactory = ({
// by keeping the decrypted data key in inner scope // by keeping the decrypted data key in inner scope
// none of the entities outside can interact directly or expose the data key // none of the entities outside can interact directly or expose the data key
// NOTICE: If changing here update migrations/utils/kms // NOTICE: If changing here update migrations/utils/kms
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => { const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
const dataKey = await $getDataKey(encryptionContext); const dataKey = await $getDataKey(encryptionContext, trx);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
return { return {
+11 -5
View File
@@ -106,14 +106,19 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("superAdmin").withSchema(TableName.Users),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey) db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
) )
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER .where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
.orderBy("firstName")
.orderBy("lastName");
return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({ return members.map(
...data, ({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, superAdmin, ...data }) => ({
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey } ...data,
})); user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey, superAdmin }
})
);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find all org members" }); throw new DatabaseError({ error, name: "Find all org members" });
} }
@@ -370,6 +375,7 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization), db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("defaultMembershipRole").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases) db.ref("externalId").withSchema(TableName.UserAliases)
) )
.where({ isGhost: false }); .where({ isGhost: false });
+52
View File
@@ -0,0 +1,52 @@
import { OrgMembershipRole } from "@app/db/schemas";
import { TFeatureSet } from "@app/ee/services/license/license-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
const RESERVED_ORG_ROLE_SLUGS = Object.values(OrgMembershipRole).filter((role) => role !== "custom");
export const isCustomOrgRole = (roleSlug: string) => !RESERVED_ORG_ROLE_SLUGS.includes(roleSlug as OrgMembershipRole);
// this is only for updating an org
export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
membershipRoleSlug,
orgRoleDAL,
plan,
orgId
}: {
orgId: string;
membershipRoleSlug: string;
orgRoleDAL: TOrgRoleDALFactory;
plan: TFeatureSet;
}) => {
if (isCustomOrgRole(membershipRoleSlug)) {
if (!plan?.rbac)
throw new BadRequestError({
message:
"Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role."
});
const customRole = await orgRoleDAL.findOne({ slug: membershipRoleSlug, orgId });
if (!customRole) throw new NotFoundError({ name: "UpdateOrg", message: "Organization role not found" });
// use ID for default role
return customRole.id;
}
// not custom, use reserved slug
return membershipRoleSlug;
};
// this is only for creating an org membership
export const getDefaultOrgMembershipRole = async (
defaultOrgMembershipRole: string // can either be ID or reserved slug
) => {
if (isCustomOrgRole(defaultOrgMembershipRole))
return {
roleId: defaultOrgMembershipRole,
role: OrgMembershipRole.Custom
};
// will be reserved slug
return { roleId: undefined, role: defaultOrgMembershipRole as OrgMembershipRole };
};
+34 -1
View File
@@ -11,6 +11,8 @@ import {
} from "@app/ee/services/permission/org-permission"; } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { ActorAuthMethod } from "../auth/auth-type"; import { ActorAuthMethod } from "../auth/auth-type";
import { TOrgRoleDALFactory } from "./org-role-dal"; import { TOrgRoleDALFactory } from "./org-role-dal";
@@ -18,11 +20,18 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
type TOrgRoleServiceFactoryDep = { type TOrgRoleServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory; orgRoleDAL: TOrgRoleDALFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
orgDAL: TOrgDALFactory;
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
}; };
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>; export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRoleServiceFactoryDep) => { export const orgRoleServiceFactory = ({
orgRoleDAL,
orgDAL,
permissionService,
externalGroupOrgRoleMappingDAL
}: TOrgRoleServiceFactoryDep) => {
const createRole = async ( const createRole = async (
userId: string, userId: string,
orgId: string, orgId: string,
@@ -129,6 +138,30 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
) => { ) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
const org = await orgDAL.findOrgById(orgId);
if (!org)
throw new NotFoundError({
message: "Failed to find organization"
});
if (org.defaultMembershipRole === roleId)
throw new BadRequestError({
message: "Cannot delete default org membership role. Please re-assign and try again."
});
const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({
orgId,
roleId
});
if (externalGroupMapping)
throw new BadRequestError({
message:
"Cannot delete role assigned to external group organization role mapping. Please re-assign external mapping and try again."
});
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId }); const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" }); if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" });
+90 -20
View File
@@ -32,6 +32,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -40,8 +41,9 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns"; import { assignWorkspaceKeysToMembers, createProjectKey } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
@@ -79,7 +81,7 @@ type TOrgServiceFactoryDep = {
TProjectMembershipDALFactory, TProjectMembershipDALFactory,
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction" "findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
@@ -93,8 +95,9 @@ type TOrgServiceFactoryDep = {
>; >;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
}; };
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>; export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@@ -121,7 +124,8 @@ export const orgServiceFactory = ({
oidcConfigDAL, oidcConfigDAL,
projectBotDAL, projectBotDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityMetadataDAL identityMetadataDAL,
projectBotService
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
/* /*
* Get organization details by the organization id * Get organization details by the organization id
@@ -264,7 +268,7 @@ export const orgServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
orgId, orgId,
data: { name, slug, authEnforced, scimEnabled } data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug }
}: TUpdateOrgDTO) => { }: TUpdateOrgDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
@@ -298,11 +302,22 @@ export const orgServiceFactory = ({
}); });
} }
let defaultMembershipRole: string | undefined;
if (defaultMembershipRoleSlug) {
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
membershipRoleSlug: defaultMembershipRoleSlug,
orgId,
orgRoleDAL,
plan
});
}
const org = await orgDAL.updateById(orgId, { const org = await orgDAL.updateById(orgId, {
name, name,
slug: slug ? slugify(slug) : undefined, slug: slug ? slugify(slug) : undefined,
authEnforced, authEnforced,
scimEnabled scimEnabled,
defaultMembershipRole
}); });
if (!org) throw new NotFoundError({ message: "Organization not found" }); if (!org) throw new NotFoundError({ message: "Organization not found" });
return org; return org;
@@ -706,20 +721,67 @@ export const orgServiceFactory = ({
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx); // this will auto generate bot
if (!ghostUser) { const { botKey, bot: autoGeneratedBot } = await projectBotService.getBotKey(projectId, true);
throw new NotFoundError({
name: "InviteUser",
message: "Failed to find project owner"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx); const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
if (!ghostUserLatestKey) { let ghostUserId = ghostUser?.id;
throw new NotFoundError({
name: "InviteUser", // backfill missing ghost user
message: "Failed to find project owner's latest key" if (!ghostUserId) {
const newGhostUser = await addGhostUser(project.orgId, tx);
const projectMembership = await projectMembershipDAL.create(
{
userId: newGhostUser.user.id,
projectId: project.id
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: newGhostUser.keys.publicKey,
privateKey: newGhostUser.keys.plainPrivateKey,
plainProjectKey: botKey
}); });
// 4. Save the project key for the ghost user.
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: newGhostUser.user.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: newGhostUser.user.id
},
tx
);
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(
newGhostUser.keys.plainPrivateKey
);
if (autoGeneratedBot) {
await projectBotDAL.updateById(
autoGeneratedBot.id,
{
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: newGhostUser.keys.publicKey,
senderId: newGhostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
}
ghostUserId = newGhostUser.user.id;
} }
const bot = await projectBotDAL.findOne({ projectId }, tx); const bot = await projectBotDAL.findOne({ projectId }, tx);
@@ -730,6 +792,14 @@ export const orgServiceFactory = ({
}); });
} }
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUserId, projectId, tx);
if (!ghostUserLatestKey) {
throw new NotFoundError({
name: "InviteUser",
message: "Failed to find project owner's latest key"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({ const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding, keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv, iv: bot.iv,
@@ -773,7 +843,7 @@ export const orgServiceFactory = ({
newWsMembers.map((el) => ({ newWsMembers.map((el) => ({
encryptedKey: el.workspaceEncryptedKey, encryptedKey: el.workspaceEncryptedKey,
nonce: el.workspaceEncryptedNonce, nonce: el.workspaceEncryptedNonce,
senderId: ghostUser.id, senderId: ghostUserId,
receiverId: el.orgMembershipId, receiverId: el.orgMembershipId,
projectId projectId
})), })),
+8 -7
View File
@@ -26,18 +26,13 @@ export type TDeleteOrgMembershipDTO = {
}; };
export type TInviteUserToOrgDTO = { export type TInviteUserToOrgDTO = {
actorId: string;
actor: ActorType;
orgId: string;
actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod;
inviteeEmails: string[]; inviteeEmails: string[];
organizationRoleSlug: string; organizationRoleSlug: string;
projects?: { projects?: {
id: string; id: string;
projectRoleSlug?: string[]; projectRoleSlug?: string[];
}[]; }[];
}; } & TOrgPermission;
export type TVerifyUserToOrgDTO = { export type TVerifyUserToOrgDTO = {
email: string; email: string;
@@ -63,7 +58,13 @@ export type TFindAllWorkspacesDTO = {
}; };
export type TUpdateOrgDTO = { export type TUpdateOrgDTO = {
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>; data: Partial<{
name: string;
slug: string;
authEnforced: boolean;
scimEnabled: boolean;
defaultMembershipRoleSlug: string;
}>;
} & TOrgPermission; } & TOrgPermission;
export type TGetOrgGroupsDTO = TOrgPermission; export type TGetOrgGroupsDTO = TOrgPermission;
@@ -24,14 +24,14 @@ export const getBotKeyFnFactory = (
projectBotDAL: TProjectBotDALFactory, projectBotDAL: TProjectBotDALFactory,
projectDAL: Pick<TProjectDALFactory, "findById"> projectDAL: Pick<TProjectDALFactory, "findById">
) => { ) => {
const getBotKeyFn = async (projectId: string) => { const getBotKeyFn = async (projectId: string, shouldGetBotKey?: boolean) => {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) if (!project)
throw new NotFoundError({ throw new NotFoundError({
message: "Project not found during bot lookup. Are you sure you are using the correct project ID?" message: "Project not found during bot lookup. Are you sure you are using the correct project ID?"
}); });
if (project.version === 3) { if (project.version === 3 && !shouldGetBotKey) {
return { project, shouldUseSecretV2Bridge: true }; return { project, shouldUseSecretV2Bridge: true };
} }
@@ -65,8 +65,9 @@ export const getBotKeyFnFactory = (
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(botKey.privateKey); const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(botKey.privateKey);
const encryptedWorkspaceKey = encryptAsymmetric(workspaceKey, botKey.publicKey, userPrivateKey); const encryptedWorkspaceKey = encryptAsymmetric(workspaceKey, botKey.publicKey, userPrivateKey);
let botId;
if (!bot) { if (!bot) {
await projectBotDAL.create({ const newBot = await projectBotDAL.create({
name: "Infisical Bot (Ghost)", name: "Infisical Bot (Ghost)",
projectId, projectId,
isActive: true, isActive: true,
@@ -80,8 +81,9 @@ export const getBotKeyFnFactory = (
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce, encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
senderId: projectV1Keys.userId senderId: projectV1Keys.userId
}); });
botId = newBot.id;
} else { } else {
await projectBotDAL.updateById(bot.id, { const updatedBot = await projectBotDAL.updateById(bot.id, {
isActive: true, isActive: true,
tag, tag,
iv, iv,
@@ -93,8 +95,10 @@ export const getBotKeyFnFactory = (
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce, encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
senderId: projectV1Keys.userId senderId: projectV1Keys.userId
}); });
botId = updatedBot.id;
} }
return { botKey: workspaceKey, project, shouldUseSecretV2Bridge: false };
return { botKey: workspaceKey, project, shouldUseSecretV2Bridge: false, bot: { id: botId } };
} }
const botPrivateKey = getBotPrivateKey({ bot }); const botPrivateKey = getBotPrivateKey({ bot });
@@ -104,7 +108,7 @@ export const getBotKeyFnFactory = (
nonce: bot.encryptedProjectKeyNonce, nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey publicKey: bot.sender.publicKey
}); });
return { botKey, project, shouldUseSecretV2Bridge: false }; return { botKey, project, shouldUseSecretV2Bridge: false, bot: { id: bot.id } };
}; };
return getBotKeyFn; return getBotKeyFn;
@@ -27,8 +27,8 @@ export const projectBotServiceFactory = ({
}: TProjectBotServiceFactoryDep) => { }: TProjectBotServiceFactoryDep) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
const getBotKey = async (projectId: string) => { const getBotKey = async (projectId: string, shouldGetBotKey?: boolean) => {
return getBotKeyFn(projectId); return getBotKeyFn(projectId, shouldGetBotKey);
}; };
const findBotByProjectId = async ({ const findBotByProjectId = async ({
@@ -65,10 +65,16 @@ export const projectEnvDALFactory = (db: TDbClient) => {
} }
}; };
const shiftPositions = async (projectId: string, pos: number, tx?: Knex) => {
// Shift all positions >= the new position up by 1
await (tx || db)(TableName.Environment).where({ projectId }).where("position", ">=", pos).increment("position", 1);
};
return { return {
...projectEnvOrm, ...projectEnvOrm,
findBySlugs, findBySlugs,
findLastEnvPosition, findLastEnvPosition,
updateAllPosition updateAllPosition,
shiftPositions
}; };
}; };
@@ -37,6 +37,7 @@ export const projectEnvServiceFactory = ({
actor, actor,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
position,
name, name,
slug slug
}: TCreateEnvDTO) => { }: TCreateEnvDTO) => {
@@ -83,9 +84,25 @@ export const projectEnvServiceFactory = ({
} }
const env = await projectEnvDAL.transaction(async (tx) => { const env = await projectEnvDAL.transaction(async (tx) => {
if (position !== undefined) {
// Check if there's an environment at the specified position
const existingEnvWithPosition = await projectEnvDAL.findOne({ projectId, position }, tx);
// If there is, then shift positions
if (existingEnvWithPosition) {
await projectEnvDAL.shiftPositions(projectId, position, tx);
}
const doc = await projectEnvDAL.create({ slug, name, projectId, position }, tx);
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
return doc;
}
// If no position is specified, add to the end
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx); const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx); const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx); await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
return doc; return doc;
}); });
@@ -150,7 +167,11 @@ export const projectEnvServiceFactory = ({
const env = await projectEnvDAL.transaction(async (tx) => { const env = await projectEnvDAL.transaction(async (tx) => {
if (position) { if (position) {
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx); const existingEnvWithPosition = await projectEnvDAL.findOne({ projectId, position }, tx);
if (existingEnvWithPosition && existingEnvWithPosition.id !== oldEnv.id) {
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
}
} }
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx); return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
}); });
@@ -199,7 +220,6 @@ export const projectEnvServiceFactory = ({
name: "DeleteEnvironment" name: "DeleteEnvironment"
}); });
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
return doc; return doc;
}); });
@@ -215,29 +235,26 @@ export const projectEnvServiceFactory = ({
} }
}; };
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => { const getEnvironmentById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {
const environment = await projectEnvDAL.findById(id);
if (!environment) {
throw new NotFoundError({
message: "Environment does not exist"
});
}
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, environment.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
const [env] = await projectEnvDAL.find({ return environment;
id,
projectId
});
if (!env) {
throw new NotFoundError({
message: "Environment does not exist"
});
}
return env;
}; };
return { return {
@@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
export type TCreateEnvDTO = { export type TCreateEnvDTO = {
name: string; name: string;
slug: string; slug: string;
position?: number;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateEnvDTO = { export type TUpdateEnvDTO = {
@@ -23,4 +24,4 @@ export type TReorderEnvDTO = {
export type TGetEnvDTO = { export type TGetEnvDTO = {
id: string; id: string;
} & TProjectPermission; } & Omit<TProjectPermission, "projectId">;
@@ -147,6 +147,7 @@ export const projectServiceFactory = ({
workspaceName, workspaceName,
slug: projectSlug, slug: projectSlug,
kmsKeyId, kmsKeyId,
tx: trx,
createDefaultEnvs = true createDefaultEnvs = true
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
@@ -169,7 +170,7 @@ export const projectServiceFactory = ({
}); });
} }
const results = await projectDAL.transaction(async (tx) => { const results = await (trx || projectDAL).transaction(async (tx) => {
const ghostUser = await orgService.addGhostUser(organization.id, tx); const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) { if (kmsKeyId) {
@@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TProjectKeys } from "@app/db/schemas"; import { TProjectKeys } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
@@ -30,6 +32,7 @@ export type TCreateProjectDTO = {
slug?: string; slug?: string;
kmsKeyId?: string; kmsKeyId?: string;
createDefaultEnvs?: boolean; createDefaultEnvs?: boolean;
tx?: Knex;
}; };
export type TDeleteProjectBySlugDTO = { export type TDeleteProjectBySlugDTO = {
@@ -502,12 +502,21 @@ export const secretFolderServiceFactory = ({
const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => { const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => {
const folder = await folderDAL.findById(id); const folder = await folderDAL.findById(id);
if (!folder) throw new NotFoundError({ message: "folder not found" }); if (!folder) throw new NotFoundError({ message: "Folder not found" });
// folder list is allowed to be read by anyone // folder list is allowed to be read by anyone
// permission to check does user has access // permission to check does user has access
await permissionService.getProjectPermission(actor, actorId, folder.projectId, actorAuthMethod, actorOrgId); await permissionService.getProjectPermission(actor, actorId, folder.projectId, actorAuthMethod, actorOrgId);
return folder; const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
if (!folderWithPath) {
throw new NotFoundError({ message: "Folder path not found" });
}
return {
...folder,
path: folderWithPath.path
};
}; };
return { return {

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