mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-24 00:15:26 +00:00
Compare commits
194 Commits
revert-252
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
614e4934a2 | |||
14e92f895a | |||
0a38374a73 | |||
ec3b94a335 | |||
ca0241bb51 | |||
7403385e7c | |||
2cd1141a65 | |||
fd7e196f8b | |||
212748f140 | |||
b61582a60e | |||
c5aa1b8664 | |||
90dbb417ac | |||
946651496f | |||
5a8ac850b5 | |||
77a88f1575 | |||
be00d13a46 | |||
84814a0012 | |||
de03692469 | |||
fb2d3e4eb7 | |||
29150e809d | |||
e18a606b23 | |||
67708411cd | |||
3e4bd28916 | |||
a2e16370fa | |||
d677654311 | |||
903fac1005 | |||
ff045214d6 | |||
57dcf5ab28 | |||
959a5ec55b | |||
b22a93a175 | |||
d7d88f3356 | |||
dbaef9d227 | |||
38d8b14b03 | |||
8b9244b079 | |||
3d938ea62f | |||
78f668bd7f | |||
13c0b315a4 | |||
99e65f7b59 | |||
96bad7bf90 | |||
5e5f20cab2 | |||
2383c93139 | |||
154ea9e55d | |||
d36a9e2000 | |||
6f334e4cab | |||
700c5409bf | |||
6158b8a91d | |||
0c3024819c | |||
c8410ac6f3 | |||
41e4af4e65 | |||
bac9936c2a | |||
936a48f458 | |||
43cfd63660 | |||
0f10874f80 | |||
a9e6c229d0 | |||
7cd83ad945 | |||
2f691db0a2 | |||
eb6d5d2fb9 | |||
fc5487396b | |||
6db8c100ba | |||
acfb4693ee | |||
aeaabe2c27 | |||
c60d957269 | |||
b6dc6ffc01 | |||
181821f8f5 | |||
6ac44a79b2 | |||
77740d2c86 | |||
17567ebd0f | |||
7ed0818279 | |||
d94b4b2a3c | |||
9d90c35629 | |||
2cff772caa | |||
849cad054e | |||
518ca5fe58 | |||
65e42f980c | |||
f95957d534 | |||
01920d7a50 | |||
83ac8abf81 | |||
44544e0491 | |||
c47e0d661b | |||
b0fc5c7e27 | |||
bf5d7b2ba1 | |||
5b4c4f4543 | |||
080cf67b8c | |||
36bb954373 | |||
93afa91239 | |||
73fbf66d4c | |||
8ae0d97973 | |||
ca5ec94082 | |||
5d5da97b45 | |||
d61f36bca8 | |||
96f5dc7300 | |||
8e5debca90 | |||
08ed544e52 | |||
8c4a26b0e2 | |||
bda0681dee | |||
cf092d8b4f | |||
a11bcab0db | |||
986bcaf0df | |||
192d1b0be3 | |||
82c8ca9c3d | |||
4a1adb76ab | |||
94b799e80b | |||
bdae136bed | |||
73e73c5489 | |||
f3bcdf74df | |||
87cd3ea727 | |||
114f42fc14 | |||
6daa1aa221 | |||
52f85753c5 | |||
0a5634aa05 | |||
3e8b9aa296 | |||
67058d8b55 | |||
d112ec2f0a | |||
73382c5363 | |||
96c0e718d0 | |||
522e1dfd0e | |||
08145f9b96 | |||
faf2c6df90 | |||
b8f3814df0 | |||
1f4db2bd80 | |||
d8d784a0bc | |||
2dc1416f30 | |||
7fdcb29bab | |||
6a89e3527c | |||
d1d0667cd5 | |||
c176a20010 | |||
865db5a9b3 | |||
ad2f19658b | |||
bed8efb24c | |||
aa9af7b41c | |||
02fd484632 | |||
96eab464c7 | |||
162005d72f | |||
09d28156f8 | |||
fc67c496c5 | |||
540a1a29b1 | |||
3163adf486 | |||
e042f9b5e2 | |||
05a1b5397b | |||
19776df46c | |||
64fd65aa52 | |||
3d58eba78c | |||
565884d089 | |||
2a83da1cb6 | |||
f186ce9649 | |||
6ecfee5faf | |||
662f1a31f6 | |||
06f9a1484b | |||
c90e8ca715 | |||
6ddc4ce4b1 | |||
4fffac07fd | |||
059c552307 | |||
75d71d4208 | |||
e38628509d | |||
0b247176bb | |||
faad09961d | |||
98d4f808e5 | |||
2ae91db65d | |||
529328f0ae | |||
e59d9ff3c6 | |||
4aad36601c | |||
4aaba3ef9f | |||
b482a9cda7 | |||
595eb739af | |||
b46bbea0c5 | |||
6dad24ffde | |||
f8759b9801 | |||
049c77c902 | |||
1478833c9c | |||
c8d40c6905 | |||
ff815b5f42 | |||
e5138d0e99 | |||
f43725a16e | |||
f6c65584bf | |||
246020729e | |||
63cc4e347d | |||
ecaca82d9a | |||
d6ef0d1c83 | |||
f2a7f164e1 | |||
dfbdc46971 | |||
3049f9e719 | |||
391c9abbb0 | |||
e191a72ca0 | |||
68c38f228d | |||
a823347c99 | |||
22b417b50b | |||
98ed063ce6 | |||
eae5e57346 | |||
acf054d992 | |||
9f6d837a9b | |||
5856a42807 | |||
ccbf09398e | |||
afbca118b7 | |||
bd29d6feb9 |
.env.example.env.migration.example
.github/workflows
Dockerfile.standalone-infisicalREADME.mdbackend
package-lock.jsonpackage.json
scripts
src
@types
db
auditlog-knexfile.tsindex.tsinstance.ts
manual-migrations
migrations
20241007052025_make-audit-log-independent.ts20241007202149_default-org-membership-roles.ts20241015084434_increase-identity-metadata-col-length.ts20241015145450_external-group-org-role-mapping.ts20241021114650_add-missing-org-cascade-references.ts
schemas
ee
routes/v1
services
access-approval-policy
access-approval-request
audit-log
group
ldap-config
oidc
saml-config
scim
secret-scanning
secret-snapshot
lib
main.tsqueue
server
services
external-group-org-role-mapping
external-group-org-role-mapping-dal.tsexternal-group-org-role-mapping-fns.tsexternal-group-org-role-mapping-service.tsexternal-group-org-role-mapping-types.ts
external-migration
external-migration-fns.tsexternal-migration-queue.tsexternal-migration-service.tsexternal-migration-types.ts
integration-auth
integration-app-list.tsintegration-auth-dal.tsintegration-auth-schema.tsintegration-auth-service.tsintegration-auth-types.tsintegration-delete-secret.tsintegration-list.tsintegration-sync-secret.tsintegration-token.ts
integration
kms
org
project-bot
project
secret-v2-bridge
secret
smtp
cli
docs
changelog
cli/commands
contributing/platform
documentation/platform
images
integrations/github
app
github-app-installation.pnggithub-app-method-selection.pngintegration-overview.pngself-hosted-github-app-basic-details.pngself-hosted-github-app-create-confirm.pngself-hosted-github-app-create.pngself-hosted-github-app-credentials.pngself-hosted-github-app-enable-oauth.pngself-hosted-github-app-organization.pngself-hosted-github-app-private-key.pngself-hosted-github-app-repository.pngself-hosted-github-app-secret.pngself-hosted-github-app-webhook.png
github-oauth-method-selection.pngintegration-overview.pngplatform
integrations
mint.jsonself-hosting/configuration
frontend
package-lock.jsonpackage.jsonindex.tsuseResetPageHelper.ts
src
components
tags/CreateTagModal
v2
helpers
hooks
api
auditLogs
externalGroupOrgRoleMappings
groups
integrationAuth
migration
organization
secrets
users
layouts
pages
integrations
org/[id]/secret-scanning
views
IntegrationsPage
Org
AuditLogsPage
MembersPage/components
OrgGroupsTab/components/OrgGroupsSection
OrgIdentityTab/components/IdentitySection
OrgMembersTab/components/OrgMembersSection
OrgRoleTabSection
RolePage/components
Project
KmsPage/components
MembersPage/components
SecretMainPage
SecretOverviewPage
SecretOverviewPage.tsx
components
Settings/OrgSettingsPage/components
ImportTab/components
OrgAuthTab
OrgNameChangeSection
Signup/components/UserInfoSSOStep
admin/DashboardPage
helm-charts/infisical-standalone-postgres
nginx
@ -36,16 +36,22 @@ CLIENT_ID_HEROKU=
|
||||
CLIENT_ID_VERCEL=
|
||||
CLIENT_ID_NETLIFY=
|
||||
CLIENT_ID_GITHUB=
|
||||
CLIENT_ID_GITHUB_APP=
|
||||
CLIENT_SLUG_GITHUB_APP=
|
||||
CLIENT_ID_GITLAB=
|
||||
CLIENT_ID_BITBUCKET=
|
||||
CLIENT_SECRET_HEROKU=
|
||||
CLIENT_SECRET_VERCEL=
|
||||
CLIENT_SECRET_NETLIFY=
|
||||
CLIENT_SECRET_GITHUB=
|
||||
CLIENT_SECRET_GITHUB_APP=
|
||||
CLIENT_SECRET_GITLAB=
|
||||
CLIENT_SECRET_BITBUCKET=
|
||||
CLIENT_SLUG_VERCEL=
|
||||
|
||||
CLIENT_PRIVATE_KEY_GITHUB_APP=
|
||||
CLIENT_APP_ID_GITHUB_APP=
|
||||
|
||||
# Sentry (optional) for monitoring errors
|
||||
SENTRY_DSN=
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
DB_CONNECTION_URI=
|
||||
AUDIT_LOGS_DB_CONNECTION_URI=
|
||||
|
91
.github/workflows/build-binaries.yml
vendored
91
.github/workflows/build-binaries.yml
vendored
@ -7,7 +7,6 @@ on:
|
||||
description: "Version number"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./backend
|
||||
@ -49,9 +48,9 @@ jobs:
|
||||
- name: Package into node binary
|
||||
run: |
|
||||
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
|
||||
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
|
||||
|
||||
# Set up .deb package structure (Debian/Ubuntu only)
|
||||
@ -83,6 +82,86 @@ jobs:
|
||||
dpkg-deb --build infisical-core
|
||||
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
|
||||
with:
|
||||
python-version: "3.x" # Specify the Python version you need
|
||||
@ -97,6 +176,12 @@ jobs:
|
||||
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
|
||||
|
||||
# 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)
|
||||
- name: Publish to Cloudsmith (Windows)
|
||||
if: matrix.os == 'win'
|
||||
|
@ -127,6 +127,7 @@ jobs:
|
||||
- name: Change directory to backend and install dependencies
|
||||
env:
|
||||
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||
AUDIT_LOGS_DB_CONNECTION_URI: ${{ secrets.AUDIT_LOGS_DB_CONNECTION_URI }}
|
||||
run: |
|
||||
cd backend
|
||||
npm install
|
||||
|
@ -95,6 +95,10 @@ RUN mkdir frontend-build
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
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 \
|
||||
&& adduser --system --uid 1001 non-root-user
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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>
|
||||
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).
|
||||
|
||||
## Security
|
||||
|
||||
@ -163,4 +161,3 @@ Not sure where to get started? You can:
|
||||
- [Twitter](https://twitter.com/infisical) for fast news
|
||||
- [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
|
||||
- [Roadmap](https://www.notion.so/infisical/be2d2585a6694e40889b03aef96ea36b?v=5b19a8127d1a4060b54769567a8785fa) for planned features
|
608
backend/package-lock.json
generated
608
backend/package-lock.json
generated
@ -21,12 +21,14 @@
|
||||
"@fastify/etag": "^5.1.0",
|
||||
"@fastify/formbody": "^7.4.0",
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"@fastify/multipart": "8.3.0",
|
||||
"@fastify/passport": "^2.4.0",
|
||||
"@fastify/rate-limit": "^9.0.0",
|
||||
"@fastify/session": "^10.7.0",
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
@ -61,7 +63,7 @@
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"knex": "^3.0.1",
|
||||
"ldapjs": "^3.0.7",
|
||||
"ldif": "^0.5.1",
|
||||
"ldif": "0.5.1",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"mongodb": "^6.8.1",
|
||||
@ -4311,6 +4313,15 @@
|
||||
"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": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.1.tgz",
|
||||
@ -4381,6 +4392,20 @@
|
||||
"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": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-2.4.0.tgz",
|
||||
@ -4976,24 +5001,73 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-app": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.0.3.tgz",
|
||||
"integrity": "sha512-9N7IlBAKEJR3tJgPSubCxIDYGXSdc+2xbkjYpk9nCyqREnH8qEMoMhiEB1WgoA9yTFp91El92XNXAi+AjuKnfw==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.1.tgz",
|
||||
"integrity": "sha512-kRAd6yelV9OgvlEJE88H0VLlQdZcag9UlLr7dV0YYP37X8PPDvhgiTy66QVhDXdyoT0AleFN2w/qXkPdrSzINg==",
|
||||
"dependencies": {
|
||||
"@octokit/auth-oauth-app": "^7.0.0",
|
||||
"@octokit/auth-oauth-user": "^4.0.0",
|
||||
"@octokit/request": "^8.0.2",
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"deprecation": "^2.3.1",
|
||||
"@octokit/auth-oauth-app": "^8.1.0",
|
||||
"@octokit/auth-oauth-user": "^5.1.0",
|
||||
"@octokit/request": "^9.1.1",
|
||||
"@octokit/request-error": "^6.1.1",
|
||||
"@octokit/types": "^13.4.1",
|
||||
"lru-cache": "^10.0.0",
|
||||
"universal-github-app-jwt": "^1.1.2",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
"universal-github-app-jwt": "^2.2.0",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
|
||||
@ -5002,53 +5076,220 @@
|
||||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.0.1.tgz",
|
||||
"integrity": "sha512-RE0KK0DCjCHXHlQBoubwlLijXEKfhMhKm9gO56xYvFmP1QTMb+vvwRPmQLLx0V+5AvV9N9I3lr1WyTzwL3rMDg==",
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz",
|
||||
"integrity": "sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==",
|
||||
"dependencies": {
|
||||
"@octokit/auth-oauth-device": "^6.0.0",
|
||||
"@octokit/auth-oauth-user": "^4.0.0",
|
||||
"@octokit/request": "^8.0.2",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@types/btoa-lite": "^1.0.0",
|
||||
"btoa-lite": "^1.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
"@octokit/auth-oauth-device": "^7.0.0",
|
||||
"@octokit/auth-oauth-user": "^5.0.1",
|
||||
"@octokit/request": "^9.0.0",
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.0.1.tgz",
|
||||
"integrity": "sha512-yxU0rkL65QkjbqQedgVx3gmW7YM5fF+r5uaSj9tM/cQGVqloXcqP2xK90eTyYvl29arFVCW8Vz4H/t47mL0ELw==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz",
|
||||
"integrity": "sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==",
|
||||
"dependencies": {
|
||||
"@octokit/oauth-methods": "^4.0.0",
|
||||
"@octokit/request": "^8.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
"@octokit/oauth-methods": "^5.0.0",
|
||||
"@octokit/request": "^9.0.0",
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-oauth-user": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.0.1.tgz",
|
||||
"integrity": "sha512-N94wWW09d0hleCnrO5wt5MxekatqEJ4zf+1vSe8MKMrhZ7gAXKFOKrDEZW2INltvBWJCyDUELgGRv8gfErH1Iw==",
|
||||
"node_modules/@octokit/auth-oauth-device/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/auth-oauth-device": "^6.0.0",
|
||||
"@octokit/oauth-methods": "^4.0.0",
|
||||
"@octokit/request": "^8.0.2",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"btoa-lite": "^1.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
"@octokit/types": "^13.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
|
||||
@ -5112,28 +5353,82 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz",
|
||||
"integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/oauth-methods": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.0.1.tgz",
|
||||
"integrity": "sha512-1NdTGCoBHyD6J0n2WGXg9+yDLZrRNZ0moTEex/LSPr49m530WNKcCfXDghofYptr3st3eTii+EHoG5k/o+vbtw==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.2.tgz",
|
||||
"integrity": "sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==",
|
||||
"dependencies": {
|
||||
"@octokit/oauth-authorization-url": "^6.0.2",
|
||||
"@octokit/request": "^8.0.2",
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"btoa-lite": "^1.0.0"
|
||||
"@octokit/oauth-authorization-url": "^7.0.0",
|
||||
"@octokit/request": "^9.1.0",
|
||||
"@octokit/request-error": "^6.1.0",
|
||||
"@octokit/types": "^13.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz",
|
||||
@ -5248,13 +5543,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"version": "8.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz",
|
||||
"integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==",
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz",
|
||||
"integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^9.0.0",
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@octokit/endpoint": "^9.0.1",
|
||||
"@octokit/request-error": "^5.1.0",
|
||||
"@octokit/types": "^13.1.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -5262,11 +5557,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz",
|
||||
"integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz",
|
||||
"integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@octokit/types": "^13.1.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
},
|
||||
@ -5274,6 +5569,32 @@
|
||||
"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": {
|
||||
"version": "20.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz",
|
||||
@ -14160,6 +14481,154 @@
|
||||
"@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": {
|
||||
"version": "5.0.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@ -18143,13 +18621,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/universal-github-app-jwt": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.1.2.tgz",
|
||||
"integrity": "sha512-t1iB2FmLFE+yyJY9+3wMx0ejB+MQpEVkH0gQv7dR6FZyltyq+ZZO0uDpbopxhrZ3SLEO4dCEkIujOMldEQ2iOA==",
|
||||
"dependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.0",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
}
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz",
|
||||
"integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ=="
|
||||
},
|
||||
"node_modules/universal-user-agent": {
|
||||
"version": "6.0.1",
|
||||
|
@ -45,13 +45,20 @@
|
||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
|
||||
"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:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||
"migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||
"migration:up": "npm run auditlog-migration:up && knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||
"migration:down": "npm run auditlog-migration:down && knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||
"migration:list": "npm run auditlog-migration:list && knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||
"migration:latest": "npm run auditlog-migration:latest && knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
|
||||
@ -119,12 +126,14 @@
|
||||
"@fastify/etag": "^5.1.0",
|
||||
"@fastify/formbody": "^7.4.0",
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"@fastify/multipart": "8.3.0",
|
||||
"@fastify/passport": "^2.4.0",
|
||||
"@fastify/rate-limit": "^9.0.0",
|
||||
"@fastify/session": "^10.7.0",
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
|
@ -90,7 +90,12 @@ const main = async () => {
|
||||
.whereRaw("table_schema = current_schema()")
|
||||
.select<{ tableName: string }[]>("table_name as tableName")
|
||||
.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) {
|
||||
const { tableName } = tables[i];
|
||||
|
84
backend/scripts/migrate-organization.ts
Normal file
84
backend/scripts/migrate-organization.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/* eslint-disable */
|
||||
import promptSync from "prompt-sync";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const prompt = promptSync({
|
||||
sigint: true
|
||||
});
|
||||
|
||||
const exportDb = () => {
|
||||
const exportHost = prompt("Enter your Postgres Host to migrate from: ");
|
||||
const exportPort = prompt("Enter your Postgres Port to migrate from [Default = 5432]: ") ?? "5432";
|
||||
const exportUser = prompt("Enter your Postgres User to migrate from: [Default = infisical]: ") ?? "infisical";
|
||||
const exportPassword = prompt("Enter your Postgres Password to migrate from: ");
|
||||
const exportDatabase = prompt("Enter your Postgres Database to migrate from [Default = infisical]: ") ?? "infisical";
|
||||
|
||||
// we do not include the audit_log and secret_sharing entries
|
||||
execSync(
|
||||
`PGDATABASE="${exportDatabase}" PGPASSWORD="${exportPassword}" PGHOST="${exportHost}" PGPORT=${exportPort} PGUSER=${exportUser} pg_dump infisical --exclude-table-data="secret_sharing" --exclude-table-data="audit_log*" > ${path.join(
|
||||
__dirname,
|
||||
"../src/db/dump.sql"
|
||||
)}`,
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
};
|
||||
|
||||
const importDbForOrg = () => {
|
||||
const importHost = prompt("Enter your Postgres Host to migrate to: ");
|
||||
const importPort = prompt("Enter your Postgres Port to migrate to [Default = 5432]: ") ?? "5432";
|
||||
const importUser = prompt("Enter your Postgres User to migrate to: [Default = infisical]: ") ?? "infisical";
|
||||
const importPassword = prompt("Enter your Postgres Password to migrate to: ");
|
||||
const importDatabase = prompt("Enter your Postgres Database to migrate to [Default = infisical]: ") ?? "infisical";
|
||||
const orgId = prompt("Enter the organization ID to migrate: ");
|
||||
|
||||
if (!existsSync(path.join(__dirname, "../src/db/dump.sql"))) {
|
||||
console.log("File not found, please export the database first.");
|
||||
return;
|
||||
}
|
||||
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -f ${path.join(
|
||||
__dirname,
|
||||
"../src/db/dump.sql"
|
||||
)}`
|
||||
);
|
||||
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c "DELETE FROM public.organizations WHERE id != '${orgId}'"`
|
||||
);
|
||||
|
||||
// delete global/instance-level resources not relevant to the organization to migrate
|
||||
// users
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM users WHERE users.id NOT IN (SELECT org_memberships."userId" FROM org_memberships)'`
|
||||
);
|
||||
|
||||
// identities
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'DELETE FROM identities WHERE id NOT IN (SELECT "identityId" FROM identity_org_memberships)'`
|
||||
);
|
||||
|
||||
// reset slack configuration in superAdmin
|
||||
execSync(
|
||||
`PGDATABASE="${importDatabase}" PGPASSWORD="${importPassword}" PGHOST="${importHost}" PGPORT=${importPort} PGUSER=${importUser} psql -c 'UPDATE super_admin SET "encryptedSlackClientId" = null, "encryptedSlackClientSecret" = null'`
|
||||
);
|
||||
|
||||
console.log("Organization migrated successfully.");
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
const action = prompt(
|
||||
"Enter the action to perform\n 1. Export from existing instance.\n 2. Import org to instance.\n \n Action: "
|
||||
);
|
||||
if (action === "1") {
|
||||
exportDb();
|
||||
} else if (action === "2") {
|
||||
importDbForOrg();
|
||||
} else {
|
||||
console.log("Invalid action");
|
||||
}
|
||||
};
|
||||
|
||||
main();
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -39,6 +39,7 @@ import { TCertificateServiceFactory } from "@app/services/certificate/certificat
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-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 { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
@ -185,6 +186,7 @@ declare module "fastify" {
|
||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||
cmek: TCmekServiceFactory;
|
||||
migration: TExternalMigrationServiceFactory;
|
||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
10
backend/src/@types/knex.d.ts
vendored
10
backend/src/@types/knex.d.ts
vendored
@ -336,6 +336,11 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TExternalGroupOrgRoleMappings,
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate
|
||||
} from "@app/db/schemas/external-group-org-role-mappings";
|
||||
import {
|
||||
TSecretV2TagJunction,
|
||||
TSecretV2TagJunctionInsert,
|
||||
@ -808,5 +813,10 @@ declare module "knex/types/tables" {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
>;
|
||||
[TableName.ExternalGroupOrgRoleMapping]: KnexOriginal.CompositeTableType<
|
||||
TExternalGroupOrgRoleMappings,
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
75
backend/src/db/auditlog-knexfile.ts
Normal file
75
backend/src/db/auditlog-knexfile.ts
Normal 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,2 +1,2 @@
|
||||
export type { TDbClient } from "./instance";
|
||||
export { initDbConnection } from "./instance";
|
||||
export { initAuditLogDbConnection, initDbConnection } from "./instance";
|
||||
|
@ -70,3 +70,45 @@ export const initDbConnection = ({
|
||||
|
||||
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;
|
||||
};
|
||||
|
161
backend/src/db/manual-migrations/partition-audit-logs.ts
Normal file
161
backend/src/db/manual-migrations/partition-audit-logs.ts
Normal file
@ -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,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);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SamlConfig, "orgId")) {
|
||||
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
|
||||
t.dropForeign("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SamlConfig, "orgId")) {
|
||||
await knex.schema.alterTable(TableName.SamlConfig, (t) => {
|
||||
t.dropForeign("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization);
|
||||
});
|
||||
}
|
||||
}
|
@ -20,7 +20,8 @@ export const AuditLogsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
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>;
|
||||
|
27
backend/src/db/schemas/external-group-org-role-mappings.ts
Normal file
27
backend/src/db/schemas/external-group-org-role-mappings.ts
Normal file
@ -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>
|
||||
>;
|
@ -17,6 +17,7 @@ export enum TableName {
|
||||
Groups = "groups",
|
||||
GroupProjectMembership = "group_project_memberships",
|
||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||
ExternalGroupOrgRoleMapping = "external_group_org_role_mappings",
|
||||
UserGroupMembership = "user_group_membership",
|
||||
UserAliases = "user_aliases",
|
||||
UserEncryptionKey = "user_encryption_keys",
|
||||
|
@ -19,7 +19,8 @@ export const OrganizationsSchema = z.object({
|
||||
authEnforced: z.boolean().default(false).nullable().optional(),
|
||||
scimEnabled: z.boolean().default(false).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>;
|
||||
|
@ -165,7 +165,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
|
||||
username: z.string().optional().describe(GROUPS.LIST_USERS.username)
|
||||
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
|
||||
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -128,7 +128,10 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
.map((key) => {
|
||||
// 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;
|
||||
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));
|
||||
|
||||
|
@ -20,7 +20,7 @@ const ScimUserSchema = z.object({
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
type: z.string().trim().default("work")
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
@ -210,8 +210,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
value: z.string().email()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
@ -281,8 +280,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
.array(
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
value: z.string().email()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
@ -301,7 +299,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
z.object({
|
||||
primary: z.boolean(),
|
||||
value: z.string().email(),
|
||||
type: z.string().trim()
|
||||
type: z.string().trim().default("work")
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
|
@ -2,6 +2,8 @@ import { z } from "zod";
|
||||
|
||||
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -23,6 +25,13 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
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({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -30,6 +39,7 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.body.organizationId
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
});
|
||||
|
@ -124,7 +124,9 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
const verifyAllApprovers = [...approverUserIds];
|
||||
|
||||
for (const groupId of groupApprovers) {
|
||||
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||
usersPromises.push(
|
||||
groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }).then((group) => group.members)
|
||||
);
|
||||
}
|
||||
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||
.flat()
|
||||
@ -327,7 +329,11 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
>[] = [];
|
||||
|
||||
for (const groupId of groupApprovers) {
|
||||
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||
usersPromises.push(
|
||||
groupDAL
|
||||
.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 })
|
||||
.then((group) => group.members)
|
||||
);
|
||||
}
|
||||
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||
.flat()
|
||||
|
@ -147,10 +147,12 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
const groupUsers = (
|
||||
await Promise.all(
|
||||
approverGroupIds.map((groupApproverId) =>
|
||||
groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: actorOrgId,
|
||||
groupId: groupApproverId
|
||||
})
|
||||
groupDAL
|
||||
.findAllGroupPossibleMembers({
|
||||
orgId: actorOrgId,
|
||||
groupId: groupApproverId
|
||||
})
|
||||
.then((group) => group.members)
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
|
@ -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 { AuditLogsSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
@ -46,7 +47,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
eventType?: EventType[];
|
||||
eventMetadata?: Record<string, string>;
|
||||
},
|
||||
tx?: Knex
|
||||
tx?: knex.Knex
|
||||
) => {
|
||||
if (!orgId && !projectId) {
|
||||
throw new Error("Either orgId or projectId must be provided");
|
||||
@ -55,11 +56,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
// Find statements
|
||||
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
||||
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
||||
// eslint-disable-next-line func-names
|
||||
.where(function () {
|
||||
if (orgId) {
|
||||
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId);
|
||||
void this.where(`${TableName.AuditLog}.orgId`, orgId);
|
||||
} else if (projectId) {
|
||||
void this.where(`${TableName.AuditLog}.projectId`, projectId);
|
||||
}
|
||||
@ -72,23 +72,19 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
// Select statements
|
||||
void sqlQuery
|
||||
.select(selectAllTableCols(TableName.AuditLog))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||
|
||||
// Special case: Filter by actor ID
|
||||
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
|
||||
if (eventMetadata && Object.keys(eventMetadata).length) {
|
||||
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) {
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
|
||||
}
|
||||
const docs = await sqlQuery;
|
||||
|
||||
return docs.map((doc) => {
|
||||
// Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above.
|
||||
// This is a quick and dirty way to get around the types.
|
||||
const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
|
||||
// we timeout long running queries to prevent DB resource issues (2 minutes)
|
||||
const docs = await sqlQuery.timeout(1000 * 120);
|
||||
|
||||
return {
|
||||
...AuditLogsSchema.parse(doc),
|
||||
...(projectDoc?.projectSlug && {
|
||||
project: {
|
||||
name: projectDoc.projectName,
|
||||
slug: projectDoc.projectSlug
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
return docs;
|
||||
} 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 });
|
||||
}
|
||||
};
|
||||
|
||||
// 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 MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
@ -148,6 +139,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.where("expiresAt", "<", today)
|
||||
.select("id")
|
||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await (tx || db)(TableName.AuditLog)
|
||||
.whereIn("id", findExpiredLogSubQuery)
|
||||
|
@ -74,6 +74,7 @@ export const auditLogQueueServiceFactory = ({
|
||||
actorMetadata: actor.metadata,
|
||||
userAgent,
|
||||
projectId,
|
||||
projectName: project?.name,
|
||||
ipAddress,
|
||||
orgId,
|
||||
eventType: event.type,
|
||||
|
@ -190,7 +190,9 @@ export enum EventType {
|
||||
DELETE_CMEK = "delete-cmek",
|
||||
GET_CMEKS = "get-cmeks",
|
||||
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 {
|
||||
@ -1604,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 =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1750,4 +1764,6 @@ export type Event =
|
||||
| DeleteCmekEvent
|
||||
| GetCmeksEvent
|
||||
| CmekEncryptEvent
|
||||
| CmekDecryptEvent;
|
||||
| CmekDecryptEvent
|
||||
| GetExternalGroupOrgRoleMappingsEvent
|
||||
| UpdateExternalGroupOrgRoleMappingsEvent;
|
||||
|
@ -65,16 +65,18 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
groupId,
|
||||
offset = 0,
|
||||
limit,
|
||||
username
|
||||
username, // depreciated in favor of search
|
||||
search
|
||||
}: {
|
||||
orgId: string;
|
||||
groupId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
username?: string;
|
||||
search?: string;
|
||||
}) => {
|
||||
try {
|
||||
let query = db
|
||||
const query = db
|
||||
.replicaNode()(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
@ -92,31 +94,39 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("firstName").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.raw(`count(*) OVER() as total_count`)
|
||||
)
|
||||
.where({ isGhost: false })
|
||||
.offset(offset);
|
||||
.offset(offset)
|
||||
.orderBy("firstName", "asc");
|
||||
|
||||
if (limit) {
|
||||
query = query.limit(limit);
|
||||
void query.limit(limit);
|
||||
}
|
||||
|
||||
if (username) {
|
||||
query = query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
|
||||
if (search) {
|
||||
void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike '%${search}%'`);
|
||||
} else if (username) {
|
||||
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
|
||||
}
|
||||
|
||||
const members = await query;
|
||||
|
||||
return members.map(
|
||||
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
|
||||
id: userId,
|
||||
email,
|
||||
username: memberUsername,
|
||||
firstName,
|
||||
lastName,
|
||||
isPartOfGroup: !!memberGroupId
|
||||
})
|
||||
);
|
||||
return {
|
||||
members: members.map(
|
||||
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({
|
||||
id: userId,
|
||||
email,
|
||||
username: memberUsername,
|
||||
firstName,
|
||||
lastName,
|
||||
isPartOfGroup: !!memberGroupId
|
||||
})
|
||||
),
|
||||
// @ts-expect-error col select is raw and not strongly typed
|
||||
totalCount: Number(members?.[0]?.total_count ?? 0)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all org members" });
|
||||
}
|
||||
|
@ -221,7 +221,8 @@ export const groupServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
search
|
||||
}: TListGroupUsersDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
@ -244,17 +245,16 @@ export const groupServiceFactory = ({
|
||||
message: `Failed to find group with ID ${id}`
|
||||
});
|
||||
|
||||
const users = await groupDAL.findAllGroupPossibleMembers({
|
||||
const { members, totalCount } = await groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id,
|
||||
offset,
|
||||
limit,
|
||||
username
|
||||
username,
|
||||
search
|
||||
});
|
||||
|
||||
const count = await orgDAL.countAllOrgMembers(group.orgId);
|
||||
|
||||
return { users, totalCount: count };
|
||||
return { users: members, totalCount };
|
||||
};
|
||||
|
||||
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
|
||||
|
@ -38,6 +38,7 @@ export type TListGroupUsersDTO = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
username?: string;
|
||||
search?: string;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TAddUserToGroupDTO = {
|
||||
|
@ -1,14 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
SecretKeyEncoding,
|
||||
TableName,
|
||||
TLdapConfigsUpdate,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
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 { TOrgBotDALFactory } from "@app/services/org/org-bot-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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
@ -444,11 +438,14 @@ export const ldapConfigServiceFactory = ({
|
||||
{ tx }
|
||||
);
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId: userAlias.userId,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
role,
|
||||
roleId,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
},
|
||||
@ -529,12 +526,15 @@ export const ldapConfigServiceFactory = ({
|
||||
);
|
||||
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: newUser.id,
|
||||
inviteEmail: email,
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
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 { TOrgBotDALFactory } from "@app/services/org/org-bot-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 { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
@ -187,12 +188,15 @@ export const oidcConfigServiceFactory = ({
|
||||
{ tx }
|
||||
);
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: userAlias.userId,
|
||||
inviteEmail: email,
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
@ -261,12 +265,15 @@ export const oidcConfigServiceFactory = ({
|
||||
);
|
||||
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: newUser.id,
|
||||
inviteEmail: email,
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
|
@ -2,7 +2,6 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
SecretKeyEncoding,
|
||||
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 { TOrgBotDALFactory } from "@app/services/org/org-bot-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 { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
@ -369,12 +369,15 @@ export const samlConfigServiceFactory = ({
|
||||
{ tx }
|
||||
);
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: userAlias.userId,
|
||||
inviteEmail: email,
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
@ -472,12 +475,15 @@ export const samlConfigServiceFactory = ({
|
||||
);
|
||||
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
|
||||
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: newUser.id,
|
||||
inviteEmail: email,
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
||||
import jwt from "jsonwebtoken";
|
||||
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 { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
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 { TOrgPermission } from "@app/lib/types";
|
||||
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 { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
@ -70,7 +72,10 @@ type TScimServiceFactoryDep = {
|
||||
| "transaction"
|
||||
| "updateMembershipById"
|
||||
>;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
|
||||
orgMembershipDAL: Pick<
|
||||
TOrgMembershipDALFactory,
|
||||
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
|
||||
>;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||
groupDAL: Pick<
|
||||
@ -101,6 +106,7 @@ type TScimServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
|
||||
};
|
||||
|
||||
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||
@ -121,7 +127,8 @@ export const scimServiceFactory = ({
|
||||
projectBotDAL,
|
||||
permissionService,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
}: TScimServiceFactoryDep) => {
|
||||
const createScimToken = async ({
|
||||
actor,
|
||||
@ -318,12 +325,15 @@ export const scimServiceFactory = ({
|
||||
);
|
||||
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
|
||||
|
||||
orgMembership = await orgMembershipDAL.create(
|
||||
{
|
||||
userId: userAlias.userId,
|
||||
inviteEmail: email,
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
@ -391,12 +401,15 @@ export const scimServiceFactory = ({
|
||||
orgMembership = foundOrgMembership;
|
||||
|
||||
if (!orgMembership) {
|
||||
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
|
||||
|
||||
orgMembership = await orgMembershipDAL.create(
|
||||
{
|
||||
userId: user.id,
|
||||
inviteEmail: email,
|
||||
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
|
||||
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 plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
@ -738,6 +788,8 @@ export const scimServiceFactory = ({
|
||||
tx
|
||||
});
|
||||
|
||||
await $syncNewMembersRoles(group, members);
|
||||
|
||||
return { group, newMembers };
|
||||
}
|
||||
|
||||
@ -782,10 +834,12 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const users = await groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id
|
||||
});
|
||||
const users = await groupDAL
|
||||
.findAllGroupPossibleMembers({
|
||||
orgId: group.orgId,
|
||||
groupId: group.id
|
||||
})
|
||||
.then((g) => g.members);
|
||||
|
||||
const orgMemberships = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
@ -813,22 +867,41 @@ export const scimServiceFactory = ({
|
||||
orgId: string,
|
||||
{ displayName, members = [] }: { displayName: string; members: { value: string }[] }
|
||||
) => {
|
||||
const updatedGroup = await groupDAL.transaction(async (tx) => {
|
||||
const [group] = await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: displayName
|
||||
}
|
||||
);
|
||||
let group = await groupDAL.findOne({
|
||||
id: groupId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
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
|
||||
@ -885,6 +958,8 @@ export const scimServiceFactory = ({
|
||||
return group;
|
||||
});
|
||||
|
||||
await $syncNewMembersRoles(group, members);
|
||||
|
||||
return updatedGroup;
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
@ -61,7 +61,7 @@ export const secretScanningQueueFactory = ({
|
||||
const getOrgAdminEmails = async (organizationId: string) => {
|
||||
// get emails of admins
|
||||
const adminsOfWork = await orgMemberDAL.findMembership({
|
||||
orgId: organizationId,
|
||||
[`${TableName.Organization}.id` as string]: organizationId,
|
||||
role: OrgMembershipRole.Admin
|
||||
});
|
||||
return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string);
|
||||
|
@ -90,7 +90,7 @@ export const secretScanningServiceFactory = ({
|
||||
const {
|
||||
data: { repositories }
|
||||
} = await octokit.apps.listReposAccessibleToInstallation();
|
||||
if (!appCfg.DISABLE_SECRET_SCANNING) {
|
||||
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
|
||||
await Promise.all(
|
||||
repositories.map(({ id, full_name }) =>
|
||||
secretScanningQueue.startFullRepoScan({
|
||||
@ -164,7 +164,7 @@ export const secretScanningServiceFactory = ({
|
||||
});
|
||||
if (!installationLink) return;
|
||||
|
||||
if (!appCfg.DISABLE_SECRET_SCANNING) {
|
||||
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
|
||||
await secretScanningQueue.startPushEventScan({
|
||||
commits,
|
||||
pusher: { name: pusher.name, email: pusher.email },
|
||||
|
@ -240,7 +240,8 @@ export const secretSnapshotServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
const snapshotSecrets = await snapshotSecretV2BridgeDAL.insertMany(
|
||||
|
||||
const snapshotSecrets = await snapshotSecretV2BridgeDAL.batchInsert(
|
||||
secretVersions.map(({ id }) => ({
|
||||
secretVersionId: id,
|
||||
envId: folder.environment.envId,
|
||||
@ -248,7 +249,8 @@ export const secretSnapshotServiceFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
const snapshotFolders = await snapshotFolderDAL.insertMany(
|
||||
|
||||
const snapshotFolders = await snapshotFolderDAL.batchInsert(
|
||||
folderVersions.map(({ id }) => ({
|
||||
folderVersionId: id,
|
||||
envId: folder.environment.envId,
|
||||
|
@ -18,7 +18,8 @@ export const GROUPS = {
|
||||
id: "The id of the group to list users for",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
|
||||
limit: "The number of users to return.",
|
||||
username: "The username to search for."
|
||||
username: "The username to search for.",
|
||||
search: "The text string that user email or name will be filtered by."
|
||||
},
|
||||
ADD_USER: {
|
||||
id: "The id of the group to add the user to.",
|
||||
|
@ -34,6 +34,12 @@ const envSchema = z
|
||||
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}`
|
||||
),
|
||||
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),
|
||||
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
|
||||
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
|
||||
@ -111,9 +117,16 @@ const envSchema = z
|
||||
// gcp secret manager
|
||||
CLIENT_ID_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_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
|
||||
CLIENT_ID_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_GIT_APP_ID: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()),
|
||||
// LICENSE
|
||||
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
|
||||
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_PRIVATE_KEY) &&
|
||||
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>>;
|
||||
|
@ -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 {
|
||||
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 {
|
||||
name: string;
|
||||
|
||||
|
@ -70,3 +70,14 @@ export const objectify = <T, Key extends string | number | symbol, Value = T>(
|
||||
{} 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;
|
||||
};
|
||||
|
@ -8,12 +8,14 @@ const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
|
||||
return filter;
|
||||
};
|
||||
|
||||
export const generateKnexQueryFromScim = (
|
||||
const processDynamicQuery = (
|
||||
rootQuery: Knex.QueryBuilder,
|
||||
rootScimFilter: string,
|
||||
getAttributeField: (attr: string) => string | null
|
||||
scimRootFilterAst: Filter,
|
||||
getAttributeField: (attr: string) => string | null,
|
||||
depth = 0
|
||||
) => {
|
||||
const scimRootFilterAst = parse(rootScimFilter);
|
||||
if (depth > 20) return;
|
||||
|
||||
const stack = [
|
||||
{
|
||||
scimFilterAst: scimRootFilterAst,
|
||||
@ -75,42 +77,35 @@ export const generateKnexQueryFromScim = (
|
||||
break;
|
||||
}
|
||||
case "and": {
|
||||
void query.andWhere((subQueryBuilder) => {
|
||||
scimFilterAst.filters.forEach((el) => {
|
||||
stack.push({
|
||||
query: subQueryBuilder,
|
||||
scimFilterAst: el
|
||||
});
|
||||
scimFilterAst.filters.forEach((el) => {
|
||||
void query.andWhere((subQueryBuilder) => {
|
||||
processDynamicQuery(subQueryBuilder, el, getAttributeField, depth + 1);
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "or": {
|
||||
void query.orWhere((subQueryBuilder) => {
|
||||
scimFilterAst.filters.forEach((el) => {
|
||||
stack.push({
|
||||
query: subQueryBuilder,
|
||||
scimFilterAst: el
|
||||
});
|
||||
scimFilterAst.filters.forEach((el) => {
|
||||
void query.orWhere((subQueryBuilder) => {
|
||||
processDynamicQuery(subQueryBuilder, el, getAttributeField, depth + 1);
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "not": {
|
||||
void query.whereNot((subQueryBuilder) => {
|
||||
stack.push({
|
||||
query: subQueryBuilder,
|
||||
scimFilterAst: scimFilterAst.filter
|
||||
});
|
||||
processDynamicQuery(subQueryBuilder, scimFilterAst.filter, getAttributeField, depth + 1);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "[]": {
|
||||
void query.whereNot((subQueryBuilder) => {
|
||||
stack.push({
|
||||
query: subQueryBuilder,
|
||||
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter)
|
||||
});
|
||||
void query.where((subQueryBuilder) => {
|
||||
processDynamicQuery(
|
||||
subQueryBuilder,
|
||||
appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter),
|
||||
getAttributeField,
|
||||
depth + 1
|
||||
);
|
||||
});
|
||||
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);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
|
||||
import { initDbConnection } from "./db";
|
||||
import { initAuditLogDbConnection, initDbConnection } from "./db";
|
||||
import { keyStoreFactory } from "./keystore/keystore";
|
||||
import { formatSmtpConfig, initEnvConfig, IS_PACKAGED } from "./lib/config/env";
|
||||
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.
|
||||
// Run the migrations and exit the process after completion.
|
||||
if (IS_PACKAGED && isMigrationMode()) {
|
||||
@ -46,7 +53,7 @@ const run = async () => {
|
||||
const queue = queueServiceFactory(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 });
|
||||
|
||||
// eslint-disable-next-line
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
|
||||
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 {
|
||||
TScanFullRepoEventPayload,
|
||||
@ -32,7 +32,8 @@ export enum QueueName {
|
||||
SecretReplication = "secret-replication",
|
||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||
ProjectV3Migration = "project-v3-migration",
|
||||
AccessTokenStatusUpdate = "access-token-status-update"
|
||||
AccessTokenStatusUpdate = "access-token-status-update",
|
||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@ -56,7 +57,8 @@ export enum QueueJobs {
|
||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||
ProjectV3Migration = "project-v3-migration",
|
||||
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 = {
|
||||
@ -166,6 +168,19 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.ProjectV3Migration;
|
||||
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>;
|
||||
|
@ -30,6 +30,7 @@ import { fastifySwagger } from "./plugins/swagger";
|
||||
import { registerRoutes } from "./routes";
|
||||
|
||||
type TMain = {
|
||||
auditLogDb?: Knex;
|
||||
db: Knex;
|
||||
smtp: TSmtpService;
|
||||
logger?: Logger;
|
||||
@ -38,7 +39,7 @@ type TMain = {
|
||||
};
|
||||
|
||||
// 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 server = fastify({
|
||||
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(registerRoutes, { smtp, queue, db, keyStore });
|
||||
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore });
|
||||
|
||||
if (appCfg.isProductionMode) {
|
||||
await server.register(registerExternalNextjs, {
|
||||
|
@ -2,6 +2,7 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-lim
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { RateLimitError } from "@app/lib/errors";
|
||||
|
||||
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
const appCfg = getConfig();
|
||||
@ -10,6 +11,11 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
: null;
|
||||
|
||||
return {
|
||||
errorResponseBuilder: (_, context) => {
|
||||
throw new RateLimitError({
|
||||
message: `Rate limit exceeded. Please try again in ${context.after}`
|
||||
});
|
||||
},
|
||||
timeWindow: 60 * 1000,
|
||||
max: 600,
|
||||
redis,
|
||||
|
@ -3,9 +3,12 @@ import fp from "fastify-plugin";
|
||||
|
||||
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) => {
|
||||
server.addHook("onRoute", (routeOptions) => {
|
||||
if (routeOptions.schema && routeOptions.schema.response) {
|
||||
if (routeOptions.schema && routeOptions.schema.response && !isScimRoutes(routeOptions.path)) {
|
||||
routeOptions.schema.response = {
|
||||
...DefaultResponseErrorsSchema,
|
||||
...routeOptions.schema.response
|
||||
|
@ -7,8 +7,10 @@ import {
|
||||
BadRequestError,
|
||||
DatabaseError,
|
||||
ForbiddenRequestError,
|
||||
GatewayTimeoutError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
ScimRequestError,
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
@ -25,7 +27,9 @@ enum HttpStatusCodes {
|
||||
Unauthorized = 401,
|
||||
Forbidden = 403,
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
InternalServerError = 500
|
||||
InternalServerError = 500,
|
||||
GatewayTimeout = 504,
|
||||
TooManyRequests = 429
|
||||
}
|
||||
|
||||
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
|
||||
@ -47,6 +51,10 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
void res
|
||||
.status(HttpStatusCodes.InternalServerError)
|
||||
.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) {
|
||||
void res
|
||||
.status(HttpStatusCodes.Unauthorized)
|
||||
@ -63,6 +71,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
message: error.message,
|
||||
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) {
|
||||
void res.status(error.status).send({
|
||||
schemas: error.schemas,
|
||||
@ -91,7 +105,11 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
message
|
||||
});
|
||||
} else {
|
||||
void res.send(error);
|
||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||
statusCode: HttpStatusCodes.InternalServerError,
|
||||
error: "InternalServerError",
|
||||
message: "Something went wrong"
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -97,6 +97,9 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat
|
||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-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 { groupProjectDALFactory } from "@app/services/group-project/group-project-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 (
|
||||
server: FastifyZodProvider,
|
||||
{
|
||||
auditLogDb,
|
||||
db,
|
||||
smtp: smtpService,
|
||||
queue: queueService,
|
||||
keyStore
|
||||
}: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
|
||||
}: { auditLogDb?: Knex; db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.DISABLE_SECRET_SCANNING) {
|
||||
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
|
||||
}
|
||||
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
|
||||
|
||||
// db layers
|
||||
const userDAL = userDALFactory(db);
|
||||
@ -283,7 +285,7 @@ export const registerRoutes = async (
|
||||
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
|
||||
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
|
||||
|
||||
const auditLogDAL = auditLogDALFactory(db);
|
||||
const auditLogDAL = auditLogDALFactory(auditLogDb ?? db);
|
||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||
const trustedIpDAL = trustedIpDALFactory(db);
|
||||
const telemetryDAL = telemetryDALFactory(db);
|
||||
@ -334,6 +336,8 @@ export const registerRoutes = async (
|
||||
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
||||
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
||||
|
||||
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@ -440,7 +444,8 @@ export const registerRoutes = async (
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
smtpService,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
|
||||
const ldapService = ldapConfigServiceFactory({
|
||||
@ -491,6 +496,9 @@ export const registerRoutes = async (
|
||||
authDAL,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
||||
|
||||
const orgService = orgServiceFactory({
|
||||
userAliasDAL,
|
||||
identityMetadataDAL,
|
||||
@ -513,7 +521,8 @@ export const registerRoutes = async (
|
||||
userDAL,
|
||||
groupDAL,
|
||||
orgBotDAL,
|
||||
oidcConfigDAL
|
||||
oidcConfigDAL,
|
||||
projectBotService
|
||||
});
|
||||
const signupService = authSignupServiceFactory({
|
||||
tokenService,
|
||||
@ -531,7 +540,12 @@ export const registerRoutes = async (
|
||||
orgService,
|
||||
licenseService
|
||||
});
|
||||
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL });
|
||||
const orgRoleService = orgRoleServiceFactory({
|
||||
permissionService,
|
||||
orgRoleDAL,
|
||||
orgDAL,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
const superAdminService = superAdminServiceFactory({
|
||||
userDAL,
|
||||
authService: loginService,
|
||||
@ -572,7 +586,6 @@ export const registerRoutes = async (
|
||||
secretScanningDAL,
|
||||
secretScanningQueue
|
||||
});
|
||||
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL });
|
||||
|
||||
const projectMembershipService = projectMembershipServiceFactory({
|
||||
projectMembershipDAL,
|
||||
@ -836,7 +849,10 @@ export const registerRoutes = async (
|
||||
integrationAuthDAL,
|
||||
snapshotDAL,
|
||||
snapshotSecretV2BridgeDAL,
|
||||
secretApprovalRequestDAL
|
||||
secretApprovalRequestDAL,
|
||||
projectKeyDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
orgService
|
||||
});
|
||||
const secretImportService = secretImportServiceFactory({
|
||||
licenseService,
|
||||
@ -1201,12 +1217,33 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const migrationService = externalMigrationServiceFactory({
|
||||
projectService,
|
||||
orgService,
|
||||
const externalMigrationQueue = externalMigrationQueueFactory({
|
||||
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,
|
||||
secretService
|
||||
licenseService,
|
||||
orgRoleDAL,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
@ -1294,7 +1331,8 @@ export const registerRoutes = async (
|
||||
orgAdmin: orgAdminService,
|
||||
slack: slackService,
|
||||
workflowIntegration: workflowIntegrationService,
|
||||
migration: migrationService
|
||||
migration: migrationService,
|
||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@ -109,7 +109,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
id: true
|
||||
id: true,
|
||||
superAdmin: true
|
||||
}).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;
|
||||
}
|
||||
});
|
||||
};
|
@ -7,6 +7,7 @@ import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-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(registerDashboardRouter, { prefix: "/dashboard" });
|
||||
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(),
|
||||
code: z.string().trim(),
|
||||
integration: z.string().trim(),
|
||||
installationId: z.string().trim().optional(),
|
||||
url: z.string().trim().url().optional()
|
||||
}),
|
||||
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({
|
||||
method: "GET",
|
||||
url: "/:integrationAuthId/github/envs",
|
||||
|
@ -52,7 +52,13 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
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: {
|
||||
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 {
|
||||
@ -11,13 +12,13 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
||||
|
||||
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
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({
|
||||
method: "GET",
|
||||
url: "/audit-logs",
|
||||
@ -125,12 +155,6 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.optional(),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
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) => {
|
||||
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({
|
||||
filter: {
|
||||
...req.query,
|
||||
@ -168,6 +187,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type
|
||||
});
|
||||
|
||||
return { auditLogs };
|
||||
}
|
||||
});
|
||||
@ -191,7 +211,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
id: true,
|
||||
superAdmin: true
|
||||
}).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")
|
||||
.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: {
|
||||
200: z.object({
|
||||
|
@ -65,7 +65,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
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,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
|
@ -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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const MB25_IN_BYTES = 26214400;
|
||||
|
||||
export const registerExternalMigrationRouter = async (server: FastifyZodProvider) => {
|
||||
await server.register(fastifyMultipart);
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
bodyLimit: MB25_IN_BYTES,
|
||||
url: "/env-key",
|
||||
config: {
|
||||
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]),
|
||||
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({
|
||||
decryptionKey: req.body.decryptionKey,
|
||||
encryptedJson: req.body.encryptedJson,
|
||||
decryptionKey,
|
||||
encryptedJson: parsedJsonFile,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
|
46
backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-dal.ts
Normal file
46
backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-dal.ts
Normal file
@ -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 };
|
||||
};
|
67
backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts
Normal file
67
backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts
Normal file
@ -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;
|
||||
};
|
78
backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts
Normal file
78
backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts
Normal file
@ -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
|
||||
};
|
||||
};
|
8
backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-types.ts
Normal file
8
backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-types.ts
Normal file
@ -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 tweetnaclUtil from "tweetnacl-util";
|
||||
|
||||
import { OrgMembershipRole, ProjectMembershipRole, SecretType } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { SecretType } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { chunkArray } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
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 { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
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";
|
||||
|
||||
export type TImportDataIntoInfisicalDTO = {
|
||||
projectService: TProjectServiceFactory;
|
||||
orgService: TOrgServiceFactory;
|
||||
projectEnvService: TProjectEnvServiceFactory;
|
||||
secretService: TSecretServiceFactory;
|
||||
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">;
|
||||
|
||||
input: TImportInfisicalDataCreate;
|
||||
};
|
||||
@ -46,13 +65,13 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
const parsedJson: TEnvKeyExportJSON = JSON.parse(decryptedJson) as TEnvKeyExportJSON;
|
||||
|
||||
const infisicalImportData: InfisicalImportData = {
|
||||
projects: new Map<string, { name: string; id: string }>(),
|
||||
environments: new Map<string, { name: string; id: string; projectId: string }>(),
|
||||
secrets: new Map<string, { name: string; id: string; projectId: string; environmentId: string; value: string }>()
|
||||
projects: [],
|
||||
environments: [],
|
||||
secrets: []
|
||||
};
|
||||
|
||||
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
|
||||
@ -63,7 +82,7 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
|
||||
// environments
|
||||
for (const env of parsedJson.baseEnvironments) {
|
||||
infisicalImportData.environments?.set(env.id, {
|
||||
infisicalImportData.environments.push({
|
||||
id: env.id,
|
||||
name: envTemplates.get(env.environmentRoleId)!,
|
||||
projectId: env.envParentId
|
||||
@ -75,9 +94,8 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
if (!env.includes("|")) {
|
||||
const envData = parsedJson.envs[env];
|
||||
for (const secret of Object.keys(envData.variables)) {
|
||||
const id = randomUUID();
|
||||
infisicalImportData.secrets?.set(id, {
|
||||
id,
|
||||
infisicalImportData.secrets.push({
|
||||
id: randomUUID(),
|
||||
name: secret,
|
||||
environmentId: env,
|
||||
value: envData.variables[secret].val
|
||||
@ -91,9 +109,14 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise<Infisica
|
||||
|
||||
export const importDataIntoInfisicalFn = async ({
|
||||
projectService,
|
||||
orgService,
|
||||
projectEnvService,
|
||||
secretService,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
secretDAL,
|
||||
kmsService,
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL,
|
||||
folderDAL,
|
||||
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
|
||||
}: TImportDataIntoInfisicalDTO) => {
|
||||
// Import data to infisical
|
||||
@ -103,95 +126,145 @@ export const importDataIntoInfisicalFn = async ({
|
||||
|
||||
const originalToNewProjectId = new Map<string, string>();
|
||||
const originalToNewEnvironmentId = new Map<string, string>();
|
||||
const projectsNotImported: string[] = [];
|
||||
|
||||
for await (const [id, project] of data.projects) {
|
||||
const newProject = await projectService
|
||||
.createProject({
|
||||
actor,
|
||||
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({
|
||||
await projectDAL.transaction(async (tx) => {
|
||||
for await (const project of data.projects) {
|
||||
const newProject = await projectService
|
||||
.createProject({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
name: environment.name,
|
||||
projectId: originalToNewProjectId.get(environment.projectId)!,
|
||||
slug: slugify(`${environment.name}-${alphaNumericNanoId(4)}`)
|
||||
workspaceName: project.name,
|
||||
createDefaultEnvs: false,
|
||||
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) {
|
||||
logger.error(`Failed to import environment: [name:${environment.name}] [id:${id}]`);
|
||||
// Import environments
|
||||
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({
|
||||
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) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to import environment: ${environment.name}]`,
|
||||
name: "EnvKeyMigrationImportEnvironment"
|
||||
|
||||
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
||||
const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
|
||||
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
||||
|
||||
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
|
||||
if (data.secrets) {
|
||||
for await (const [id, secret] of data.secrets) {
|
||||
const dataProjectId = data.environments?.get(secret.environmentId)?.projectId;
|
||||
if (!dataProjectId) {
|
||||
throw new BadRequestError({ message: `Failed to import secret "${secret.name}", project not found` });
|
||||
}
|
||||
const projectId = originalToNewProjectId.get(dataProjectId);
|
||||
const newSecret = await secretService.createSecretRaw({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
environment: originalToNewEnvironmentId.get(secret.environmentId)!,
|
||||
actorAuthMethod,
|
||||
projectId: projectId!,
|
||||
secretPath: "/",
|
||||
secretName: secret.name,
|
||||
type: SecretType.Shared,
|
||||
secretValue: secret.value
|
||||
});
|
||||
if (!newSecret) {
|
||||
throw new BadRequestError({ message: `Failed to import secret: [name:${secret.name}] [id:${id}]` });
|
||||
// for each of the mappedEnvironmentId
|
||||
for await (const [envId, secrets] of mappedToEnvironmentId) {
|
||||
const environment = data.environments.find((env) => env.id === envId);
|
||||
const projectId = originalToNewProjectId.get(environment?.projectId as string)!;
|
||||
|
||||
if (!projectId) {
|
||||
throw new BadRequestError({ message: `Failed to import secret, project not found` });
|
||||
}
|
||||
|
||||
const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
|
||||
{
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const envSlug = originalToNewEnvironmentId.get(envId)!;
|
||||
const folder = await folderDAL.findBySecretPath(projectId, envSlug, "/", tx);
|
||||
if (!folder)
|
||||
throw new NotFoundError({
|
||||
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectServiceFactory } from "../project/project-service";
|
||||
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||
import { TSecretServiceFactory } from "../secret/secret-service";
|
||||
import { decryptEnvKeyDataFn, importDataIntoInfisicalFn, parseEnvKeyDataFn } from "./external-migration-fns";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { decryptEnvKeyDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
|
||||
import { TExternalMigrationQueueFactory } from "./external-migration-queue";
|
||||
import { TImportEnvKeyDataCreate } from "./external-migration-types";
|
||||
|
||||
type TExternalMigrationServiceFactoryDep = {
|
||||
projectService: TProjectServiceFactory;
|
||||
orgService: TOrgServiceFactory;
|
||||
projectEnvService: TProjectEnvServiceFactory;
|
||||
secretService: TSecretServiceFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
externalMigrationQueue: TExternalMigrationQueueFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
|
||||
|
||||
export const externalMigrationServiceFactory = ({
|
||||
projectService,
|
||||
orgService,
|
||||
projectEnvService,
|
||||
permissionService,
|
||||
secretService
|
||||
externalMigrationQueue,
|
||||
userDAL
|
||||
}: TExternalMigrationServiceFactoryDep) => {
|
||||
const importEnvKeyData = async ({
|
||||
decryptionKey,
|
||||
@ -41,21 +36,28 @@ export const externalMigrationServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (membership.role !== OrgMembershipRole.Admin) {
|
||||
throw new ForbiddenRequestError({ message: "Only admins can import data" });
|
||||
}
|
||||
|
||||
const user = await userDAL.findById(actorId);
|
||||
const json = await decryptEnvKeyDataFn(decryptionKey, encryptedJson);
|
||||
const envKeyData = await parseEnvKeyDataFn(json);
|
||||
const response = await importDataIntoInfisicalFn({
|
||||
input: { data: envKeyData, actor, actorId, actorOrgId, actorAuthMethod },
|
||||
projectService,
|
||||
orgService,
|
||||
projectEnvService,
|
||||
secretService
|
||||
|
||||
const stringifiedJson = JSON.stringify({
|
||||
data: envKeyData,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
});
|
||||
|
||||
const encrypted = infisicalSymmetricEncypt(stringifiedJson);
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorEmail: user.email!,
|
||||
data: encrypted
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -1,26 +1,9 @@
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
|
||||
export type InfisicalImportData = {
|
||||
projects: Map<string, { name: string; id: string }>;
|
||||
|
||||
environments?: Map<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
projectId: string;
|
||||
}
|
||||
>;
|
||||
|
||||
secrets?: Map<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
environmentId: string;
|
||||
value: string;
|
||||
}
|
||||
>;
|
||||
projects: Array<{ name: string; id: string }>;
|
||||
environments: Array<{ name: string; id: string; projectId: string }>;
|
||||
secrets: Array<{ name: string; id: string; environmentId: string; value: string }>;
|
||||
};
|
||||
|
||||
export type TImportEnvKeyDataCreate = {
|
||||
@ -104,3 +87,7 @@ export type TEnvKeyExportJSON = {
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export enum ExternalPlatforms {
|
||||
EnvKey = "EnvKey"
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
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 { NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||
import { Integrations, IntegrationUrls } from "./integration-list";
|
||||
|
||||
// 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
|
||||
*/
|
||||
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||
const getAppsGithub = async ({
|
||||
accessToken,
|
||||
authMetadata
|
||||
}: {
|
||||
accessToken: string;
|
||||
authMetadata?: TIntegrationAuthMetadata;
|
||||
}) => {
|
||||
interface GitHubApp {
|
||||
id: 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
|
||||
const repos = (await new Octokit({
|
||||
auth: accessToken
|
||||
@ -1056,6 +1089,7 @@ const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: strin
|
||||
|
||||
export const getApps = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
accessToken,
|
||||
accessId,
|
||||
teamId,
|
||||
@ -1066,6 +1100,7 @@ export const getApps = async ({
|
||||
integration: string;
|
||||
accessToken: string;
|
||||
accessId?: string;
|
||||
integrationAuth: TIntegrationAuths;
|
||||
teamId?: string | null;
|
||||
azureDevOpsOrgName?: string | null;
|
||||
workspaceSlug?: string;
|
||||
@ -1099,7 +1134,8 @@ export const getApps = async ({
|
||||
|
||||
case Integrations.GITHUB:
|
||||
return getAppsGithub({
|
||||
accessToken
|
||||
accessToken,
|
||||
authMetadata: IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {})
|
||||
});
|
||||
|
||||
case Integrations.GITLAB:
|
||||
|
@ -3,7 +3,7 @@ import { Knex } from "knex";
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TIntegrationAuths, TIntegrationAuthsUpdate } from "@app/db/schemas";
|
||||
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>;
|
||||
|
||||
@ -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 {
|
||||
...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 { createAppAuth } from "@octokit/auth-app";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import AWS from "aws-sdk";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
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 { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
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 { TKmsServiceFactory } from "../kms/kms-service";
|
||||
@ -16,11 +18,13 @@ import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { getApps } from "./integration-app-list";
|
||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||
import {
|
||||
TBitbucketWorkspace,
|
||||
TChecklyGroups,
|
||||
TDeleteIntegrationAuthByIdDTO,
|
||||
TDeleteIntegrationAuthsDTO,
|
||||
TDuplicateGithubIntegrationAuthDTO,
|
||||
TGetIntegrationAuthDTO,
|
||||
TGetIntegrationAuthTeamCityBuildConfigDTO,
|
||||
THerokuPipelineCoupling,
|
||||
@ -86,6 +90,24 @@ export const integrationAuthServiceFactory = ({
|
||||
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 integrationAuth = await integrationAuthDAL.findById(id);
|
||||
if (!integrationAuth) throw new NotFoundError({ message: "Failed to find integration" });
|
||||
@ -109,7 +131,8 @@ export const integrationAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
integration,
|
||||
url,
|
||||
code
|
||||
code,
|
||||
installationId
|
||||
}: TOauthExchangeDTO) => {
|
||||
if (!Object.values(Integrations).includes(integration as Integrations))
|
||||
throw new BadRequestError({ message: "Invalid integration" });
|
||||
@ -123,7 +146,7 @@ export const integrationAuthServiceFactory = ({
|
||||
);
|
||||
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 = {
|
||||
projectId,
|
||||
integration,
|
||||
@ -141,6 +164,16 @@ export const integrationAuthServiceFactory = ({
|
||||
updateDoc.metadata = {
|
||||
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);
|
||||
@ -176,12 +209,23 @@ export const integrationAuthServiceFactory = ({
|
||||
updateDoc.accessCiphertext = accessEncToken.ciphertext;
|
||||
}
|
||||
}
|
||||
|
||||
return integrationAuthDAL.transaction(async (tx) => {
|
||||
const doc = await integrationAuthDAL.findOne({ projectId, integration }, tx);
|
||||
if (!doc) {
|
||||
const integrationAuths = await integrationAuthDAL.find({ projectId, integration }, { tx });
|
||||
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.updateById(doc.id, updateDoc, tx);
|
||||
|
||||
return integrationAuthDAL.updateById(existingIntegrationAuth.id, updateDoc, tx);
|
||||
});
|
||||
};
|
||||
|
||||
@ -334,6 +378,13 @@ export const integrationAuthServiceFactory = ({
|
||||
) {
|
||||
return { accessToken: "", accessId: "" };
|
||||
}
|
||||
if (
|
||||
integrationAuth.integration === Integrations.GITHUB &&
|
||||
IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}).installationId
|
||||
) {
|
||||
return { accessToken: "", accessId: "" };
|
||||
}
|
||||
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
@ -460,6 +511,7 @@ export const integrationAuthServiceFactory = ({
|
||||
const { accessToken, accessId } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||
const apps = await getApps({
|
||||
integration: integrationAuth.integration,
|
||||
integrationAuth,
|
||||
accessToken,
|
||||
accessId,
|
||||
teamId,
|
||||
@ -575,6 +627,7 @@ export const integrationAuthServiceFactory = ({
|
||||
};
|
||||
|
||||
const getGithubOrgs = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TIntegrationAuthGithubOrgsDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||
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);
|
||||
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
|
||||
});
|
||||
|
||||
@ -598,7 +686,9 @@ export const integrationAuthServiceFactory = ({
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
});
|
||||
if (!data) return [];
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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 { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: accessToken
|
||||
});
|
||||
let octokit: Octokit;
|
||||
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 {
|
||||
data: { environments }
|
||||
@ -1315,8 +1420,58 @@ export const integrationAuthServiceFactory = ({
|
||||
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 {
|
||||
listIntegrationAuthByProjectId,
|
||||
listOrgIntegrationAuth,
|
||||
getIntegrationOptions,
|
||||
getIntegrationAuth,
|
||||
oauthExchange,
|
||||
@ -1343,6 +1498,7 @@ export const integrationAuthServiceFactory = ({
|
||||
getNorthFlankSecretGroups,
|
||||
getTeamcityBuildConfigs,
|
||||
getBitbucketWorkspaces,
|
||||
getIntegrationAccessToken
|
||||
getIntegrationAccessToken,
|
||||
duplicateIntegrationAuth
|
||||
};
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ export type TOauthExchangeDTO = {
|
||||
integration: string;
|
||||
code: string;
|
||||
url?: string;
|
||||
installationId?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TSaveIntegrationAccessTokenDTO = {
|
||||
@ -107,6 +108,10 @@ export type TDeleteIntegrationAuthByIdDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDuplicateGithubIntegrationAuthDTO = {
|
||||
id: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
|
||||
id: 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 { Octokit } from "@octokit/rest";
|
||||
|
||||
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
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 { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||
import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
|
||||
import { Integrations } from "./integration-list";
|
||||
|
||||
@ -154,10 +158,12 @@ const getIntegrationSecretsV1 = async (
|
||||
|
||||
export const deleteGithubSecrets = async ({
|
||||
integration,
|
||||
authMetadata,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: Omit<TIntegrations, "envId">;
|
||||
authMetadata: TIntegrationAuthMetadata;
|
||||
secrets: Record<string, boolean>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
@ -170,9 +176,23 @@ export const deleteGithubSecrets = async ({
|
||||
}
|
||||
|
||||
const OctokitWithRetry = Octokit.plugin(retry);
|
||||
const octokit = new OctokitWithRetry({
|
||||
auth: accessToken
|
||||
});
|
||||
let octokit: Octokit;
|
||||
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 {
|
||||
Repo = "github-repo",
|
||||
@ -192,6 +212,7 @@ export const deleteGithubSecrets = async ({
|
||||
break;
|
||||
}
|
||||
case GithubScope.Env: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
encryptedGithubSecrets = (
|
||||
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
|
||||
repository_id: Number(integration.appId),
|
||||
@ -346,6 +367,7 @@ export const deleteIntegrationSecrets = async ({
|
||||
case Integrations.GITHUB: {
|
||||
await deleteGithubSecrets({
|
||||
integration,
|
||||
authMetadata: IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {}),
|
||||
accessToken,
|
||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
|
||||
});
|
||||
|
@ -96,7 +96,9 @@ export enum IntegrationUrls {
|
||||
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
|
||||
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||
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 () => {
|
||||
@ -138,6 +140,7 @@ export const getIntegrationOptions = async () => {
|
||||
isAvailable: true,
|
||||
type: "oauth",
|
||||
clientId: appCfg.CLIENT_ID_GITHUB,
|
||||
clientSlug: appCfg.CLIENT_SLUG_GITHUB_APP,
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
import {
|
||||
CreateSecretCommand,
|
||||
DeleteSecretCommand,
|
||||
DescribeSecretCommand,
|
||||
GetSecretValueCommand,
|
||||
ResourceNotFoundException,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
UpdateSecretCommand
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import AWS, { AWSError } from "aws-sdk";
|
||||
import { AxiosError } from "axios";
|
||||
@ -35,6 +37,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
|
||||
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
@ -727,7 +730,7 @@ const syncSecretsAWSParameterStore = async ({
|
||||
awsParameterStoreSecretsObj[key].KeyId !== metadata.kmsKeyId;
|
||||
|
||||
// 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
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
@ -788,7 +791,7 @@ const syncSecretsAWSParameterStore = async ({
|
||||
logger.info(
|
||||
`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(
|
||||
`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)) {
|
||||
await secretsManager.send(
|
||||
new UpdateSecretCommand({
|
||||
SecretId: secretId,
|
||||
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue)
|
||||
})
|
||||
);
|
||||
if (secretValue) {
|
||||
await secretsManager.send(
|
||||
new UpdateSecretCommand({
|
||||
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;
|
||||
@ -989,16 +1001,21 @@ const syncSecretsAWSSecretManager = async ({
|
||||
} catch (err) {
|
||||
// case 1: when AWS manager can't find the specified secret
|
||||
if (err instanceof ResourceNotFoundException && secretsManager) {
|
||||
await secretsManager.send(
|
||||
new CreateSecretCommand({
|
||||
Name: secretId,
|
||||
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
|
||||
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
|
||||
: []
|
||||
})
|
||||
);
|
||||
if (secretValue) {
|
||||
await secretsManager.send(
|
||||
new CreateSecretCommand({
|
||||
Name: secretId,
|
||||
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
|
||||
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
|
||||
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
|
||||
} else {
|
||||
throw err;
|
||||
@ -1527,11 +1544,13 @@ const syncSecretsNetlify = async ({
|
||||
*/
|
||||
const syncSecretsGitHub = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken,
|
||||
appendices
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
integrationAuth: TIntegrationAuths;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessToken: string;
|
||||
appendices?: { prefix: string; suffix: string };
|
||||
@ -1553,9 +1572,24 @@ const syncSecretsGitHub = async ({
|
||||
selected_repositories_url?: string | undefined;
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: accessToken
|
||||
});
|
||||
const authMetadata = IntegrationAuthMetadataSchema.parse(integrationAuth.metadata || {});
|
||||
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 {
|
||||
Repo = "github-repo",
|
||||
@ -4054,6 +4088,7 @@ export const syncIntegrationSecrets = async ({
|
||||
case Integrations.GITHUB:
|
||||
await syncSecretsGitHub({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken,
|
||||
appendices
|
||||
|
@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
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";
|
||||
|
||||
@ -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();
|
||||
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 = (
|
||||
await request.get<ExchangeCodeGithubResponse>(IntegrationUrls.GITHUB_TOKEN_URL, {
|
||||
params: {
|
||||
@ -346,6 +407,7 @@ type TExchangeReturn = {
|
||||
url?: string;
|
||||
teamId?: string;
|
||||
accountId?: string;
|
||||
installationName?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -355,11 +417,13 @@ type TExchangeReturn = {
|
||||
export const exchangeCode = async ({
|
||||
integration,
|
||||
code,
|
||||
url
|
||||
url,
|
||||
installationId
|
||||
}: {
|
||||
integration: string;
|
||||
code: string;
|
||||
url?: string;
|
||||
installationId?: string;
|
||||
}): Promise<TExchangeReturn> => {
|
||||
switch (integration) {
|
||||
case Integrations.GCP_SECRET_MANAGER:
|
||||
@ -384,7 +448,8 @@ export const exchangeCode = async ({
|
||||
});
|
||||
case Integrations.GITHUB:
|
||||
return exchangeCodeGithub({
|
||||
code
|
||||
code,
|
||||
installationId
|
||||
});
|
||||
case Integrations.GITLAB:
|
||||
return exchangeCodeGitlab({
|
||||
|
@ -120,7 +120,13 @@ export const integrationServiceFactory = ({
|
||||
secretPath,
|
||||
projectId: integrationAuth.projectId
|
||||
});
|
||||
return { integration, integrationAuth };
|
||||
return {
|
||||
integration: {
|
||||
...integration,
|
||||
environment: folder.environment
|
||||
},
|
||||
integrationAuth
|
||||
};
|
||||
};
|
||||
|
||||
const updateIntegration = async ({
|
||||
@ -183,7 +189,10 @@ export const integrationServiceFactory = ({
|
||||
projectId: folder.projectId
|
||||
});
|
||||
|
||||
return updatedIntegration;
|
||||
return {
|
||||
...updatedIntegration,
|
||||
environment: folder.environment
|
||||
};
|
||||
};
|
||||
|
||||
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
|
||||
@ -249,27 +258,7 @@ export const integrationServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const deletedIntegration = await integrationDAL.transaction(async (tx) => {
|
||||
// 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;
|
||||
});
|
||||
|
||||
const deletedIntegration = await integrationDAL.deleteById(id);
|
||||
return { ...integration, ...deletedIntegration };
|
||||
};
|
||||
|
||||
|
@ -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 real time this would be milliseconds
|
||||
*/
|
||||
const getOrgKmsKeyId = async (orgId: string) => {
|
||||
let org = await orgDAL.findById(orgId);
|
||||
const getOrgKmsKeyId = async (orgId: string, trx?: Knex) => {
|
||||
let org = await orgDAL.findById(orgId, trx);
|
||||
|
||||
if (!org) {
|
||||
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")
|
||||
});
|
||||
|
||||
org = await orgDAL.findById(orgId);
|
||||
org = await orgDAL.findById(orgId, trx);
|
||||
} else {
|
||||
const keyId = await orgDAL.transaction(async (tx) => {
|
||||
const keyId = await (trx || orgDAL).transaction(async (tx) => {
|
||||
org = await orgDAL.findById(orgId, tx);
|
||||
if (org.kmsDefaultKeyId) {
|
||||
return org.kmsDefaultKeyId;
|
||||
@ -240,11 +240,12 @@ export const kmsServiceFactory = ({
|
||||
|
||||
const decryptWithKmsKey = async ({
|
||||
kmsId,
|
||||
depth = 0
|
||||
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number }) => {
|
||||
depth = 0,
|
||||
tx
|
||||
}: Omit<TDecryptWithKmsDTO, "cipherTextBlob"> & { depth?: number; tx?: Knex }) => {
|
||||
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) {
|
||||
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
|
||||
const orgKmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsDoc.orgKms.id,
|
||||
depth: depth + 1
|
||||
depth: depth + 1,
|
||||
tx
|
||||
});
|
||||
|
||||
const orgKmsDataKey = await orgKmsDecryptor({
|
||||
@ -375,9 +377,9 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const $getOrgKmsDataKey = async (orgId: string) => {
|
||||
const kmsKeyId = await getOrgKmsKeyId(orgId);
|
||||
let org = await orgDAL.findById(orgId);
|
||||
const $getOrgKmsDataKey = async (orgId: string, trx?: Knex) => {
|
||||
const kmsKeyId = await getOrgKmsKeyId(orgId, trx);
|
||||
let org = await orgDAL.findById(orgId, trx);
|
||||
|
||||
if (!org) {
|
||||
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")
|
||||
});
|
||||
|
||||
org = await orgDAL.findById(orgId);
|
||||
org = await orgDAL.findById(orgId, trx);
|
||||
} else {
|
||||
const orgDataKey = await orgDAL.transaction(async (tx) => {
|
||||
const orgDataKey = await (trx || orgDAL).transaction(async (tx) => {
|
||||
org = await orgDAL.findById(orgId, tx);
|
||||
if (org.kmsEncryptedDataKey) {
|
||||
return;
|
||||
@ -455,8 +457,8 @@ export const kmsServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getProjectSecretManagerKmsKeyId = async (projectId: string) => {
|
||||
let project = await projectDAL.findById(projectId);
|
||||
const getProjectSecretManagerKmsKeyId = async (projectId: string, trx?: Knex) => {
|
||||
let project = await projectDAL.findById(projectId, trx);
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: "Project not found" });
|
||||
}
|
||||
@ -477,7 +479,7 @@ export const kmsServiceFactory = ({
|
||||
|
||||
project = await projectDAL.findById(projectId);
|
||||
} else {
|
||||
const kmsKeyId = await projectDAL.transaction(async (tx) => {
|
||||
const kmsKeyId = await (trx || projectDAL).transaction(async (tx) => {
|
||||
project = await projectDAL.findById(projectId, tx);
|
||||
if (project.kmsSecretManagerKeyId) {
|
||||
return project.kmsSecretManagerKeyId;
|
||||
@ -520,9 +522,9 @@ export const kmsServiceFactory = ({
|
||||
return project.kmsSecretManagerKeyId;
|
||||
};
|
||||
|
||||
const $getProjectSecretManagerKmsDataKey = async (projectId: string) => {
|
||||
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId);
|
||||
let project = await projectDAL.findById(projectId);
|
||||
const $getProjectSecretManagerKmsDataKey = async (projectId: string, trx?: Knex) => {
|
||||
const kmsKeyId = await getProjectSecretManagerKmsKeyId(projectId, trx);
|
||||
let project = await projectDAL.findById(projectId, trx);
|
||||
|
||||
if (!project.kmsSecretManagerEncryptedDataKey) {
|
||||
const lock = await keyStore
|
||||
@ -538,18 +540,21 @@ export const kmsServiceFactory = ({
|
||||
delay: 500
|
||||
});
|
||||
|
||||
project = await projectDAL.findById(projectId);
|
||||
project = await projectDAL.findById(projectId, trx);
|
||||
} else {
|
||||
const projectDataKey = await projectDAL.transaction(async (tx) => {
|
||||
const projectDataKey = await (trx || projectDAL).transaction(async (tx) => {
|
||||
project = await projectDAL.findById(projectId, tx);
|
||||
if (project.kmsSecretManagerEncryptedDataKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataKey = randomSecureBytes();
|
||||
const kmsEncryptor = await encryptWithKmsKey({
|
||||
kmsId: kmsKeyId
|
||||
});
|
||||
const kmsEncryptor = await encryptWithKmsKey(
|
||||
{
|
||||
kmsId: kmsKeyId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const { cipherTextBlob } = await kmsEncryptor({
|
||||
plainText: dataKey
|
||||
@ -585,7 +590,8 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
const kmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsKeyId
|
||||
kmsId: kmsKeyId,
|
||||
tx: trx
|
||||
});
|
||||
|
||||
return kmsDecryptor({
|
||||
@ -593,13 +599,13 @@ export const kmsServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO) => {
|
||||
const $getDataKey = async (dto: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
||||
switch (dto.type) {
|
||||
case KmsDataKey.SecretManager: {
|
||||
return $getProjectSecretManagerKmsDataKey(dto.projectId);
|
||||
return $getProjectSecretManagerKmsDataKey(dto.projectId, trx);
|
||||
}
|
||||
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
|
||||
// none of the entities outside can interact directly or expose the data key
|
||||
// NOTICE: If changing here update migrations/utils/kms
|
||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO) => {
|
||||
const dataKey = await $getDataKey(encryptionContext);
|
||||
const createCipherPairWithDataKey = async (encryptionContext: TEncryptWithKmsDataKeyDTO, trx?: Knex) => {
|
||||
const dataKey = await $getDataKey(encryptionContext, trx);
|
||||
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
|
||||
return {
|
||||
|
@ -106,14 +106,19 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("superAdmin").withSchema(TableName.Users),
|
||||
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 }) => ({
|
||||
...data,
|
||||
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
|
||||
}));
|
||||
return members.map(
|
||||
({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, superAdmin, ...data }) => ({
|
||||
...data,
|
||||
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey, superAdmin }
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
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("lastName").withSchema(TableName.Users),
|
||||
db.ref("scimEnabled").withSchema(TableName.Organization),
|
||||
db.ref("defaultMembershipRole").withSchema(TableName.Organization),
|
||||
db.ref("externalId").withSchema(TableName.UserAliases)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
52
backend/src/services/org/org-role-fns.ts
Normal file
52
backend/src/services/org/org-role-fns.ts
Normal 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 };
|
||||
};
|
@ -11,6 +11,8 @@ import {
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
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 { TOrgRoleDALFactory } from "./org-role-dal";
|
||||
@ -18,11 +20,18 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
|
||||
type TOrgRoleServiceFactoryDep = {
|
||||
orgRoleDAL: TOrgRoleDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
orgDAL: TOrgDALFactory;
|
||||
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
|
||||
};
|
||||
|
||||
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
|
||||
|
||||
export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
|
||||
export const orgRoleServiceFactory = ({
|
||||
orgRoleDAL,
|
||||
orgDAL,
|
||||
permissionService,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
}: TOrgRoleServiceFactoryDep) => {
|
||||
const createRole = async (
|
||||
userId: string,
|
||||
orgId: string,
|
||||
@ -129,6 +138,30 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
||||
) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||
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 });
|
||||
if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" });
|
||||
|
||||
|
@ -32,6 +32,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
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 { 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 { TIdentityMetadataDALFactory } from "../identity/identity-metadata-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 { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
@ -79,7 +81,7 @@ type TOrgServiceFactoryDep = {
|
||||
TProjectMembershipDALFactory,
|
||||
"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">;
|
||||
incidentContactDAL: TIncidentContactsDALFactory;
|
||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||
@ -93,8 +95,9 @@ type TOrgServiceFactoryDep = {
|
||||
>;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
};
|
||||
|
||||
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||
@ -121,7 +124,8 @@ export const orgServiceFactory = ({
|
||||
oidcConfigDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
identityMetadataDAL
|
||||
identityMetadataDAL,
|
||||
projectBotService
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
/*
|
||||
* Get organization details by the organization id
|
||||
@ -264,7 +268,7 @@ export const orgServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
orgId,
|
||||
data: { name, slug, authEnforced, scimEnabled }
|
||||
data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug }
|
||||
}: TUpdateOrgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
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, {
|
||||
name,
|
||||
slug: slug ? slugify(slug) : undefined,
|
||||
authEnforced,
|
||||
scimEnabled
|
||||
scimEnabled,
|
||||
defaultMembershipRole
|
||||
});
|
||||
if (!org) throw new NotFoundError({ message: "Organization not found" });
|
||||
return org;
|
||||
@ -706,20 +721,67 @@ export const orgServiceFactory = ({
|
||||
|
||||
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
||||
if (!ghostUser) {
|
||||
throw new NotFoundError({
|
||||
name: "InviteUser",
|
||||
message: "Failed to find project owner"
|
||||
});
|
||||
}
|
||||
// this will auto generate bot
|
||||
const { botKey, bot: autoGeneratedBot } = await projectBotService.getBotKey(projectId, true);
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new NotFoundError({
|
||||
name: "InviteUser",
|
||||
message: "Failed to find project owner's latest key"
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
|
||||
let ghostUserId = ghostUser?.id;
|
||||
|
||||
// backfill missing ghost user
|
||||
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);
|
||||
@ -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({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
@ -773,7 +843,7 @@ export const orgServiceFactory = ({
|
||||
newWsMembers.map((el) => ({
|
||||
encryptedKey: el.workspaceEncryptedKey,
|
||||
nonce: el.workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
senderId: ghostUserId,
|
||||
receiverId: el.orgMembershipId,
|
||||
projectId
|
||||
})),
|
||||
|
@ -26,18 +26,13 @@ export type TDeleteOrgMembershipDTO = {
|
||||
};
|
||||
|
||||
export type TInviteUserToOrgDTO = {
|
||||
actorId: string;
|
||||
actor: ActorType;
|
||||
orgId: string;
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
inviteeEmails: string[];
|
||||
organizationRoleSlug: string;
|
||||
projects?: {
|
||||
id: string;
|
||||
projectRoleSlug?: string[];
|
||||
}[];
|
||||
};
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TVerifyUserToOrgDTO = {
|
||||
email: string;
|
||||
@ -63,7 +58,13 @@ export type TFindAllWorkspacesDTO = {
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
export type TGetOrgGroupsDTO = TOrgPermission;
|
||||
|
@ -24,14 +24,14 @@ export const getBotKeyFnFactory = (
|
||||
projectBotDAL: TProjectBotDALFactory,
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">
|
||||
) => {
|
||||
const getBotKeyFn = async (projectId: string) => {
|
||||
const getBotKeyFn = async (projectId: string, shouldGetBotKey?: boolean) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project)
|
||||
throw new NotFoundError({
|
||||
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 };
|
||||
}
|
||||
|
||||
@ -65,8 +65,9 @@ export const getBotKeyFnFactory = (
|
||||
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(botKey.privateKey);
|
||||
const encryptedWorkspaceKey = encryptAsymmetric(workspaceKey, botKey.publicKey, userPrivateKey);
|
||||
|
||||
let botId;
|
||||
if (!bot) {
|
||||
await projectBotDAL.create({
|
||||
const newBot = await projectBotDAL.create({
|
||||
name: "Infisical Bot (Ghost)",
|
||||
projectId,
|
||||
isActive: true,
|
||||
@ -80,8 +81,9 @@ export const getBotKeyFnFactory = (
|
||||
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
|
||||
senderId: projectV1Keys.userId
|
||||
});
|
||||
botId = newBot.id;
|
||||
} else {
|
||||
await projectBotDAL.updateById(bot.id, {
|
||||
const updatedBot = await projectBotDAL.updateById(bot.id, {
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
@ -93,8 +95,10 @@ export const getBotKeyFnFactory = (
|
||||
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
|
||||
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 });
|
||||
@ -104,7 +108,7 @@ export const getBotKeyFnFactory = (
|
||||
nonce: bot.encryptedProjectKeyNonce,
|
||||
publicKey: bot.sender.publicKey
|
||||
});
|
||||
return { botKey, project, shouldUseSecretV2Bridge: false };
|
||||
return { botKey, project, shouldUseSecretV2Bridge: false, bot: { id: bot.id } };
|
||||
};
|
||||
|
||||
return getBotKeyFn;
|
||||
|
@ -27,8 +27,8 @@ export const projectBotServiceFactory = ({
|
||||
}: TProjectBotServiceFactoryDep) => {
|
||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
|
||||
|
||||
const getBotKey = async (projectId: string) => {
|
||||
return getBotKeyFn(projectId);
|
||||
const getBotKey = async (projectId: string, shouldGetBotKey?: boolean) => {
|
||||
return getBotKeyFn(projectId, shouldGetBotKey);
|
||||
};
|
||||
|
||||
const findBotByProjectId = async ({
|
||||
|
@ -147,6 +147,7 @@ export const projectServiceFactory = ({
|
||||
workspaceName,
|
||||
slug: projectSlug,
|
||||
kmsKeyId,
|
||||
tx: trx,
|
||||
createDefaultEnvs = true
|
||||
}: TCreateProjectDTO) => {
|
||||
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);
|
||||
|
||||
if (kmsKeyId) {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
@ -30,6 +32,7 @@ export type TCreateProjectDTO = {
|
||||
slug?: string;
|
||||
kmsKeyId?: string;
|
||||
createDefaultEnvs?: boolean;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
export type TDeleteProjectBySlugDTO = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
|
||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
@ -82,7 +82,10 @@ export const fnSecretBulkInsert = async ({
|
||||
})
|
||||
);
|
||||
|
||||
const newSecrets = await secretDAL.insertMany(sanitizedInputSecrets.map((el) => ({ ...el, folderId })));
|
||||
const newSecrets = await secretDAL.insertMany(
|
||||
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
||||
tx
|
||||
);
|
||||
const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key);
|
||||
const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) =>
|
||||
secretTags.map((tag) => ({
|
||||
@ -339,7 +342,7 @@ export const recursivelyGetSecretPaths = async ({
|
||||
});
|
||||
|
||||
if (!env) {
|
||||
throw new Error(`'${environment}' environment not found in project with ID ${projectId}`);
|
||||
throw new NotFoundError({ message: `'${environment}' environment not found in project with ID ${projectId}` });
|
||||
}
|
||||
|
||||
// Fetch all folders in env once with a single query
|
||||
|
@ -193,14 +193,16 @@ export const secretV2BridgeServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
if (inputSecret.type === SecretType.Shared) {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
}
|
||||
|
||||
return reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...secret[0],
|
||||
@ -349,14 +351,17 @@ export const secretV2BridgeServiceFactory = ({
|
||||
projectId
|
||||
});
|
||||
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
actor,
|
||||
actorId,
|
||||
secretPath,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
if (inputSecret.type === SecretType.Shared) {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
}
|
||||
|
||||
return reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...updatedSecret[0],
|
||||
value: inputSecret.secretValue || "",
|
||||
@ -427,14 +432,16 @@ export const secretV2BridgeServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
actor,
|
||||
actorId,
|
||||
secretPath,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
if (inputSecret.type === SecretType.Shared) {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
|
@ -152,7 +152,7 @@ export const recursivelyGetSecretPaths = ({
|
||||
});
|
||||
|
||||
if (!env) {
|
||||
throw new Error(`'${environment}' environment not found in project with ID ${projectId}`);
|
||||
throw new NotFoundError({ message: `'${environment}' environment not found in project with ID ${projectId}` });
|
||||
}
|
||||
|
||||
// Fetch all folders in env once with a single query
|
||||
|
@ -17,6 +17,7 @@ import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/sn
|
||||
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn";
|
||||
@ -37,10 +38,14 @@ import { syncIntegrationSecrets } from "../integration-auth/integration-sync-sec
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { createProjectKey } from "../project/project-fns";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
@ -77,7 +82,8 @@ type TSecretQueueFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
||||
projectDAL: TProjectDALFactory;
|
||||
projectBotDAL: TProjectBotDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers" | "create">;
|
||||
smtpService: TSmtpService;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
|
||||
secretVersionDAL: TSecretVersionDALFactory;
|
||||
@ -85,7 +91,7 @@ type TSecretQueueFactoryDep = {
|
||||
secretTagDAL: TSecretTagDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
secretVersionTagDAL: TSecretVersionTagDALFactory;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
kmsService: TKmsServiceFactory;
|
||||
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;
|
||||
@ -95,6 +101,8 @@ type TSecretQueueFactoryDep = {
|
||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
};
|
||||
|
||||
export type TGetSecrets = {
|
||||
@ -111,6 +119,8 @@ type TIntegrationSecret = Record<
|
||||
string,
|
||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
|
||||
>;
|
||||
|
||||
// TODO(akhilmhdh): split this into multiple queue
|
||||
export const secretQueueFactory = ({
|
||||
queueService,
|
||||
integrationDAL,
|
||||
@ -141,7 +151,10 @@ export const secretQueueFactory = ({
|
||||
snapshotSecretV2BridgeDAL,
|
||||
secretApprovalRequestDAL,
|
||||
keyStore,
|
||||
auditLogService
|
||||
auditLogService,
|
||||
orgService,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectKeyDAL
|
||||
}: TSecretQueueFactoryDep) => {
|
||||
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||
const appCfg = getConfig();
|
||||
@ -1028,11 +1041,13 @@ export const secretQueueFactory = ({
|
||||
const {
|
||||
botKey,
|
||||
shouldUseSecretV2Bridge: isProjectUpgradedToV3,
|
||||
project
|
||||
project,
|
||||
bot
|
||||
} = await projectBotService.getBotKey(projectId);
|
||||
if (isProjectUpgradedToV3 || project.upgradeStatus === ProjectUpgradeStatus.InProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!botKey) throw new NotFoundError({ message: "Project bot not found" });
|
||||
await projectDAL.updateById(projectId, { upgradeStatus: ProjectUpgradeStatus.InProgress });
|
||||
|
||||
@ -1044,6 +1059,57 @@ export const secretQueueFactory = ({
|
||||
const folders = await folderDAL.findByProjectId(projectId);
|
||||
// except secret version and snapshot migrate rest of everything first in a transaction
|
||||
await secretDAL.transaction(async (tx) => {
|
||||
// if project v1 create the project ghost user
|
||||
if (project.version === ProjectVersion.V1) {
|
||||
const ghostUser = await orgService.addGhostUser(project.orgId, tx);
|
||||
const projectMembership = await projectMembershipDAL.create(
|
||||
{
|
||||
userId: ghostUser.user.id,
|
||||
projectId: project.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.create(
|
||||
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
|
||||
tx
|
||||
);
|
||||
|
||||
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
||||
publicKey: ghostUser.keys.publicKey,
|
||||
privateKey: ghostUser.keys.plainPrivateKey,
|
||||
plainProjectKey: botKey
|
||||
});
|
||||
|
||||
// 4. Save the project key for the ghost user.
|
||||
await projectKeyDAL.create(
|
||||
{
|
||||
projectId: project.id,
|
||||
receiverId: ghostUser.user.id,
|
||||
encryptedKey: encryptedProjectKey,
|
||||
nonce: encryptedProjectKeyIv,
|
||||
senderId: ghostUser.user.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||
await projectBotDAL.updateById(
|
||||
bot.id,
|
||||
{
|
||||
tag,
|
||||
iv,
|
||||
encryptedProjectKey,
|
||||
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: true,
|
||||
publicKey: ghostUser.keys.publicKey,
|
||||
senderId: ghostUser.user.id,
|
||||
algorithm,
|
||||
keyEncoding: encoding
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
for (const folder of folders) {
|
||||
const folderId = folder.id;
|
||||
/*
|
||||
|
@ -264,14 +264,16 @@ export const secretServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath: path,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
if (inputSecret.type === SecretType.Shared) {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath: path,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
}
|
||||
return { ...secret[0], environment, workspace: projectId, tags, secretPath: path };
|
||||
};
|
||||
|
||||
@ -399,14 +401,16 @@ export const secretServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
actor,
|
||||
actorId,
|
||||
secretPath: path,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
if (inputSecret.type === SecretType.Shared) {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath: path,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
}
|
||||
return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path };
|
||||
};
|
||||
|
||||
@ -474,15 +478,17 @@ export const secretServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
actor,
|
||||
actorId,
|
||||
secretPath: path,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
// TODO(akhilmhdh-pg): license check, posthog service and snapshot
|
||||
if (inputSecret.type === SecretType.Shared) {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath: path,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
}
|
||||
|
||||
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path };
|
||||
};
|
||||
|
||||
|
@ -34,7 +34,10 @@ export enum SmtpTemplates {
|
||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
|
||||
IntegrationSyncFailed = "integrationSyncFailed.handlebars"
|
||||
IntegrationSyncFailed = "integrationSyncFailed.handlebars",
|
||||
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
||||
ExternalImportFailed = "externalImportFailed.handlebars",
|
||||
ExternalImportStarted = "externalImportStarted.handlebars"
|
||||
}
|
||||
|
||||
export enum SmtpHost {
|
||||
|
@ -0,0 +1,21 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Import failed</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>An import from {{provider}} to Infisical has failed</h2>
|
||||
<p>An import from
|
||||
{{provider}}
|
||||
to Infisical has failed due to unforeseen circumstances. Please re-try your import, and if the issue persists, you
|
||||
can contact the Infisical team at team@infisical.com.
|
||||
</p>
|
||||
|
||||
<p>Error: {{error}}</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -0,0 +1,17 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Import in progress</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>An import from {{provider}} to Infisical is in progress</h2>
|
||||
<p>An import from
|
||||
{{provider}}
|
||||
to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import
|
||||
has finished or if it fails.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user