Compare commits

..

108 Commits

Author SHA1 Message Date
Sheen Capadngan
06a7e804eb misc: add custom role slug in fetch group 2025-06-13 17:26:36 +08:00
Maidul Islam
0f00474243 Merge pull request #3735 from Infisical/misc/add-checks-for-helm-verification
misc: add verification pipelines for helm charts
2025-06-12 22:29:44 -04:00
Maidul Islam
3df010f266 Merge branch 'main' into misc/add-checks-for-helm-verification 2025-06-12 22:22:17 -04:00
x032205
333ce9d164 Merge pull request #3755 from Infisical/ENG-2773
feat(secret-rotation): Oracle Database
2025-06-12 21:06:57 -04:00
carlosmonastyrski
9621df4f8b Merge pull request #3736 from Infisical/feat/azureDevopsSecretSync
Feat/azure devops secret sync
2025-06-12 22:06:05 -03:00
x032205
3f2de2c5ef Rename API token mentions to access token 2025-06-12 20:36:34 -04:00
x032205
b2b1c13393 Lint 2025-06-12 20:24:09 -04:00
Maidul Islam
1fb0c638d6 Merge pull request #3787 from Infisical/ENG-2909
Update wording for service tokens
2025-06-12 19:32:54 -04:00
x032205
c1ad49a532 Update wording for service tokens 2025-06-12 19:28:41 -04:00
x032205
d1fcc739c9 Merge pull request #3552 from Infisical/ENG-2705
feat(dynamic-secrets): GCP IAM
2025-06-12 18:01:17 -04:00
x032205
c7458d94aa Warning about tokens 2025-06-12 15:45:30 -04:00
x032205
93570df318 TForm update 2025-06-12 15:39:52 -04:00
x032205
e798b4a7ba Merge branch 'main' into ENG-2705 2025-06-12 15:18:00 -04:00
x032205
36c93f47d9 Review fixes 2025-06-12 15:17:22 -04:00
x032205
dbbcb157ef Merge branch 'main' into ENG-2773 2025-06-12 15:09:38 -04:00
x032205
bdc23d22e7 Merge pull request #3775 from Infisical/ENG-2861
feat(machine-identity): Alibaba Cloud
2025-06-12 13:57:14 -04:00
x032205
08c1740afc Merge pull request #3782 from Infisical/ENG-2900
improvement(secret-scanning): Multi-select actions
2025-06-12 11:56:28 -04:00
x032205
3cac4ef927 Reviews 2025-06-12 11:43:32 -04:00
carlosmonastyrski
2667f8f0f2 Merge pull request #3785 from Infisical/fix/auth0SamlMappingsTip
fix(docs): add a tip on Auth0 SAML doc tip
2025-06-12 12:03:10 -03:00
carlosmonastyrski
b39537472b fix(docs): fix indentation issue 2025-06-12 11:56:19 -03:00
carlosmonastyrski
6b60b2562d Merge pull request #3784 from Infisical/fix/pitBannerImprovements
feat(pit): improve banner messaging
2025-06-12 11:46:39 -03:00
carlosmonastyrski
c2a7827080 fix(docs): add a tip on Auth0 SAML doc to remind that the mappings could be adapted to the custom settings of the organization 2025-06-12 11:42:41 -03:00
carlosmonastyrski
64e09b0dcd feat(pit): improve banner messaging 2025-06-12 11:28:56 -03:00
Daniel Hougaard
a7176d44dd Merge pull request #3762 from Infisical/daniel/aws-auth-eks
docs(identities/aws-auth): eks pod auth
2025-06-12 18:11:59 +04:00
Daniel Hougaard
09d4cdc634 requested changes 2025-06-12 18:03:30 +04:00
x032205
8a93c0bd59 Cap array 2025-06-12 02:16:07 -04:00
x032205
c0f8f50981 lint 2025-06-12 02:04:01 -04:00
x032205
fec47ef81c Mass-update endpoint 2025-06-12 01:59:47 -04:00
x032205
348f4b9787 Greptile review fixes + pagination tweaks 2025-06-12 01:39:23 -04:00
x032205
aa577b095c improvement(secret-scanning): Multi-select actions 2025-06-12 01:25:53 -04:00
carlosmonastyrski
f515cc83d7 Fix lint issue 2025-06-11 20:18:58 -03:00
carlosmonastyrski
17bbdbe7bb feat(secret-sync): Add Azure Devops PR suggestions 2025-06-11 20:06:45 -03:00
carlosmonastyrski
427de068d5 Merge remote-tracking branch 'origin/main' into feat/azureDevopsSecretSync 2025-06-11 19:20:26 -03:00
x032205
dbf7ecc9b6 Merge pull request #3763 from Infisical/docs/add-packer-plugin-docs
feat(docs): Packer Plugin Docs
2025-06-11 17:44:35 -04:00
x032205
1ef9885062 Review fixes 2025-06-11 17:09:17 -04:00
carlosmonastyrski
de48c3e161 Merge pull request #3781 from Infisical/fix/inviteUsersWithIdentities
feat(invite-users): fix issue where invitations were not sent when the actor was an identity
2025-06-11 16:42:04 -03:00
carlosmonastyrski
852664e2cb feat(invite-users): fix issue where invitations were not sent when the actor was an identity 2025-06-11 16:11:34 -03:00
Sheen
fbc8264732 Merge pull request #3779 from Infisical/misc/cli-dynamic-secret-and-agent-improvements
misc: added project slug flag support to dynamic secret commands
2025-06-12 02:08:17 +08:00
Sheen Capadngan
4303547d8c misc: added more descriptive comment 2025-06-12 01:58:56 +08:00
Sheen Capadngan
f1c8a66d31 misc: converted flags to dash 2025-06-12 01:39:16 +08:00
carlosmonastyrski
baa05714ab Merge pull request #3780 from Infisical/fix/azureClientSecretsManualDeletionCheck
feat(secret-rotation): Azure Client Secrets manually deleted client secrets check
2025-06-11 14:31:21 -03:00
Sheen Capadngan
0c21c19c95 misc: agent improvements 2025-06-12 01:25:47 +08:00
carlosmonastyrski
c487614c38 feat(secret-rotation): fix Azure Client Secrets to check if the client secret has been manually deleted to avoid blocking the process 2025-06-11 13:28:34 -03:00
carlosmonastyrski
a55c8cacea Merge pull request #3778 from Infisical/fix/secretRequestReadIssue
feat(secret-request): hide secret value on missing secret read permission
2025-06-11 12:13:22 -03:00
Sheen Capadngan
62308fb0a3 misc: added project slug flag support to dynamic secret commands 2025-06-11 23:06:27 +08:00
Sheen
55aa1e87c0 Merge pull request #3767 from Infisical/feat/allow-k8-dynamic-secret-multi-namespace-and-others
feat: allow k8 dynamic secret multi namespace and show proper error
2025-06-11 23:01:00 +08:00
carlosmonastyrski
c5c7adbc42 feat(secret-request): hide secret value on missing secret read permission 2025-06-11 11:43:14 -03:00
Sheen Capadngan
f686882ce6 misc: addressed doc 2025-06-11 22:41:16 +08:00
Maidul Islam
e35417e11b Update kubernetes-helm.mdx 2025-06-11 10:06:45 -04:00
Sheen Capadngan
ff0f4cf46a misc: added support for copying gateway ID 2025-06-11 20:49:10 +08:00
x032205
53968e07d0 Lint + greptile review fixes 2025-06-11 02:59:04 -04:00
Sheen Capadngan
64093e9175 misc: final revisions 2025-06-11 14:55:41 +08:00
x032205
c315eed4d4 feat(machine-identity): Alibaba Cloud 2025-06-11 02:44:53 -04:00
Sheen Capadngan
78fd852588 Merge remote-tracking branch 'origin/main' into feat/allow-k8-dynamic-secret-multi-namespace-and-others 2025-06-11 14:28:15 +08:00
Maidul Islam
0c1f761a9a Merge pull request #3774 from Infisical/akhilmhdh-patch-4
Update aws-iam.mdx
2025-06-10 23:23:16 -04:00
Akhil Mohan
c363f485eb Update aws-iam.mdx 2025-06-11 08:52:35 +05:30
Maidul Islam
433d83641d Merge pull request #3765 from Infisical/help-fix-frontend-cache-issue
disable caching for frontend assets
2025-06-10 19:29:10 -04:00
carlosmonastyrski
35bb7f299c Merge pull request #3773 from Infisical/fix/pitSecretVersionsZeroIssue
feat(pit): improve commit changes condition as some old versions can be zero
2025-06-10 20:17:11 -03:00
carlosmonastyrski
160e2b773b feat(pit): improve commit changes condition as some old versions can be zero 2025-06-10 19:02:02 -03:00
Daniel Hougaard
f0a70e23ac Merge pull request #3772 from Infisical/daniel/full-gateway-auth-2
fix: allow for empty target URLs
2025-06-11 01:56:57 +04:00
Daniel Hougaard
a6271a6187 fix: allow for empty target URLs 2025-06-11 01:45:38 +04:00
Sheen Capadngan
b2fbec740f misc: updated to use new proxy action 2025-06-11 05:11:23 +08:00
Sheen Capadngan
86e5f46d89 Merge remote-tracking branch 'origin/main' into feat/allow-k8-dynamic-secret-multi-namespace-and-others 2025-06-11 04:58:44 +08:00
Sheen Capadngan
720789025c misc: addressed greptile 2025-06-11 04:58:12 +08:00
Daniel Hougaard
811b3d5934 Merge pull request #3769 from Infisical/daniel/full-gateway-auth
feat(gateway): use gateway for full k8s request life-cycle
2025-06-11 00:55:38 +04:00
Daniel Hougaard
cac702415f Update IdentityKubernetesAuthForm.tsx 2025-06-11 00:51:47 +04:00
carlosmonastyrski
dbe7acdc80 Merge pull request #3771 from Infisical/fix/secretRotationIssueCommits
feat(secret-rotation): fix metadata empty objects breaking version co…
2025-06-10 17:48:51 -03:00
carlosmonastyrski
b33985b338 feat(secret-rotation): fix metadata empty objects breaking version comparison 2025-06-10 17:45:58 -03:00
Daniel Hougaard
670376336e Update IdentityKubernetesAuthForm.tsx 2025-06-11 00:27:26 +04:00
Sheen
c59eddb00a doc: added api reference for k8 lease 2025-06-10 20:19:33 +00:00
Sheen Capadngan
fe40ba497b misc: added flag to CLI 2025-06-11 04:11:51 +08:00
Daniel Hougaard
c5b7e3d8be minor patches 2025-06-11 00:11:00 +04:00
Daniel Hougaard
47e778a0b8 feat(gateway): use gateway for full k8s request life-cycle 2025-06-10 23:59:10 +04:00
Sheen Capadngan
8b443e0957 misc: url and ssl config not needed when gateway auth 2025-06-11 02:51:22 +08:00
Sheen Capadngan
f7fb015bd8 feat: allow k8 dynamic secret multi namespace and show proper error 2025-06-11 01:11:29 +08:00
carlosmonastyrski
0d7cd357c3 Merge pull request #3766 from Infisical/fix/fixDocsForCliUsageUrlEurope
feat(docs): Added a small note to clarify the usage of the env variable INFISICAL_API_URL for EU users
2025-06-10 13:01:03 -03:00
carlosmonastyrski
e40f65836f feat(docs): Added a small note to clarify the usage of the env variable INFISICAL_API_URL for EU users 2025-06-10 08:25:06 -03:00
x032205
2be56f6a70 Greptile review fix 2025-06-09 16:57:39 -04:00
x032205
1ff1f3fad3 feat(docs): Packer Plugin Docs 2025-06-09 16:55:41 -04:00
x032205
0ae96dfff4 Proper quote escaping 2025-06-09 13:26:47 -04:00
x032205
8ad6488bd9 Bug fix 2025-06-09 13:17:59 -04:00
x032205
e264b68b7e Merge branch 'check-non-re2-regex-workflow' into ENG-2773 2025-06-09 13:12:24 -04:00
x032205
9e881534ec Merge branch 'check-non-re2-regex-workflow' into ENG-2773 2025-06-09 12:31:45 -04:00
x032205
2832ff5c76 Merge RE2 regex workflow for performance testing 2025-06-09 12:11:49 -04:00
x032205
4c6cca0864 Greptile review fixes 2025-06-09 12:10:47 -04:00
x032205
c06bbf0b9b Merge branch 'main' into ENG-2773 2025-06-09 12:03:54 -04:00
Daniel Hougaard
69392a4a51 fix(identity/aws-auth): allow for lowercase authoriazation header 2025-06-09 19:45:05 +04:00
Daniel Hougaard
130f1a167e docs: add docs for eks pod auth 2025-06-09 19:44:36 +04:00
x032205
8ab710817d Fixes 2025-06-09 10:01:56 -04:00
x032205
ca39e75434 Merge 2025-06-09 09:20:13 -04:00
x032205
265b25a4c6 Update some username stuff 2025-06-07 01:44:58 -04:00
x032205
54f6e0b5c6 docs 2025-06-07 01:08:32 -04:00
x032205
f2cdefaeec Remove comment 2025-06-07 00:08:43 -04:00
x032205
2d588d87ac Tweaks 2025-06-07 00:08:32 -04:00
x032205
5ee2eb1aa2 feat(secret-rotation): Oracle DB 2025-06-07 00:07:34 -04:00
carlosmonastyrski
ff5f66a75f feat(secret-sync): Add Azure Devops PR suggestions 2025-06-06 10:27:13 -03:00
carlosmonastyrski
bf72638600 feat(secret-sync): Add Azure Devops PR suggestions 2025-06-06 10:08:31 -03:00
carlosmonastyrski
d9bc4da6f1 feat(secret-sync): Add Azure Devops docs 2025-06-05 15:17:35 -03:00
carlosmonastyrski
7f8d5ec11a feat(secret-sync): Add Azure Devops Secret Sync 2025-06-05 13:57:41 -03:00
Sheen Capadngan
141d0ede2d misc: add pr checks for gateway 2025-06-05 22:29:54 +08:00
Sheen Capadngan
ab78a79415 misc: add test workflow for gateway helm 2025-06-05 22:25:24 +08:00
Sheen Capadngan
8fa6af9ba4 misc: added checks for infisical standalone helm 2025-06-05 21:26:53 +08:00
Sheen Capadngan
f0a2845637 Merge remote-tracking branch 'origin/main' into misc/add-checks-for-helm-verification 2025-06-05 21:24:46 +08:00
Sheen Capadngan
8ffc88ba28 misc: add verification check for secret operator 2025-06-05 03:28:04 +08:00
x032205
05d132a1bb lint fix 2025-05-06 16:32:36 -04:00
x032205
bd7c4fc4eb review fixes 2025-05-06 16:26:51 -04:00
x032205
45c84d4936 Merge branch 'main' into ENG-2705 2025-05-06 15:28:16 -04:00
x032205
8e8e2e0dfe feat(dynamic-secrets): GCP IAM 2025-05-06 15:27:55 -04:00
296 changed files with 8113 additions and 761 deletions

View File

@@ -3,7 +3,62 @@ name: Release Infisical Core Helm chart
on: [workflow_dispatch] on: [workflow_dispatch]
jobs: jobs:
test-helm:
name: Test Helm Chart
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
with:
version: v3.17.0
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.x"
check-latest: true
- name: Add Helm repositories
run: |
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.7.0
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml --charts helm-charts/infisical-standalone-postgres
- name: Create kind cluster
uses: helm/kind-action@v1.12.0
- name: Create namespace
run: kubectl create namespace infisical-standalone-postgres
- name: Create Infisical secrets
run: |
kubectl create secret generic infisical-secrets \
--namespace infisical-standalone-postgres \
--from-literal=AUTH_SECRET=6c1fe4e407b8911c104518103505b218 \
--from-literal=ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 \
--from-literal=SITE_URL=http://localhost:8080
- name: Run chart-testing (install)
run: |
ct install \
--config ct.yaml \
--charts helm-charts/infisical-standalone-postgres \
--helm-extra-args="--timeout=300s" \
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \
--namespace infisical-standalone-postgres
release: release:
needs: test-helm
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -3,8 +3,40 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
test-helm:
name: Test Helm Chart
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
with:
version: v3.17.0
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.x"
check-latest: true
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.7.0
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml --charts helm-charts/secrets-operator
- name: Create kind cluster
uses: helm/kind-action@v1.12.0
- name: Run chart-testing (install)
run: ct install --config ct.yaml --charts helm-charts/secrets-operator
release-helm: release-helm:
name: Release Helm Chart name: Release Helm Chart
needs: test-helm
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -3,8 +3,51 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
test-helm:
name: Test Helm Chart
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
with:
version: v3.17.0
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.x"
check-latest: true
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.7.0
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
- name: Create kind cluster
uses: helm/kind-action@v1.12.0
- name: Create namespace
run: kubectl create namespace infisical-gateway
- name: Create gateway secret
run: kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=my-test-token -n infisical-gateway
- name: Run chart-testing (install)
run: |
ct install \
--config ct.yaml \
--charts helm-charts/infisical-gateway \
--helm-extra-args="--timeout=300s" \
--namespace infisical-gateway
release-helm: release-helm:
name: Release Helm Chart name: Release Helm Chart
needs: test-helm
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -0,0 +1,49 @@
name: Run Helm Chart Tests for Gateway
on:
pull_request:
paths:
- "helm-charts/infisical-gateway/**"
- ".github/workflows/run-helm-chart-tests-infisical-gateway.yml"
jobs:
test-helm:
name: Test Helm Chart
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
with:
version: v3.17.0
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.x"
check-latest: true
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.7.0
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
- name: Create kind cluster
uses: helm/kind-action@v1.12.0
- name: Create namespace
run: kubectl create namespace infisical-gateway
- name: Create gateway secret
run: kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=my-test-token -n infisical-gateway
- name: Run chart-testing (install)
run: |
ct install \
--config ct.yaml \
--charts helm-charts/infisical-gateway \
--helm-extra-args="--timeout=300s" \
--namespace infisical-gateway

View File

@@ -0,0 +1,61 @@
name: Run Helm Chart Tests for Infisical Standalone Postgres
on:
pull_request:
paths:
- "helm-charts/infisical-standalone-postgres/**"
- ".github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml"
jobs:
test-helm:
name: Test Helm Chart
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
with:
version: v3.17.0
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.x"
check-latest: true
- name: Add Helm repositories
run: |
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.7.0
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml --charts helm-charts/infisical-standalone-postgres
- name: Create kind cluster
uses: helm/kind-action@v1.12.0
- name: Create namespace
run: kubectl create namespace infisical-standalone-postgres
- name: Create Infisical secrets
run: |
kubectl create secret generic infisical-secrets \
--namespace infisical-standalone-postgres \
--from-literal=AUTH_SECRET=6c1fe4e407b8911c104518103505b218 \
--from-literal=ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 \
--from-literal=SITE_URL=http://localhost:8080
- name: Run chart-testing (install)
run: |
ct install \
--config ct.yaml \
--charts helm-charts/infisical-standalone-postgres \
--helm-extra-args="--timeout=300s" \
--helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \
--namespace infisical-standalone-postgres

View File

@@ -0,0 +1,38 @@
name: Run Helm Chart Tests for Secret Operator
on:
pull_request:
paths:
- "helm-charts/secrets-operator/**"
- ".github/workflows/run-helm-chart-tests-secret-operator.yml"
jobs:
test-helm:
name: Test Helm Chart
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
with:
version: v3.17.0
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.x"
check-latest: true
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.7.0
- name: Run chart-testing (lint)
run: ct lint --config ct.yaml --charts helm-charts/secrets-operator
- name: Create kind cluster
uses: helm/kind-action@v1.12.0
- name: Run chart-testing (install)
run: ct install --config ct.yaml --charts helm-charts/secrets-operator

View File

@@ -40,4 +40,8 @@ cli/detect/config/gitleaks.toml:gcp-api-key:578
cli/detect/config/gitleaks.toml:gcp-api-key:579 cli/detect/config/gitleaks.toml:gcp-api-key:579
cli/detect/config/gitleaks.toml:gcp-api-key:581 cli/detect/config/gitleaks.toml:gcp-api-key:581
cli/detect/config/gitleaks.toml:gcp-api-key:582 cli/detect/config/gitleaks.toml:gcp-api-key:582
.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml:generic-api-key:51
.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml:generic-api-key:50
.github/workflows/helm-release-infisical-core.yml:generic-api-key:48
.github/workflows/helm-release-infisical-core.yml:generic-api-key:47
backend/src/services/smtp/smtp-service.ts:generic-api-key:79 backend/src/services/smtp/smtp-service.ts:generic-api-key:79

View File

@@ -65,6 +65,7 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p
import { THsmServiceFactory } from "@app/services/hsm/hsm-service"; import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service";
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
@@ -218,6 +219,7 @@ declare module "fastify" {
identityUa: TIdentityUaServiceFactory; identityUa: TIdentityUaServiceFactory;
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory; identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory; identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOciAuth: TIdentityOciAuthServiceFactory; identityOciAuth: TIdentityOciAuthServiceFactory;

View File

@@ -125,6 +125,9 @@ import {
TIdentityAccessTokens, TIdentityAccessTokens,
TIdentityAccessTokensInsert, TIdentityAccessTokensInsert,
TIdentityAccessTokensUpdate, TIdentityAccessTokensUpdate,
TIdentityAlicloudAuths,
TIdentityAlicloudAuthsInsert,
TIdentityAlicloudAuthsUpdate,
TIdentityAwsAuths, TIdentityAwsAuths,
TIdentityAwsAuthsInsert, TIdentityAwsAuthsInsert,
TIdentityAwsAuthsUpdate, TIdentityAwsAuthsUpdate,
@@ -786,6 +789,11 @@ declare module "knex/types/tables" {
TIdentityGcpAuthsInsert, TIdentityGcpAuthsInsert,
TIdentityGcpAuthsUpdate TIdentityGcpAuthsUpdate
>; >;
[TableName.IdentityAliCloudAuth]: KnexOriginal.CompositeTableType<
TIdentityAlicloudAuths,
TIdentityAlicloudAuthsInsert,
TIdentityAlicloudAuthsUpdate
>;
[TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType< [TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType<
TIdentityAwsAuths, TIdentityAwsAuths,
TIdentityAwsAuthsInsert, TIdentityAwsAuthsInsert,

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasConfigColumn = await knex.schema.hasColumn(TableName.DynamicSecretLease, "config");
if (!hasConfigColumn) {
await knex.schema.alterTable(TableName.DynamicSecretLease, (table) => {
table.jsonb("config");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasConfigColumn = await knex.schema.hasColumn(TableName.DynamicSecretLease, "config");
if (hasConfigColumn) {
await knex.schema.alterTable(TableName.DynamicSecretLease, (table) => {
table.dropColumn("config");
});
}
}

View File

@@ -0,0 +1,45 @@
import { Knex } from "knex";
import { selectAllTableCols } from "@app/lib/knex";
import { TableName } from "../schemas";
const BATCH_SIZE = 1000;
export async function up(knex: Knex): Promise<void> {
const hasKubernetesHostColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "kubernetesHost");
if (hasKubernetesHostColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.string("kubernetesHost").nullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasKubernetesHostColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "kubernetesHost");
// find all rows where kubernetesHost is null
const rows = await knex(TableName.IdentityKubernetesAuth)
.whereNull("kubernetesHost")
.select(selectAllTableCols(TableName.IdentityKubernetesAuth));
if (rows.length > 0) {
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
const batch = rows.slice(i, i + BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop
await knex(TableName.IdentityKubernetesAuth)
.whereIn(
"id",
batch.map((row) => row.id)
)
.update({ kubernetesHost: "" });
}
}
if (hasKubernetesHostColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.string("kubernetesHost").notNullable().alter();
});
}
}

View File

@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityAliCloudAuth))) {
await knex.schema.createTable(TableName.IdentityAliCloudAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
t.jsonb("accessTokenTrustedIps").notNullable();
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("type").notNullable();
t.string("allowedArns").notNullable();
});
}
await createOnUpdateTrigger(knex, TableName.IdentityAliCloudAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityAliCloudAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityAliCloudAuth);
}

View File

@@ -16,7 +16,8 @@ export const DynamicSecretLeasesSchema = z.object({
statusDetails: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(),
dynamicSecretId: z.string().uuid(), dynamicSecretId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
config: z.unknown().nullable().optional()
}); });
export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>; export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>;

View File

@@ -0,0 +1,25 @@
// 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 IdentityAlicloudAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
type: z.string(),
allowedArns: z.string()
});
export type TIdentityAlicloudAuths = z.infer<typeof IdentityAlicloudAuthsSchema>;
export type TIdentityAlicloudAuthsInsert = Omit<z.input<typeof IdentityAlicloudAuthsSchema>, TImmutableDBKeys>;
export type TIdentityAlicloudAuthsUpdate = Partial<Omit<z.input<typeof IdentityAlicloudAuthsSchema>, TImmutableDBKeys>>;

View File

@@ -18,7 +18,7 @@ export const IdentityKubernetesAuthsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
identityId: z.string().uuid(), identityId: z.string().uuid(),
kubernetesHost: z.string(), kubernetesHost: z.string().nullable().optional(),
encryptedCaCert: z.string().nullable().optional(), encryptedCaCert: z.string().nullable().optional(),
caCertIV: z.string().nullable().optional(), caCertIV: z.string().nullable().optional(),
caCertTag: z.string().nullable().optional(), caCertTag: z.string().nullable().optional(),

View File

@@ -39,6 +39,7 @@ export * from "./group-project-memberships";
export * from "./groups"; export * from "./groups";
export * from "./identities"; export * from "./identities";
export * from "./identity-access-tokens"; export * from "./identity-access-tokens";
export * from "./identity-alicloud-auths";
export * from "./identity-aws-auths"; export * from "./identity-aws-auths";
export * from "./identity-azure-auths"; export * from "./identity-azure-auths";
export * from "./identity-gcp-auths"; export * from "./identity-gcp-auths";

View File

@@ -80,6 +80,7 @@ export enum TableName {
IdentityGcpAuth = "identity_gcp_auths", IdentityGcpAuth = "identity_gcp_auths",
IdentityAzureAuth = "identity_azure_auths", IdentityAzureAuth = "identity_azure_auths",
IdentityUaClientSecret = "identity_ua_client_secrets", IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAliCloudAuth = "identity_alicloud_auths",
IdentityAwsAuth = "identity_aws_auths", IdentityAwsAuth = "identity_aws_auths",
IdentityOciAuth = "identity_oci_auths", IdentityOciAuth = "identity_oci_auths",
IdentityOidcAuth = "identity_oidc_auths", IdentityOidcAuth = "identity_oidc_auths",
@@ -247,6 +248,7 @@ export enum IdentityAuthMethod {
UNIVERSAL_AUTH = "universal-auth", UNIVERSAL_AUTH = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth", KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth", GCP_AUTH = "gcp-auth",
ALICLOUD_AUTH = "alicloud-auth",
AWS_AUTH = "aws-auth", AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth", AZURE_AUTH = "azure-auth",
OCI_AUTH = "oci-auth", OCI_AUTH = "oci-auth",

View File

@@ -0,0 +1,17 @@
import {
CreateOracleDBConnectionSchema,
SanitizedOracleDBConnectionSchema,
UpdateOracleDBConnectionSchema
} from "@app/ee/services/app-connections/oracledb";
import { registerAppConnectionEndpoints } from "@app/server/routes/v1/app-connection-routers/app-connection-endpoints";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const registerOracleDBConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.OracleDB,
server,
sanitizedResponseSchema: SanitizedOracleDBConnectionSchema,
createSchema: CreateOracleDBConnectionSchema,
updateSchema: UpdateOracleDBConnectionSchema
});
};

View File

@@ -36,7 +36,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}), }),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path), path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.path) environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
config: z.any().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -0,0 +1,67 @@
import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerKubernetesDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.DynamicSecrets],
body: z.object({
dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(),
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug),
ttl: z
.string()
.optional()
.describe(DYNAMIC_SECRET_LEASES.CREATE.ttl)
.superRefine((val, ctx) => {
if (!val) return;
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
config: z
.object({
namespace: z.string().min(1).optional().describe(DYNAMIC_SECRET_LEASES.KUBERNETES.CREATE.config.namespace)
})
.optional()
}),
response: {
200: z.object({
lease: DynamicSecretLeasesSchema,
dynamicSecret: SanitizedDynamicSecretSchema,
data: z.unknown()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { data, lease, dynamicSecret } = await server.services.dynamicSecretLease.create({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.body.dynamicSecretName,
...req.body
});
return { lease, data, dynamicSecret };
}
});
};

View File

@@ -48,7 +48,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(GROUPS.GET_BY_ID.id) id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
}), }),
response: { response: {
200: GroupsSchema 200: GroupsSchema.extend({
customRoleSlug: z.string().nullable()
})
} }
}, },
handler: async (req) => { handler: async (req) => {

View File

@@ -6,6 +6,7 @@ import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerCaCrlRouter } from "./certificate-authority-crl-router"; import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router"; import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGatewayRouter } from "./gateway-router"; import { registerGatewayRouter } from "./gateway-router";
@@ -71,6 +72,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
async (dynamicSecretRouter) => { async (dynamicSecretRouter) => {
await dynamicSecretRouter.register(registerDynamicSecretRouter); await dynamicSecretRouter.register(registerDynamicSecretRouter);
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" }); await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
await dynamicSecretRouter.register(registerKubernetesDynamicSecretLeaseRouter, { prefix: "/leases/kubernetes" });
}, },
{ prefix: "/dynamic-secrets" } { prefix: "/dynamic-secrets" }
); );

View File

@@ -6,6 +6,7 @@ import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-r
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router"; import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router"; import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router";
import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
export * from "./secret-rotation-v2-router"; export * from "./secret-rotation-v2-router";
@@ -17,6 +18,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter, [SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter, [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.MySqlCredentials]: registerMySqlCredentialsRotationRouter, [SecretRotation.MySqlCredentials]: registerMySqlCredentialsRotationRouter,
[SecretRotation.OracleDBCredentials]: registerOracleDBCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter, [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter, [SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter, [SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,

View File

@@ -0,0 +1,19 @@
import {
CreateOracleDBCredentialsRotationSchema,
OracleDBCredentialsRotationSchema,
UpdateOracleDBCredentialsRotationSchema
} from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerOracleDBCredentialsRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.OracleDBCredentials,
server,
responseSchema: OracleDBCredentialsRotationSchema,
createSchema: CreateOracleDBCredentialsRotationSchema,
updateSchema: UpdateOracleDBCredentialsRotationSchema,
generatedCredentialsSchema: SqlCredentialsRotationGeneratedCredentialsSchema
});

View File

@@ -7,6 +7,7 @@ import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs"; import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs";
@@ -18,6 +19,7 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema, PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema, MsSqlCredentialsRotationListItemSchema,
MySqlCredentialsRotationListItemSchema, MySqlCredentialsRotationListItemSchema,
OracleDBCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema, Auth0ClientSecretRotationListItemSchema,
AzureClientSecretRotationListItemSchema, AzureClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema, AwsIamUserSecretRotationListItemSchema,

View File

@@ -187,6 +187,56 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
} }
}); });
server.route({
method: "PATCH",
url: "/findings",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.SecretScanning],
description: "Update one or more Secret Scanning Findings in a batch.",
body: z
.object({
findingId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningFindings.UPDATE.findingId),
status: z.nativeEnum(SecretScanningFindingStatus).optional().describe(SecretScanningFindings.UPDATE.status),
remarks: z.string().nullish().describe(SecretScanningFindings.UPDATE.remarks)
})
.array()
.max(500),
response: {
200: z.object({ findings: SecretScanningFindingSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { body, permission } = req;
const updatedFindingPromises = body.map(async (findingUpdatePayload) => {
const { finding, projectId } = await server.services.secretScanningV2.updateSecretScanningFindingById(
findingUpdatePayload,
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.SECRET_SCANNING_FINDING_UPDATE,
metadata: findingUpdatePayload
}
});
return finding;
});
const findings = await Promise.all(updatedFindingPromises);
return { findings };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/configs", url: "/configs",

View File

@@ -0,0 +1,4 @@
export * from "./oracledb-connection-enums";
export * from "./oracledb-connection-fns";
export * from "./oracledb-connection-schemas";
export * from "./oracledb-connection-types";

View File

@@ -0,0 +1,3 @@
export enum OracleDBConnectionMethod {
UsernameAndPassword = "username-and-password"
}

View File

@@ -0,0 +1,12 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { OracleDBConnectionMethod } from "./oracledb-connection-enums";
export const getOracleDBConnectionListItem = () => {
return {
name: "OracleDB" as const,
app: AppConnection.OracleDB as const,
methods: Object.values(OracleDBConnectionMethod) as [OracleDBConnectionMethod.UsernameAndPassword],
supportsPlatformManagement: true as const
};
};

View File

@@ -0,0 +1,64 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { BaseSqlUsernameAndPasswordConnectionSchema } from "@app/services/app-connection/shared/sql";
import { OracleDBConnectionMethod } from "./oracledb-connection-enums";
export const OracleDBConnectionCredentialsSchema = BaseSqlUsernameAndPasswordConnectionSchema;
const BaseOracleDBConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.OracleDB) });
export const OracleDBConnectionSchema = BaseOracleDBConnectionSchema.extend({
method: z.literal(OracleDBConnectionMethod.UsernameAndPassword),
credentials: OracleDBConnectionCredentialsSchema
});
export const SanitizedOracleDBConnectionSchema = z.discriminatedUnion("method", [
BaseOracleDBConnectionSchema.extend({
method: z.literal(OracleDBConnectionMethod.UsernameAndPassword),
credentials: OracleDBConnectionCredentialsSchema.pick({
host: true,
database: true,
port: true,
username: true,
sslEnabled: true,
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);
export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(OracleDBConnectionMethod.UsernameAndPassword)
.describe(AppConnections.CREATE(AppConnection.OracleDB).method),
credentials: OracleDBConnectionCredentialsSchema.describe(AppConnections.CREATE(AppConnection.OracleDB).credentials)
})
]);
export const CreateOracleDBConnectionSchema = ValidateOracleDBConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true })
);
export const UpdateOracleDBConnectionSchema = z
.object({
credentials: OracleDBConnectionCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.OracleDB).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true }));
export const OracleDBConnectionListItemSchema = z.object({
name: z.literal("OracleDB"),
app: z.literal(AppConnection.OracleDB),
methods: z.nativeEnum(OracleDBConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
});

View File

@@ -0,0 +1,17 @@
import z from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateOracleDBConnectionSchema,
OracleDBConnectionSchema,
ValidateOracleDBConnectionCredentialsSchema
} from "./oracledb-connection-schemas";
export type TOracleDBConnection = z.infer<typeof OracleDBConnectionSchema>;
export type TOracleDBConnectionInput = z.infer<typeof CreateOracleDBConnectionSchema> & {
app: AppConnection.OracleDB;
};
export type TValidateOracleDBConnectionCredentialsSchema = typeof ValidateOracleDBConnectionCredentialsSchema;

View File

@@ -170,6 +170,12 @@ export enum EventType {
REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth", REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth",
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth", GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth",
LOGIN_IDENTITY_ALICLOUD_AUTH = "login-identity-alicloud-auth",
ADD_IDENTITY_ALICLOUD_AUTH = "add-identity-alicloud-auth",
UPDATE_IDENTITY_ALICLOUD_AUTH = "update-identity-alicloud-auth",
REVOKE_IDENTITY_ALICLOUD_AUTH = "revoke-identity-alicloud-auth",
GET_IDENTITY_ALICLOUD_AUTH = "get-identity-alicloud-auth",
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth", LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth", ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth", UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
@@ -1060,6 +1066,53 @@ interface GetIdentityAwsAuthEvent {
}; };
} }
interface LoginIdentityAliCloudAuthEvent {
type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH;
metadata: {
identityId: string;
identityAliCloudAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityAliCloudAuthEvent {
type: EventType.ADD_IDENTITY_ALICLOUD_AUTH;
metadata: {
identityId: string;
allowedArns: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityAliCloudAuthEvent {
type: EventType.REVOKE_IDENTITY_ALICLOUD_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityAliCloudAuthEvent {
type: EventType.UPDATE_IDENTITY_ALICLOUD_AUTH;
metadata: {
identityId: string;
allowedArns: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityAliCloudAuthEvent {
type: EventType.GET_IDENTITY_ALICLOUD_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityOciAuthEvent { interface LoginIdentityOciAuthEvent {
type: EventType.LOGIN_IDENTITY_OCI_AUTH; type: EventType.LOGIN_IDENTITY_OCI_AUTH;
metadata: { metadata: {
@@ -3272,6 +3325,11 @@ export type Event =
| UpdateIdentityAwsAuthEvent | UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent | GetIdentityAwsAuthEvent
| DeleteIdentityAwsAuthEvent | DeleteIdentityAwsAuthEvent
| LoginIdentityAliCloudAuthEvent
| AddIdentityAliCloudAuthEvent
| UpdateIdentityAliCloudAuthEvent
| GetIdentityAliCloudAuthEvent
| DeleteIdentityAliCloudAuthEvent
| LoginIdentityOciAuthEvent | LoginIdentityOciAuthEvent
| AddIdentityOciAuthEvent | AddIdentityOciAuthEvent
| UpdateIdentityOciAuthEvent | UpdateIdentityOciAuthEvent

View File

@@ -10,6 +10,7 @@ import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models"; import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal"; import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types";
type TDynamicSecretLeaseQueueServiceFactoryDep = { type TDynamicSecretLeaseQueueServiceFactoryDep = {
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
@@ -134,10 +135,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
await Promise.all( await Promise.all(
dynamicSecretLeases.map(({ externalEntityId }) => dynamicSecretLeases.map(({ externalEntityId, config }) =>
selectedProvider.revoke(decryptedStoredInput, externalEntityId, { selectedProvider.revoke(
decryptedStoredInput,
externalEntityId,
{
projectId: folder.projectId projectId: folder.projectId
}) },
config as TDynamicSecretLeaseConfig
)
) )
); );
} }

View File

@@ -29,6 +29,7 @@ import {
TCreateDynamicSecretLeaseDTO, TCreateDynamicSecretLeaseDTO,
TDeleteDynamicSecretLeaseDTO, TDeleteDynamicSecretLeaseDTO,
TDetailsDynamicSecretLeaseDTO, TDetailsDynamicSecretLeaseDTO,
TDynamicSecretLeaseConfig,
TListDynamicSecretLeasesDTO, TListDynamicSecretLeasesDTO,
TRenewDynamicSecretLeaseDTO TRenewDynamicSecretLeaseDTO
} from "./dynamic-secret-lease-types"; } from "./dynamic-secret-lease-types";
@@ -77,7 +78,8 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
ttl ttl,
config
}: TCreateDynamicSecretLeaseDTO) => { }: TCreateDynamicSecretLeaseDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
@@ -163,7 +165,8 @@ export const dynamicSecretLeaseServiceFactory = ({
expireAt: expireAt.getTime(), expireAt: expireAt.getTime(),
usernameTemplate: dynamicSecretCfg.usernameTemplate, usernameTemplate: dynamicSecretCfg.usernameTemplate,
identity, identity,
metadata: { projectId } metadata: { projectId },
config
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) { if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
@@ -177,8 +180,10 @@ export const dynamicSecretLeaseServiceFactory = ({
expireAt, expireAt,
version: 1, version: 1,
dynamicSecretId: dynamicSecretCfg.id, dynamicSecretId: dynamicSecretCfg.id,
externalEntityId: entityId externalEntityId: entityId,
config
}); });
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date())); await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data }; return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
}; };
@@ -259,7 +264,10 @@ export const dynamicSecretLeaseServiceFactory = ({
const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL)); const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
if (maxTTL) { if (maxTTL) {
const maxExpiryDate = new Date(dynamicSecretLease.createdAt.getTime() + ms(maxTTL)); const maxExpiryDate = new Date(dynamicSecretLease.createdAt.getTime() + ms(maxTTL));
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max ttl" }); if (expireAt > maxExpiryDate)
throw new BadRequestError({
message: "The requested renewal would exceed the maximum allowed lease duration. Please choose a shorter TTL"
});
} }
const { entityId } = await selectedProvider.renew( const { entityId } = await selectedProvider.renew(
@@ -342,7 +350,12 @@ export const dynamicSecretLeaseServiceFactory = ({
) as object; ) as object;
const revokeResponse = await selectedProvider const revokeResponse = await selectedProvider
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId }) .revoke(
decryptedStoredInput,
dynamicSecretLease.externalEntityId,
{ projectId },
dynamicSecretLease.config as TDynamicSecretLeaseConfig
)
.catch(async (err) => { .catch(async (err) => {
// only propogate this error if forced is false // only propogate this error if forced is false
if (!isForced) return { error: err as Error }; if (!isForced) return { error: err as Error };

View File

@@ -10,6 +10,7 @@ export type TCreateDynamicSecretLeaseDTO = {
environmentSlug: string; environmentSlug: string;
ttl?: string; ttl?: string;
projectSlug: string; projectSlug: string;
config?: TDynamicSecretLeaseConfig;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDetailsDynamicSecretLeaseDTO = { export type TDetailsDynamicSecretLeaseDTO = {
@@ -41,3 +42,9 @@ export type TRenewDynamicSecretLeaseDTO = {
ttl?: string; ttl?: string;
projectSlug: string; projectSlug: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDynamicSecretKubernetesLeaseConfig = {
namespace?: string;
};
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;

View File

@@ -0,0 +1,105 @@
import { gaxios, Impersonated, JWT } from "google-auth-library";
import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretGcpIamSchema, TDynamicProviderFns } from "./models";
export const GcpIamProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretGcpIamSchema.parseAsync(inputs);
return providerInputs;
};
const $getToken = async (serviceAccountEmail: string, ttl: number): Promise<string> => {
const appCfg = getConfig();
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
throw new InternalServerError({
message: "Environment variable has not been configured: INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL"
});
}
const credJson = JSON.parse(appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) as {
client_email: string;
private_key: string;
};
const sourceClient = new JWT({
email: credJson.client_email,
key: credJson.private_key,
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
});
const impersonatedCredentials = new Impersonated({
sourceClient,
targetPrincipal: serviceAccountEmail,
lifetime: ttl,
delegates: [],
targetScopes: ["https://www.googleapis.com/auth/iam", "https://www.googleapis.com/auth/cloud-platform"]
});
let tokenResponse: GetAccessTokenResponse | undefined;
try {
tokenResponse = await impersonatedCredentials.getAccessToken();
} catch (error) {
let message = "Unable to validate connection";
if (error instanceof gaxios.GaxiosError) {
message = error.message;
}
throw new BadRequestError({
message
});
}
if (!tokenResponse || !tokenResponse.token) {
throw new BadRequestError({
message: "Unable to validate connection"
});
}
return tokenResponse.token;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await $getToken(providerInputs.serviceAccountEmail, 10);
return true;
};
const create = async (data: { inputs: unknown; expireAt: number }) => {
const { inputs, expireAt } = data;
const providerInputs = await validateProviderInputs(inputs);
const now = Math.floor(Date.now() / 1000);
const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0);
const token = await $getToken(providerInputs.serviceAccountEmail, ttl);
const entityId = alphaNumericNanoId(32);
return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } };
};
const revoke = async (_inputs: unknown, entityId: string) => {
// There's no way to revoke GCP IAM access tokens
return { entityId };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
// To renew a token it must be re-created
const data = await create({ inputs, expireAt });
return { ...data, entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@@ -6,6 +6,7 @@ import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id"; import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra"; import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search"; import { ElasticSearchProvider } from "./elastic-search";
import { GcpIamProvider } from "./gcp-iam";
import { KubernetesProvider } from "./kubernetes"; import { KubernetesProvider } from "./kubernetes";
import { LdapProvider } from "./ldap"; import { LdapProvider } from "./ldap";
import { DynamicSecretProviders, TDynamicProviderFns } from "./models"; import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
@@ -42,5 +43,6 @@ export const buildDynamicSecretProviders = ({
[DynamicSecretProviders.Totp]: TotpProvider(), [DynamicSecretProviders.Totp]: TotpProvider(),
[DynamicSecretProviders.SapAse]: SapAseProvider(), [DynamicSecretProviders.SapAse]: SapAseProvider(),
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }), [DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }) [DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
[DynamicSecretProviders.GcpIam]: GcpIamProvider()
}); });

View File

@@ -1,13 +1,14 @@
import axios from "axios"; import axios, { AxiosError } from "axios";
import handlebars from "handlebars"; import handlebars from "handlebars";
import https from "https"; import https from "https";
import { InternalServerError } from "@app/lib/errors"; import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { import {
DynamicSecretKubernetesSchema, DynamicSecretKubernetesSchema,
@@ -19,6 +20,9 @@ import {
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
// This value is just a placeholder. When using gateway auth method, the url is irrelevant.
const GATEWAY_AUTH_DEFAULT_URL = "https://kubernetes.default.svc.cluster.local";
type TKubernetesProviderDTO = { type TKubernetesProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
@@ -36,7 +40,7 @@ const generateUsername = (usernameTemplate?: string | null) => {
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => { export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => { const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs); const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
if (!providerInputs.gatewayId) { if (!providerInputs.gatewayId && providerInputs.url) {
await blockLocalAndPrivateIpAddresses(providerInputs.url); await blockLocalAndPrivateIpAddresses(providerInputs.url);
} }
@@ -103,37 +107,46 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const serviceAccountName = generateUsername(); const serviceAccountName = generateUsername();
const roleBindingName = `${serviceAccountName}-role-binding`; const roleBindingName = `${serviceAccountName}-role-binding`;
const namespaces = providerInputs.namespace.split(",").map((namespace) => namespace.trim());
// Test each namespace sequentially instead of in parallel to simplify cleanup
for await (const namespace of namespaces) {
try {
// 1. Create a test service account // 1. Create a test service account
await axios.post( await axios.post(
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`, `${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts`,
{ {
metadata: { metadata: {
name: serviceAccountName, name: serviceAccountName,
namespace: providerInputs.namespace namespace
} }
}, },
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
// 2. Create a test role binding // 2. Create a test role binding
const roleBindingUrl = const roleBindingUrl =
providerInputs.roleType === KubernetesRoleType.ClusterRole providerInputs.roleType === KubernetesRoleType.ClusterRole
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings` ? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`; : `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings`;
const roleBindingMetadata = { const roleBindingMetadata = {
name: roleBindingName, name: roleBindingName,
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace }) ...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace })
}; };
await axios.post( await axios.post(
@@ -149,7 +162,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
{ {
kind: "ServiceAccount", kind: "ServiceAccount",
name: serviceAccountName, name: serviceAccountName,
namespace: providerInputs.namespace namespace
} }
] ]
}, },
@@ -157,18 +170,22 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
// 3. Request a token for the test service account // 3. Request a token for the test service account
await axios.post( await axios.post(
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`, `${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}/token`,
{ {
spec: { spec: {
expirationSeconds: 600, // 10 minutes expirationSeconds: 600, // 10 minutes
@@ -179,59 +196,84 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
// 4. Cleanup: delete role binding and service account // 4. Cleanup: delete role binding and service account
if (providerInputs.roleType === KubernetesRoleType.Role) { if (providerInputs.roleType === KubernetesRoleType.Role) {
await axios.delete( await axios.delete(
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`, `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings/${roleBindingName}`,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
} else { } else {
await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, { await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
}
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}); });
} }
await axios.delete( await axios.delete(`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}`, {
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}`,
{
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
); : {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
});
} catch (error) {
const cleanupInfo = `You may need to manually clean up the following resources in namespace "${namespace}": Service Account - ${serviceAccountName}, ${providerInputs.roleType === KubernetesRoleType.Role ? "Role" : "Cluster Role"} Binding - ${roleBindingName}.`;
let mainErrorMessage = "Unknown error";
if (error instanceof AxiosError) {
mainErrorMessage = (error.response?.data as { message: string })?.message;
} else if (error instanceof Error) {
mainErrorMessage = error.message;
}
throw new Error(`${mainErrorMessage}. ${cleanupInfo}`);
}
}
}; };
const serviceAccountStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => { const serviceAccountStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
@@ -247,17 +289,23 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
}; };
const url = new URL(providerInputs.url); const rawUrl =
providerInputs.authMethod === KubernetesAuthMethod.Gateway ? GATEWAY_AUTH_DEFAULT_URL : providerInputs.url || "";
const url = new URL(rawUrl);
const k8sGatewayHost = url.hostname; const k8sGatewayHost = url.hostname;
const k8sPort = url.port ? Number(url.port) : 443; const k8sPort = url.port ? Number(url.port) : 443;
const k8sHost = `${url.protocol}//${url.hostname}`; const k8sHost = `${url.protocol}//${url.hostname}`;
@@ -315,11 +363,13 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const create = async ({ const create = async ({
inputs, inputs,
expireAt, expireAt,
usernameTemplate usernameTemplate,
config
}: { }: {
inputs: unknown; inputs: unknown;
expireAt: number; expireAt: number;
usernameTemplate?: string | null; usernameTemplate?: string | null;
config?: TDynamicSecretKubernetesLeaseConfig;
}) => { }) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
@@ -331,38 +381,56 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const baseUrl = port ? `${host}:${port}` : host; const baseUrl = port ? `${host}:${port}` : host;
const serviceAccountName = generateUsername(usernameTemplate); const serviceAccountName = generateUsername(usernameTemplate);
const roleBindingName = `${serviceAccountName}-role-binding`; const roleBindingName = `${serviceAccountName}-role-binding`;
const allowedNamespaces = providerInputs.namespace.split(",").map((namespace) => namespace.trim());
if (config?.namespace && !allowedNamespaces?.includes(config?.namespace)) {
throw new BadRequestError({
message: `Namespace ${config?.namespace} is not allowed. Allowed namespaces: ${allowedNamespaces?.join(", ")}`
});
}
const namespace = config?.namespace || allowedNamespaces[0];
if (!namespace) {
throw new BadRequestError({
message: "No namespace provided"
});
}
// 1. Create the service account // 1. Create the service account
await axios.post( await axios.post(
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`, `${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts`,
{ {
metadata: { metadata: {
name: serviceAccountName, name: serviceAccountName,
namespace: providerInputs.namespace namespace
} }
}, },
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
// 2. Create the role binding // 2. Create the role binding
const roleBindingUrl = const roleBindingUrl =
providerInputs.roleType === KubernetesRoleType.ClusterRole providerInputs.roleType === KubernetesRoleType.ClusterRole
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings` ? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`; : `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings`;
const roleBindingMetadata = { const roleBindingMetadata = {
name: roleBindingName, name: roleBindingName,
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace }) ...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace })
}; };
await axios.post( await axios.post(
@@ -378,7 +446,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
{ {
kind: "ServiceAccount", kind: "ServiceAccount",
name: serviceAccountName, name: serviceAccountName,
namespace: providerInputs.namespace namespace
} }
] ]
}, },
@@ -386,18 +454,22 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
// 3. Request a token for the service account // 3. Request a token for the service account
const res = await axios.post<TKubernetesTokenRequest>( const res = await axios.post<TKubernetesTokenRequest>(
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`, `${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}/token`,
{ {
spec: { spec: {
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000), expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
@@ -408,13 +480,17 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
return { ...res.data, serviceAccountName }; return { ...res.data, serviceAccountName };
@@ -425,6 +501,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
throw new Error("invalid callback"); throw new Error("invalid callback");
} }
if (config?.namespace && config.namespace !== providerInputs.namespace) {
throw new BadRequestError({
message: `Namespace ${config?.namespace} is not allowed. Allowed namespace: ${providerInputs.namespace}.`
});
}
const baseUrl = port ? `${host}:${port}` : host; const baseUrl = port ? `${host}:${port}` : host;
const res = await axios.post<TKubernetesTokenRequest>( const res = await axios.post<TKubernetesTokenRequest>(
@@ -439,19 +521,25 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
return { ...res.data, serviceAccountName: providerInputs.serviceAccountName }; return { ...res.data, serviceAccountName: providerInputs.serviceAccountName };
}; };
const url = new URL(providerInputs.url); const rawUrl =
providerInputs.authMethod === KubernetesAuthMethod.Gateway ? GATEWAY_AUTH_DEFAULT_URL : providerInputs.url || "";
const url = new URL(rawUrl);
const k8sHost = `${url.protocol}//${url.hostname}`; const k8sHost = `${url.protocol}//${url.hostname}`;
const k8sGatewayHost = url.hostname; const k8sGatewayHost = url.hostname;
const k8sPort = url.port ? Number(url.port) : 443; const k8sPort = url.port ? Number(url.port) : 443;
@@ -511,7 +599,13 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
} }
}; };
const revoke = async (inputs: unknown, entityId: string) => { const revoke = async (
inputs: unknown,
entityId: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_metadata: { projectId: string },
config?: TDynamicSecretKubernetesLeaseConfig
) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => { const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
@@ -522,51 +616,70 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const baseUrl = port ? `${host}:${port}` : host; const baseUrl = port ? `${host}:${port}` : host;
const roleBindingName = `${entityId}-role-binding`; const roleBindingName = `${entityId}-role-binding`;
const namespace = config?.namespace ?? providerInputs.namespace.split(",")[0].trim();
if (providerInputs.roleType === KubernetesRoleType.Role) { if (providerInputs.roleType === KubernetesRoleType.Role) {
await axios.delete( await axios.delete(
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`, `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings/${roleBindingName}`,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
} }
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}
); );
} else { } else {
await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, { await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
}
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}); });
} }
// Delete the service account // Delete the service account
await axios.delete(`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${entityId}`, { await axios.delete(`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${entityId}`, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway ...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken } ? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
: { Authorization: `Bearer ${providerInputs.clusterToken}` }) : { Authorization: `Bearer ${providerInputs.clusterToken}` })
}, },
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT), ...(providerInputs.authMethod === KubernetesAuthMethod.Api
timeout: EXTERNAL_REQUEST_TIMEOUT, ? {
httpsAgent httpsAgent
}
: {}),
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
timeout: EXTERNAL_REQUEST_TIMEOUT
}); });
}; };
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) { if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
const url = new URL(providerInputs.url); const rawUrl =
providerInputs.authMethod === KubernetesAuthMethod.Gateway
? GATEWAY_AUTH_DEFAULT_URL
: providerInputs.url || "";
const url = new URL(rawUrl);
const k8sGatewayHost = url.hostname; const k8sGatewayHost = url.hostname;
const k8sPort = url.port ? Number(url.port) : 443; const k8sPort = url.port ? Number(url.port) : 443;
const k8sHost = `${url.protocol}//${url.hostname}`; const k8sHost = `${url.protocol}//${url.hostname}`;

View File

@@ -1,5 +1,10 @@
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
export type PasswordRequirements = { export type PasswordRequirements = {
length: number; length: number;
required: { required: {
@@ -323,24 +328,54 @@ export const LdapSchema = z.union([
export const DynamicSecretKubernetesSchema = z export const DynamicSecretKubernetesSchema = z
.discriminatedUnion("credentialType", [ .discriminatedUnion("credentialType", [
z.object({ z.object({
url: z.string().url().trim().min(1), url: z
.string()
.optional()
.refine((val: string | undefined) => !val || new RE2(/^https?:\/\/.+/).test(val), {
message: "Invalid URL. Must start with http:// or https:// (e.g. https://example.com)"
}),
clusterToken: z.string().trim().optional(), clusterToken: z.string().trim().optional(),
ca: z.string().optional(), ca: z.string().optional(),
sslEnabled: z.boolean().default(false), sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesCredentialType.Static), credentialType: z.literal(KubernetesCredentialType.Static),
serviceAccountName: z.string().trim().min(1), serviceAccountName: z.string().trim().min(1),
namespace: z.string().trim().min(1), namespace: z
.string()
.trim()
.min(1)
.refine((val) => !val.includes(","), "Namespace must be a single value, not a comma-separated list")
.refine(
(val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val),
"Invalid namespace format"
),
gatewayId: z.string().optional(), gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)), audiences: z.array(z.string().trim().min(1)),
authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api) authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api)
}), }),
z.object({ z.object({
url: z.string().url().trim().min(1), url: z
.string()
.url()
.optional()
.refine((val: string | undefined) => !val || new RE2(/^https?:\/\/.+/).test(val), {
message: "Invalid URL. Must start with http:// or https:// (e.g. https://example.com)"
}),
clusterToken: z.string().trim().optional(), clusterToken: z.string().trim().optional(),
ca: z.string().optional(), ca: z.string().optional(),
sslEnabled: z.boolean().default(false), sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesCredentialType.Dynamic), credentialType: z.literal(KubernetesCredentialType.Dynamic),
namespace: z.string().trim().min(1), namespace: z
.string()
.trim()
.min(1)
.refine((val) => {
const namespaces = val.split(",").map((ns) => ns.trim());
return (
namespaces.length > 0 &&
namespaces.every((ns) => ns.length > 0) &&
namespaces.every((ns) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(ns))
);
}, "Must be a valid comma-separated list of namespace values"),
gatewayId: z.string().optional(), gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)), audiences: z.array(z.string().trim().min(1)),
roleType: z.nativeEnum(KubernetesRoleType), roleType: z.nativeEnum(KubernetesRoleType),
@@ -356,13 +391,22 @@ export const DynamicSecretKubernetesSchema = z
message: "When auth method is set to Gateway, a gateway must be selected" message: "When auth method is set to Gateway, a gateway must be selected"
}); });
} }
if ((data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) && !data.clusterToken) { if (data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) {
if (!data.clusterToken) {
ctx.addIssue({ ctx.addIssue({
path: ["clusterToken"], path: ["clusterToken"],
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: "When auth method is set to Manual Token, a cluster token must be provided" message: "When auth method is set to Token, a cluster token must be provided"
}); });
} }
if (!data.url) {
ctx.addIssue({
path: ["url"],
code: z.ZodIssueCode.custom,
message: "When auth method is set to Token, a cluster URL must be provided"
});
}
}
}); });
export const DynamicSecretVerticaSchema = z.object({ export const DynamicSecretVerticaSchema = z.object({
@@ -426,6 +470,10 @@ export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
}) })
]); ]);
export const DynamicSecretGcpIamSchema = z.object({
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128)
});
export enum DynamicSecretProviders { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
Cassandra = "cassandra", Cassandra = "cassandra",
@@ -443,7 +491,8 @@ export enum DynamicSecretProviders {
Totp = "totp", Totp = "totp",
SapAse = "sap-ase", SapAse = "sap-ase",
Kubernetes = "kubernetes", Kubernetes = "kubernetes",
Vertica = "vertica" Vertica = "vertica",
GcpIam = "gcp-iam"
} }
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -463,7 +512,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }), z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }), z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }), z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }) z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }),
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema })
]); ]);
export type TDynamicProviderFns = { export type TDynamicProviderFns = {
@@ -475,10 +525,16 @@ export type TDynamicProviderFns = {
name: string; name: string;
}; };
metadata: { projectId: string }; metadata: { projectId: string };
config?: TDynamicSecretLeaseConfig;
}) => Promise<{ entityId: string; data: unknown }>; }) => Promise<{ entityId: string; data: unknown }>;
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>; validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>; validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>; revoke: (
inputs: unknown,
entityId: string,
metadata: { projectId: string },
config?: TDynamicSecretLeaseConfig
) => Promise<{ entityId: string }>;
renew: ( renew: (
inputs: unknown, inputs: unknown,
entityId: string, entityId: string,

View File

@@ -169,11 +169,29 @@ export const groupDALFactory = (db: TDbClient) => {
} }
}; };
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.Groups)
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
.where(`${TableName.Groups}.id`, id)
.select(
selectAllTableCols(TableName.Groups),
db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles)
)
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "Find by id" });
}
};
return { return {
...groupOrm,
findGroups, findGroups,
findByOrgId, findByOrgId,
findAllGroupPossibleMembers, findAllGroupPossibleMembers,
findGroupsByProjectId, findGroupsByProjectId,
...groupOrm findById
}; };
}; };

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { import {
@@ -246,7 +247,7 @@ export const secretApprovalRequestServiceFactory = ({
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const { policy } = secretApprovalRequest; const { policy } = secretApprovalRequest;
const { hasRole } = await permissionService.getProjectPermission({ const { hasRole, permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -262,6 +263,12 @@ export const secretApprovalRequestServiceFactory = ({
throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
} }
const hasSecretReadAccess = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
const hiddenSecretValue = "******";
let secrets; let secrets;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
@@ -278,9 +285,9 @@ export const secretApprovalRequestServiceFactory = ({
version: el.version, version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO, secretMetadata: el.secretMetadata as ResourceMetadataDTO,
isRotatedSecret: el.secret?.isRotatedSecret ?? false, isRotatedSecret: el.secret?.isRotatedSecret ?? false,
secretValue: secretValue: !hasSecretReadAccess
// eslint-disable-next-line no-nested-ternary ? hiddenSecretValue
el.secret && el.secret.isRotatedSecret : el.secret && el.secret.isRotatedSecret
? undefined ? undefined
: el.encryptedValue : el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
@@ -293,7 +300,9 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.secret.key, secretKey: el.secret.key,
id: el.secret.id, id: el.secret.id,
version: el.secret.version, version: el.secret.version,
secretValue: el.secret.encryptedValue secretValue: !hasSecretReadAccess
? hiddenSecretValue
: el.secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
: "", : "",
secretComment: el.secret.encryptedComment secretComment: el.secret.encryptedComment
@@ -306,7 +315,9 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.secretVersion.key, secretKey: el.secretVersion.key,
id: el.secretVersion.id, id: el.secretVersion.id,
version: el.secretVersion.version, version: el.secretVersion.version,
secretValue: el.secretVersion.encryptedValue secretValue: !hasSecretReadAccess
? hiddenSecretValue
: el.secretVersion.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
: "", : "",
secretComment: el.secretVersion.encryptedComment secretComment: el.secretVersion.encryptedComment

View File

@@ -101,10 +101,56 @@ export const azureClientSecretRotationFactory: TRotationFactory<
} }
}; };
/**
* Checks if a credential with the given keyId exists.
*/
const credentialExists = async (keyId: string): Promise<boolean> => {
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/passwordCredentials`;
try {
const { data } = await request.get<{ value: Array<{ keyId: string }> }>(endpoint, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
});
return data.value?.some((credential) => credential.keyId === keyId) || false;
} catch (error: unknown) {
if (error instanceof AxiosError) {
let message;
if (
error.response?.data &&
typeof error.response.data === "object" &&
"error" in error.response.data &&
typeof (error.response.data as AzureErrorResponse).error.message === "string"
) {
message = (error.response.data as AzureErrorResponse).error.message;
}
throw new BadRequestError({
message: `Failed to check credential existence for app ${objectId}: ${
message || error.message || "Unknown error"
}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
};
/** /**
* Revokes a client secret from the Azure app using its keyId. * Revokes a client secret from the Azure app using its keyId.
* First checks if the credential exists before attempting revocation.
*/ */
const revokeCredential = async (keyId: string) => { const revokeCredential = async (keyId: string) => {
// Check if credential exists before attempting revocation
const exists = await credentialExists(keyId);
if (!exists) {
return; // Credential doesn't exist, nothing to revoke
}
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService); const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`; const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;

View File

@@ -0,0 +1,3 @@
export * from "./oracledb-credentials-rotation-constants";
export * from "./oracledb-credentials-rotation-schemas";
export * from "./oracledb-credentials-rotation-types";

View File

@@ -0,0 +1,20 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "OracleDB Credentials",
type: SecretRotation.OracleDBCredentials,
connection: AppConnection.OracleDB,
template: {
createUserStatement: `-- create user
CREATE USER INFISICAL_USER IDENTIFIED BY "temporary_password";
-- grant all privileges
GRANT ALL PRIVILEGES TO INFISICAL_USER;`,
secretsMapping: {
username: "ORACLEDB_USERNAME",
password: "ORACLEDB_PASSWORD"
}
}
};

View File

@@ -0,0 +1,41 @@
import { z } from "zod";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
BaseCreateSecretRotationSchema,
BaseSecretRotationSchema,
BaseUpdateSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import {
SqlCredentialsRotationParametersSchema,
SqlCredentialsRotationSecretsMappingSchema,
SqlCredentialsRotationTemplateSchema
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const OracleDBCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.OracleDBCredentials).extend({
type: z.literal(SecretRotation.OracleDBCredentials),
parameters: SqlCredentialsRotationParametersSchema,
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
});
export const CreateOracleDBCredentialsRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.OracleDBCredentials
).extend({
parameters: SqlCredentialsRotationParametersSchema,
secretsMapping: SqlCredentialsRotationSecretsMappingSchema
});
export const UpdateOracleDBCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.OracleDBCredentials
).extend({
parameters: SqlCredentialsRotationParametersSchema.optional(),
secretsMapping: SqlCredentialsRotationSecretsMappingSchema.optional()
});
export const OracleDBCredentialsRotationListItemSchema = z.object({
name: z.literal("OracleDB Credentials"),
connection: z.literal(AppConnection.OracleDB),
type: z.literal(SecretRotation.OracleDBCredentials),
template: SqlCredentialsRotationTemplateSchema
});

View File

@@ -0,0 +1,18 @@
import { z } from "zod";
import { TOracleDBConnection } from "../../app-connections/oracledb";
import {
CreateOracleDBCredentialsRotationSchema,
OracleDBCredentialsRotationListItemSchema,
OracleDBCredentialsRotationSchema
} from "./oracledb-credentials-rotation-schemas";
export type TOracleDBCredentialsRotation = z.infer<typeof OracleDBCredentialsRotationSchema>;
export type TOracleDBCredentialsRotationInput = z.infer<typeof CreateOracleDBCredentialsRotationSchema>;
export type TOracleDBCredentialsRotationListItem = z.infer<typeof OracleDBCredentialsRotationListItemSchema>;
export type TOracleDBCredentialsRotationWithConnection = TOracleDBCredentialsRotation & {
connection: TOracleDBConnection;
};

View File

@@ -2,6 +2,7 @@ export enum SecretRotation {
PostgresCredentials = "postgres-credentials", PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials", MsSqlCredentials = "mssql-credentials",
MySqlCredentials = "mysql-credentials", MySqlCredentials = "mysql-credentials",
OracleDBCredentials = "oracledb-credentials",
Auth0ClientSecret = "auth0-client-secret", Auth0ClientSecret = "auth0-client-secret",
AzureClientSecret = "azure-client-secret", AzureClientSecret = "azure-client-secret",
AwsIamUserSecret = "aws-iam-user-secret", AwsIamUserSecret = "aws-iam-user-secret",

View File

@@ -10,6 +10,7 @@ import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret"
import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password"; import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials"; import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
import { TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service"; import { TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service";
@@ -25,6 +26,7 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION, [SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION, [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MySqlCredentials]: MYSQL_CREDENTIALS_ROTATION_LIST_OPTION, [SecretRotation.MySqlCredentials]: MYSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.OracleDBCredentials]: ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION, [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION, [SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION, [SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,

View File

@@ -5,6 +5,7 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials", [SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
[SecretRotation.MySqlCredentials]: "MySQL Credentials", [SecretRotation.MySqlCredentials]: "MySQL Credentials",
[SecretRotation.OracleDBCredentials]: "OracleDB Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret", [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.AzureClientSecret]: "Azure Client Secret", [SecretRotation.AzureClientSecret]: "Azure Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret", [SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
@@ -15,6 +16,7 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql, [SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.MySqlCredentials]: AppConnection.MySql, [SecretRotation.MySqlCredentials]: AppConnection.MySql,
[SecretRotation.OracleDBCredentials]: AppConnection.OracleDB,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets, [SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS, [SecretRotation.AwsIamUserSecret]: AppConnection.AWS,

View File

@@ -123,6 +123,7 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, [SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MySqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, [SecretRotation.MySqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.OracleDBCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation, [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation, [SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation, [SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,

View File

@@ -45,6 +45,12 @@ import {
TMySqlCredentialsRotationListItem, TMySqlCredentialsRotationListItem,
TMySqlCredentialsRotationWithConnection TMySqlCredentialsRotationWithConnection
} from "./mysql-credentials"; } from "./mysql-credentials";
import {
TOracleDBCredentialsRotation,
TOracleDBCredentialsRotationInput,
TOracleDBCredentialsRotationListItem,
TOracleDBCredentialsRotationWithConnection
} from "./oracledb-credentials";
import { import {
TPostgresCredentialsRotation, TPostgresCredentialsRotation,
TPostgresCredentialsRotationInput, TPostgresCredentialsRotationInput,
@@ -58,6 +64,7 @@ export type TSecretRotationV2 =
| TPostgresCredentialsRotation | TPostgresCredentialsRotation
| TMsSqlCredentialsRotation | TMsSqlCredentialsRotation
| TMySqlCredentialsRotation | TMySqlCredentialsRotation
| TOracleDBCredentialsRotation
| TAuth0ClientSecretRotation | TAuth0ClientSecretRotation
| TAzureClientSecretRotation | TAzureClientSecretRotation
| TLdapPasswordRotation | TLdapPasswordRotation
@@ -67,6 +74,7 @@ export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection | TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection | TMsSqlCredentialsRotationWithConnection
| TMySqlCredentialsRotationWithConnection | TMySqlCredentialsRotationWithConnection
| TOracleDBCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection | TAuth0ClientSecretRotationWithConnection
| TAzureClientSecretRotationWithConnection | TAzureClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection | TLdapPasswordRotationWithConnection
@@ -83,6 +91,7 @@ export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput | TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput | TMsSqlCredentialsRotationInput
| TMySqlCredentialsRotationInput | TMySqlCredentialsRotationInput
| TOracleDBCredentialsRotationInput
| TAuth0ClientSecretRotationInput | TAuth0ClientSecretRotationInput
| TAzureClientSecretRotationInput | TAzureClientSecretRotationInput
| TLdapPasswordRotationInput | TLdapPasswordRotationInput
@@ -92,6 +101,7 @@ export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem | TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem | TMsSqlCredentialsRotationListItem
| TMySqlCredentialsRotationListItem | TMySqlCredentialsRotationListItem
| TOracleDBCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem | TAuth0ClientSecretRotationListItem
| TAzureClientSecretRotationListItem | TAzureClientSecretRotationListItem
| TLdapPasswordRotationListItem | TLdapPasswordRotationListItem

View File

@@ -1,18 +1,19 @@
import { z } from "zod"; import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { AwsIamUserSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret"; import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret";
export const SecretRotationV2Schema = z.discriminatedUnion("type", [ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema, PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema, MsSqlCredentialsRotationSchema,
MySqlCredentialsRotationSchema, MySqlCredentialsRotationSchema,
OracleDBCredentialsRotationSchema,
Auth0ClientSecretRotationSchema, Auth0ClientSecretRotationSchema,
AzureClientSecretRotationSchema, AzureClientSecretRotationSchema,
LdapPasswordRotationSchema, LdapPasswordRotationSchema,

View File

@@ -2,14 +2,15 @@ import { z } from "zod";
import { TMsSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { TMsSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { TMySqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; import { TMySqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { TOracleDBCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { TPostgresCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { TPostgresCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas";
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "./sql-credentials-rotation-schemas";
export type TSqlCredentialsRotationWithConnection = export type TSqlCredentialsRotationWithConnection =
| TPostgresCredentialsRotationWithConnection | TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection | TMsSqlCredentialsRotationWithConnection
| TMySqlCredentialsRotationWithConnection; | TMySqlCredentialsRotationWithConnection
| TOracleDBCredentialsRotationWithConnection;
export type TSqlCredentialsRotationGeneratedCredentials = z.infer< export type TSqlCredentialsRotationGeneratedCredentials = z.infer<
typeof SqlCredentialsRotationGeneratedCredentialsSchema typeof SqlCredentialsRotationGeneratedCredentialsSchema

View File

@@ -178,6 +178,13 @@ export const getDbSetQuery = (db: TDbProviderClients, variables: { username: str
}; };
} }
if (db === TDbProviderClients.OracleDB) {
return {
query: `ALTER USER ?? IDENTIFIED BY "${variables.password}"`,
variables: [variables.username]
};
}
// add more based on client // add more based on client
return { return {
query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`, query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,

View File

@@ -10,7 +10,8 @@ export enum TDbProviderClients {
// mysql and maria db // mysql and maria db
MySql = "mysql", MySql = "mysql",
MsSqlServer = "mssql" MsSqlServer = "mssql",
OracleDB = "oracledb"
} }
export enum TAwsProviderSystems { export enum TAwsProviderSystems {

View File

@@ -21,6 +21,7 @@ export enum ApiDocsTags {
TokenAuth = "Token Auth", TokenAuth = "Token Auth",
UniversalAuth = "Universal Auth", UniversalAuth = "Universal Auth",
GcpAuth = "GCP Auth", GcpAuth = "GCP Auth",
AliCloudAuth = "Alibaba Cloud Auth",
AwsAuth = "AWS Auth", AwsAuth = "AWS Auth",
OciAuth = "OCI Auth", OciAuth = "OCI Auth",
AzureAuth = "Azure Auth", AzureAuth = "Azure Auth",
@@ -243,6 +244,43 @@ export const LDAP_AUTH = {
} }
} as const; } as const;
export const ALICLOUD_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
Action: "The Alibaba Cloud API action. For STS GetCallerIdentity, this should be 'GetCallerIdentity'.",
Format: "The response format. For STS GetCallerIdentity, this should be 'JSON'.",
Version: "The API version. This should be in 'YYYY-MM-DD' format (e.g., '2015-04-01').",
AccessKeyId: "The AccessKey ID of the RAM user or STS token.",
SignatureMethod: "The signature algorithm. For STS GetCallerIdentity, this should be 'HMAC-SHA1'.",
Timestamp: "The timestamp of the request in UTC, formatted as 'YYYY-MM-DDTHH:mm:ssZ'.",
SignatureVersion: "The signature version. For STS GetCallerIdentity, this should be '1.0'.",
SignatureNonce: "A unique random string to prevent replay attacks.",
Signature: "The signature string calculated based on the request parameters and AccessKey Secret."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
}
} as const;
export const AWS_AUTH = { export const AWS_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login.", identityId: "The ID of the identity to login.",
@@ -1113,6 +1151,14 @@ export const DYNAMIC_SECRET_LEASES = {
leaseId: "The ID of the dynamic secret lease.", leaseId: "The ID of the dynamic secret lease.",
isForced: isForced:
"A boolean flag to delete the the dynamic secret from Infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally." "A boolean flag to delete the the dynamic secret from Infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
},
KUBERNETES: {
CREATE: {
config: {
namespace:
"The Kubernetes namespace to create the lease in. If not specified, the first namespace defined in the configuration will be used."
}
}
} }
} as const; } as const;
export const SECRET_TAGS = { export const SECRET_TAGS = {
@@ -2162,6 +2208,11 @@ export const AppConnections = {
code: "The OAuth code to use to connect with Azure Client Secrets.", code: "The OAuth code to use to connect with Azure Client Secrets.",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets." tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
}, },
AZURE_DEVOPS: {
code: "The OAuth code to use to connect with Azure DevOps.",
tenantId: "The Tenant ID to use to connect with Azure DevOps.",
orgName: "The Organization name to use to connect with Azure DevOps."
},
OCI: { OCI: {
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.", userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.", tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
@@ -2276,6 +2327,10 @@ export const SecretSyncs = {
"The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/", "The URL of the Azure App Configuration to sync secrets to. Example: https://example.azconfig.io/",
label: "An optional label to assign to secrets created in Azure App Configuration." label: "An optional label to assign to secrets created in Azure App Configuration."
}, },
AZURE_DEVOPS: {
devopsProjectId: "The ID of the Azure DevOps project to sync secrets to.",
devopsProjectName: "The name of the Azure DevOps project to sync secrets to."
},
GCP: { GCP: {
scope: "The Google project scope that secrets should be synced to.", scope: "The Google project scope that secrets should be synced to.",
projectId: "The ID of the Google project secrets should be synced to.", projectId: "The ID of the Google project secrets should be synced to.",

View File

@@ -149,8 +149,8 @@ const setupProxyServer = async ({
protocol = GatewayProxyProtocol.Tcp, protocol = GatewayProxyProtocol.Tcp,
httpsAgent httpsAgent
}: { }: {
targetHost: string; targetHost?: string;
targetPort: number; targetPort?: number;
relayPort: number; relayPort: number;
relayHost: string; relayHost: string;
tlsOptions: TGatewayTlsOptions; tlsOptions: TGatewayTlsOptions;
@@ -183,9 +183,19 @@ const setupProxyServer = async ({
let command: string; let command: string;
if (protocol === GatewayProxyProtocol.Http) { if (protocol === GatewayProxyProtocol.Http) {
if (!targetHost && !targetPort) {
command = `FORWARD-HTTP`;
logger.debug(`Using HTTP proxy mode, no target URL provided [command=${command.trim()}]`);
} else {
if (!targetHost || targetPort === undefined) {
throw new BadRequestError({
message: `Target host and port are required for HTTP proxy mode with custom target`
});
}
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http) const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
command = `FORWARD-HTTP ${targetUrl}`; command = `FORWARD-HTTP ${targetUrl}`;
logger.debug(`Using HTTP proxy mode: ${command.trim()}`); logger.debug(`Using HTTP proxy mode, custom target URL provided [command=${command.trim()}]`);
// extract ca certificate from httpsAgent if present // extract ca certificate from httpsAgent if present
if (httpsAgent && targetHost.startsWith("https://")) { if (httpsAgent && targetHost.startsWith("https://")) {
@@ -198,12 +208,19 @@ const setupProxyServer = async ({
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false; const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
command += ` verify=${rejectUnauthorized}`; command += ` verify=${rejectUnauthorized}`;
logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`); logger.debug(`Using HTTP proxy mode, custom target URL provided [command=${command.trim()}]`);
}
} }
} }
command += "\n"; command += "\n";
} else if (protocol === GatewayProxyProtocol.Tcp) { } else if (protocol === GatewayProxyProtocol.Tcp) {
if (!targetHost || !targetPort) {
throw new BadRequestError({
message: `Target host and port are required for TCP proxy mode`
});
}
// For TCP mode, send FORWARD-TCP with host:port // For TCP mode, send FORWARD-TCP with host:port
command = `FORWARD-TCP ${targetHost}:${targetPort}\n`; command = `FORWARD-TCP ${targetHost}:${targetPort}\n`;
logger.debug(`Using TCP proxy mode: ${command.trim()}`); logger.debug(`Using TCP proxy mode: ${command.trim()}`);

View File

@@ -10,12 +10,13 @@ export enum GatewayProxyProtocol {
} }
export enum GatewayHttpProxyActions { export enum GatewayHttpProxyActions {
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token" InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token",
UseGatewayK8sServiceAccount = "use-k8s-sa"
} }
export interface IGatewayProxyOptions { export interface IGatewayProxyOptions {
targetHost: string; targetHost?: string;
targetPort: number; targetPort?: number;
relayHost: string; relayHost: string;
relayPort: number; relayPort: number;
tlsOptions: TGatewayTlsOptions; tlsOptions: TGatewayTlsOptions;

View File

@@ -172,6 +172,8 @@ import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { identityServiceFactory } from "@app/services/identity/identity-service"; import { identityServiceFactory } from "@app/services/identity/identity-service";
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal"; import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal";
import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service";
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal"; import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal"; import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal";
@@ -383,6 +385,7 @@ export const registerRoutes = async (
const identityUaDAL = identityUaDALFactory(db); const identityUaDAL = identityUaDALFactory(db);
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db); const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAliCloudAuthDAL = identityAliCloudAuthDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const identityOciAuthDAL = identityOciAuthDALFactory(db); const identityOciAuthDAL = identityOciAuthDALFactory(db);
@@ -1482,6 +1485,14 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({
identityAccessTokenDAL,
identityAliCloudAuthDAL,
identityOrgMembershipDAL,
licenseService,
permissionService
});
const identityAwsAuthService = identityAwsAuthServiceFactory({ const identityAwsAuthService = identityAwsAuthServiceFactory({
identityAccessTokenDAL, identityAccessTokenDAL,
identityAwsAuthDAL, identityAwsAuthDAL,
@@ -1931,6 +1942,7 @@ export const registerRoutes = async (
identityUa: identityUaService, identityUa: identityUaService,
identityKubernetesAuth: identityKubernetesAuthService, identityKubernetesAuth: identityKubernetesAuthService,
identityGcpAuth: identityGcpAuthService, identityGcpAuth: identityGcpAuthService,
identityAliCloudAuth: identityAliCloudAuthService,
identityAwsAuth: identityAwsAuthService, identityAwsAuth: identityAwsAuthService,
identityAzureAuth: identityAzureAuthService, identityAzureAuth: identityAzureAuthService,
identityOciAuth: identityOciAuthService, identityOciAuth: identityOciAuthService,

View File

@@ -1,6 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci"; import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci";
import {
OracleDBConnectionListItemSchema,
SanitizedOracleDBConnectionSchema
} from "@app/ee/services/app-connections/oracledb";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs"; import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
@@ -19,6 +23,10 @@ import {
AzureClientSecretsConnectionListItemSchema, AzureClientSecretsConnectionListItemSchema,
SanitizedAzureClientSecretsConnectionSchema SanitizedAzureClientSecretsConnectionSchema
} from "@app/services/app-connection/azure-client-secrets"; } from "@app/services/app-connection/azure-client-secrets";
import {
AzureDevOpsConnectionListItemSchema,
SanitizedAzureDevOpsConnectionSchema
} from "@app/services/app-connection/azure-devops/azure-devops-schemas";
import { import {
AzureKeyVaultConnectionListItemSchema, AzureKeyVaultConnectionListItemSchema,
SanitizedAzureKeyVaultConnectionSchema SanitizedAzureKeyVaultConnectionSchema
@@ -75,6 +83,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedGcpConnectionSchema.options, ...SanitizedGcpConnectionSchema.options,
...SanitizedAzureKeyVaultConnectionSchema.options, ...SanitizedAzureKeyVaultConnectionSchema.options,
...SanitizedAzureAppConfigurationConnectionSchema.options, ...SanitizedAzureAppConfigurationConnectionSchema.options,
...SanitizedAzureDevOpsConnectionSchema.options,
...SanitizedDatabricksConnectionSchema.options, ...SanitizedDatabricksConnectionSchema.options,
...SanitizedHumanitecConnectionSchema.options, ...SanitizedHumanitecConnectionSchema.options,
...SanitizedTerraformCloudConnectionSchema.options, ...SanitizedTerraformCloudConnectionSchema.options,
@@ -90,6 +99,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedLdapConnectionSchema.options, ...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options, ...SanitizedTeamCityConnectionSchema.options,
...SanitizedOCIConnectionSchema.options, ...SanitizedOCIConnectionSchema.options,
...SanitizedOracleDBConnectionSchema.options,
...SanitizedOnePassConnectionSchema.options ...SanitizedOnePassConnectionSchema.options
]); ]);
@@ -100,6 +110,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
GcpConnectionListItemSchema, GcpConnectionListItemSchema,
AzureKeyVaultConnectionListItemSchema, AzureKeyVaultConnectionListItemSchema,
AzureAppConfigurationConnectionListItemSchema, AzureAppConfigurationConnectionListItemSchema,
AzureDevOpsConnectionListItemSchema,
DatabricksConnectionListItemSchema, DatabricksConnectionListItemSchema,
HumanitecConnectionListItemSchema, HumanitecConnectionListItemSchema,
TerraformCloudConnectionListItemSchema, TerraformCloudConnectionListItemSchema,
@@ -115,6 +126,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
LdapConnectionListItemSchema, LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema, TeamCityConnectionListItemSchema,
OCIConnectionListItemSchema, OCIConnectionListItemSchema,
OracleDBConnectionListItemSchema,
OnePassConnectionListItemSchema OnePassConnectionListItemSchema
]); ]);

View File

@@ -0,0 +1,49 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateAzureDevOpsConnectionSchema,
SanitizedAzureDevOpsConnectionSchema,
UpdateAzureDevOpsConnectionSchema
} from "@app/services/app-connection/azure-devops/azure-devops-schemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerAzureDevOpsConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.AzureDevOps,
server,
sanitizedResponseSchema: SanitizedAzureDevOpsConnectionSchema,
createSchema: CreateAzureDevOpsConnectionSchema,
updateSchema: UpdateAzureDevOpsConnectionSchema
});
server.route({
method: "GET",
url: `/:connectionId/projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
projects: z.object({ name: z.string(), id: z.string(), appId: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.azureDevOps.listProjects(connectionId, req.permission);
return { projects };
}
});
};

View File

@@ -1,4 +1,5 @@
import { registerOCIConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oci-connection-router"; import { registerOCIConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oci-connection-router";
import { registerOracleDBConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oracledb-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerOnePassConnectionRouter } from "./1password-connection-router"; import { registerOnePassConnectionRouter } from "./1password-connection-router";
@@ -6,6 +7,7 @@ import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router"; import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
@@ -34,6 +36,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter, [AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter, [AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter, [AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
[AppConnection.AzureDevOps]: registerAzureDevOpsConnectionRouter,
[AppConnection.Databricks]: registerDatabricksConnectionRouter, [AppConnection.Databricks]: registerDatabricksConnectionRouter,
[AppConnection.Humanitec]: registerHumanitecConnectionRouter, [AppConnection.Humanitec]: registerHumanitecConnectionRouter,
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter, [AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
@@ -48,5 +51,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.LDAP]: registerLdapConnectionRouter, [AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter, [AppConnection.TeamCity]: registerTeamCityConnectionRouter,
[AppConnection.OCI]: registerOCIConnectionRouter, [AppConnection.OCI]: registerOCIConnectionRouter,
[AppConnection.OracleDB]: registerOracleDBConnectionRouter,
[AppConnection.OnePass]: registerOnePassConnectionRouter [AppConnection.OnePass]: registerOnePassConnectionRouter
}; };

View File

@@ -0,0 +1,381 @@
import RE2 from "re2";
import { z } from "zod";
import { IdentityAlicloudAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ALICLOUD_AUTH, ApiDocsTags } from "@app/lib/api-docs";
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";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateArns } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/alicloud-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Login with Alibaba Cloud Auth",
body: z.object({
identityId: z.string().trim().describe(ALICLOUD_AUTH.LOGIN.identityId),
Action: z.enum(["GetCallerIdentity"]).describe(ALICLOUD_AUTH.LOGIN.Action),
Format: z.enum(["JSON"]).describe(ALICLOUD_AUTH.LOGIN.Format),
Version: z
.string()
.refine((val) => new RE2("^\\d{4}-\\d{2}-\\d{2}$").test(val), {
message: "Version must be in YYYY-MM-DD format"
})
.describe(ALICLOUD_AUTH.LOGIN.Version),
AccessKeyId: z
.string()
.refine((val) => new RE2("^[A-Za-z0-9]+$").test(val), {
message: "AccessKeyId must be alphanumeric"
})
.describe(ALICLOUD_AUTH.LOGIN.AccessKeyId),
SignatureMethod: z.enum(["HMAC-SHA1"]).describe(ALICLOUD_AUTH.LOGIN.SignatureMethod),
Timestamp: z
.string()
.datetime({
message: "Timestamp must be in YYYY-MM-DDTHH:mm:ssZ format"
})
.refine((val) => val.endsWith("Z"), {
message: "Timestamp must be in YYYY-MM-DDTHH:mm:ssZ format"
})
.describe(ALICLOUD_AUTH.LOGIN.Timestamp),
SignatureVersion: z.enum(["1.0"]).describe(ALICLOUD_AUTH.LOGIN.SignatureVersion),
SignatureNonce: z
.string()
.refine((val) => new RE2("^[a-zA-Z0-9-_.]+$").test(val), {
message:
"SignatureNonce must be at least 1 character long and contain only URL-safe characters (alphanumeric, -, _, .)"
})
.describe(ALICLOUD_AUTH.LOGIN.SignatureNonce),
Signature: z
.string()
.refine((val) => new RE2("^[A-Za-z0-9+/=]+$").test(val), {
message: "Signature must be base64 characters"
})
.describe(ALICLOUD_AUTH.LOGIN.Signature)
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityAliCloudAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityAliCloudAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH,
metadata: {
identityId: identityAliCloudAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityAliCloudAuthId: identityAliCloudAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityAliCloudAuth.accessTokenTTL,
accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/alicloud-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Attach Alibaba Cloud Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(ALICLOUD_AUTH.ATTACH.identityId)
}),
body: z
.object({
allowedArns: validateArns.describe(ALICLOUD_AUTH.ATTACH.allowedArns),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(ALICLOUD_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(ALICLOUD_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(ALICLOUD_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(ALICLOUD_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityAliCloudAuth: IdentityAlicloudAuthsSchema
})
}
},
handler: async (req) => {
const identityAliCloudAuth = await server.services.identityAliCloudAuth.attachAliCloudAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityAliCloudAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_ALICLOUD_AUTH,
metadata: {
identityId: identityAliCloudAuth.identityId,
allowedArns: identityAliCloudAuth.allowedArns,
accessTokenTTL: identityAliCloudAuth.accessTokenTTL,
accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityAliCloudAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit
}
}
});
return { identityAliCloudAuth };
}
});
server.route({
method: "PATCH",
url: "/alicloud-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Update Alibaba Cloud Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(ALICLOUD_AUTH.UPDATE.identityId)
}),
body: z
.object({
allowedArns: validateArns.describe(ALICLOUD_AUTH.UPDATE.allowedArns),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(ALICLOUD_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(ALICLOUD_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.optional()
.describe(ALICLOUD_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.min(0)
.optional()
.describe(ALICLOUD_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityAliCloudAuth: IdentityAlicloudAuthsSchema
})
}
},
handler: async (req) => {
const identityAliCloudAuth = await server.services.identityAliCloudAuth.updateAliCloudAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId,
allowedArns: req.body.allowedArns
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityAliCloudAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_ALICLOUD_AUTH,
metadata: {
identityId: identityAliCloudAuth.identityId,
allowedArns: identityAliCloudAuth.allowedArns,
accessTokenTTL: identityAliCloudAuth.accessTokenTTL,
accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityAliCloudAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit
}
}
});
return { identityAliCloudAuth };
}
});
server.route({
method: "GET",
url: "/alicloud-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Retrieve Alibaba Cloud Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(ALICLOUD_AUTH.RETRIEVE.identityId)
}),
response: {
200: z.object({
identityAliCloudAuth: IdentityAlicloudAuthsSchema
})
}
},
handler: async (req) => {
const identityAliCloudAuth = await server.services.identityAliCloudAuth.getAliCloudAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityAliCloudAuth.orgId,
event: {
type: EventType.GET_IDENTITY_ALICLOUD_AUTH,
metadata: {
identityId: identityAliCloudAuth.identityId
}
}
});
return { identityAliCloudAuth };
}
});
server.route({
method: "DELETE",
url: "/alicloud-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Delete Alibaba Cloud Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(ALICLOUD_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityAliCloudAuth: IdentityAlicloudAuthsSchema
})
}
},
handler: async (req) => {
const identityAliCloudAuth = await server.services.identityAliCloudAuth.revokeIdentityAliCloudAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityAliCloudAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_ALICLOUD_AUTH,
metadata: {
identityId: identityAliCloudAuth.identityId
}
}
});
return { identityAliCloudAuth };
}
});
};

View File

@@ -108,17 +108,21 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
.string() .string()
.trim() .trim()
.min(1) .min(1)
.nullable()
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost) .describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
.refine( .refine(
(val) => (val) => {
characterValidator([ if (val === null) return true;
return characterValidator([
CharacterType.Alphabets, CharacterType.Alphabets,
CharacterType.Numbers, CharacterType.Numbers,
CharacterType.Colon, CharacterType.Colon,
CharacterType.Period, CharacterType.Period,
CharacterType.ForwardSlash, CharacterType.ForwardSlash,
CharacterType.Hyphen CharacterType.Hyphen
])(val), ])(val);
},
{ {
message: message:
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes." "Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
@@ -164,6 +168,13 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit) .describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api && !data.kubernetesHost) {
ctx.addIssue({
path: ["kubernetesHost"],
code: z.ZodIssueCode.custom,
message: "When token review mode is set to API, a Kubernetes host must be provided"
});
}
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) { if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
ctx.addIssue({ ctx.addIssue({
path: ["gatewayId"], path: ["gatewayId"],
@@ -171,6 +182,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
message: "When token review mode is set to Gateway, a gateway must be selected" message: "When token review mode is set to Gateway, a gateway must be selected"
}); });
} }
if (data.accessTokenTTL > data.accessTokenMaxTTL) { if (data.accessTokenTTL > data.accessTokenMaxTTL) {
ctx.addIssue({ ctx.addIssue({
path: ["accessTokenTTL"], path: ["accessTokenTTL"],
@@ -203,7 +215,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH, type: EventType.ADD_IDENTITY_KUBERNETES_AUTH,
metadata: { metadata: {
identityId: identityKubernetesAuth.identityId, identityId: identityKubernetesAuth.identityId,
kubernetesHost: identityKubernetesAuth.kubernetesHost, kubernetesHost: identityKubernetesAuth.kubernetesHost ?? "",
allowedNamespaces: identityKubernetesAuth.allowedNamespaces, allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
allowedNames: identityKubernetesAuth.allowedNames, allowedNames: identityKubernetesAuth.allowedNames,
accessTokenTTL: identityKubernetesAuth.accessTokenTTL, accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
@@ -243,6 +255,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
.string() .string()
.trim() .trim()
.min(1) .min(1)
.nullable()
.optional() .optional()
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost) .describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
.refine( .refine(
@@ -345,7 +358,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH, type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH,
metadata: { metadata: {
identityId: identityKubernetesAuth.identityId, identityId: identityKubernetesAuth.identityId,
kubernetesHost: identityKubernetesAuth.kubernetesHost, kubernetesHost: identityKubernetesAuth.kubernetesHost ?? "",
allowedNamespaces: identityKubernetesAuth.allowedNamespaces, allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
allowedNames: identityKubernetesAuth.allowedNames, allowedNames: identityKubernetesAuth.allowedNames,
accessTokenTTL: identityKubernetesAuth.accessTokenTTL, accessTokenTTL: identityKubernetesAuth.accessTokenTTL,

View File

@@ -15,6 +15,7 @@ import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router"; import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAliCloudAuthRouter } from "./identity-alicloud-auth-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
@@ -63,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityKubernetesRouter); await authRouter.register(registerIdentityKubernetesRouter);
await authRouter.register(registerIdentityGcpAuthRouter); await authRouter.register(registerIdentityGcpAuthRouter);
await authRouter.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAliCloudAuthRouter);
await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAwsAuthRouter);
await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOciAuthRouter); await authRouter.register(registerIdentityOciAuthRouter);

View File

@@ -0,0 +1,17 @@
import {
AzureDevOpsSyncSchema,
CreateAzureDevOpsSyncSchema,
UpdateAzureDevOpsSyncSchema
} from "@app/services/secret-sync/azure-devops";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerAzureDevOpsSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.AzureDevOps,
server,
responseSchema: AzureDevOpsSyncSchema,
createSchema: CreateAzureDevOpsSyncSchema,
updateSchema: UpdateAzureDevOpsSyncSchema
});

View File

@@ -5,6 +5,7 @@ import { registerOnePassSyncRouter } from "./1password-sync-router";
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router"; import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router"; import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router"; import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router";
import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router";
import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router";
@@ -26,6 +27,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.GCPSecretManager]: registerGcpSyncRouter, [SecretSync.GCPSecretManager]: registerGcpSyncRouter,
[SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter, [SecretSync.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter, [SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
[SecretSync.AzureDevOps]: registerAzureDevOpsSyncRouter,
[SecretSync.Databricks]: registerDatabricksSyncRouter, [SecretSync.Databricks]: registerDatabricksSyncRouter,
[SecretSync.Humanitec]: registerHumanitecSyncRouter, [SecretSync.Humanitec]: registerHumanitecSyncRouter,
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter, [SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,

View File

@@ -19,6 +19,7 @@ import {
AzureAppConfigurationSyncListItemSchema, AzureAppConfigurationSyncListItemSchema,
AzureAppConfigurationSyncSchema AzureAppConfigurationSyncSchema
} from "@app/services/secret-sync/azure-app-configuration"; } from "@app/services/secret-sync/azure-app-configuration";
import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/services/secret-sync/azure-devops";
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault"; import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda"; import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks"; import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
@@ -38,6 +39,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
GcpSyncSchema, GcpSyncSchema,
AzureKeyVaultSyncSchema, AzureKeyVaultSyncSchema,
AzureAppConfigurationSyncSchema, AzureAppConfigurationSyncSchema,
AzureDevOpsSyncSchema,
DatabricksSyncSchema, DatabricksSyncSchema,
HumanitecSyncSchema, HumanitecSyncSchema,
TerraformCloudSyncSchema, TerraformCloudSyncSchema,
@@ -57,6 +59,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
GcpSyncListItemSchema, GcpSyncListItemSchema,
AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncListItemSchema,
AzureAppConfigurationSyncListItemSchema, AzureAppConfigurationSyncListItemSchema,
AzureDevOpsSyncListItemSchema,
DatabricksSyncListItemSchema, DatabricksSyncListItemSchema,
HumanitecSyncListItemSchema, HumanitecSyncListItemSchema,
TerraformCloudSyncListItemSchema, TerraformCloudSyncListItemSchema,

View File

@@ -7,6 +7,7 @@ export enum AppConnection {
AzureKeyVault = "azure-key-vault", AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration", AzureAppConfiguration = "azure-app-configuration",
AzureClientSecrets = "azure-client-secrets", AzureClientSecrets = "azure-client-secrets",
AzureDevOps = "azure-devops",
Humanitec = "humanitec", Humanitec = "humanitec",
TerraformCloud = "terraform-cloud", TerraformCloud = "terraform-cloud",
Vercel = "vercel", Vercel = "vercel",
@@ -20,6 +21,7 @@ export enum AppConnection {
LDAP = "ldap", LDAP = "ldap",
TeamCity = "teamcity", TeamCity = "teamcity",
OCI = "oci", OCI = "oci",
OracleDB = "oracledb",
OnePass = "1password" OnePass = "1password"
} }

View File

@@ -4,6 +4,7 @@ import {
OCIConnectionMethod, OCIConnectionMethod,
validateOCIConnectionCredentials validateOCIConnectionCredentials
} from "@app/ee/services/app-connections/oci"; } from "@app/ee/services/app-connections/oci";
import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { generateHash } from "@app/lib/crypto/encryption"; import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@@ -39,6 +40,11 @@ import {
getAzureClientSecretsConnectionListItem, getAzureClientSecretsConnectionListItem,
validateAzureClientSecretsConnectionCredentials validateAzureClientSecretsConnectionCredentials
} from "./azure-client-secrets"; } from "./azure-client-secrets";
import { AzureDevOpsConnectionMethod } from "./azure-devops/azure-devops-enums";
import {
getAzureDevopsConnectionListItem,
validateAzureDevOpsConnectionCredentials
} from "./azure-devops/azure-devops-fns";
import { import {
AzureKeyVaultConnectionMethod, AzureKeyVaultConnectionMethod,
getAzureKeyVaultConnectionListItem, getAzureKeyVaultConnectionListItem,
@@ -98,6 +104,7 @@ export const listAppConnectionOptions = () => {
getGcpConnectionListItem(), getGcpConnectionListItem(),
getAzureKeyVaultConnectionListItem(), getAzureKeyVaultConnectionListItem(),
getAzureAppConfigurationConnectionListItem(), getAzureAppConfigurationConnectionListItem(),
getAzureDevopsConnectionListItem(),
getDatabricksConnectionListItem(), getDatabricksConnectionListItem(),
getHumanitecConnectionListItem(), getHumanitecConnectionListItem(),
getTerraformCloudConnectionListItem(), getTerraformCloudConnectionListItem(),
@@ -113,6 +120,7 @@ export const listAppConnectionOptions = () => {
getLdapConnectionListItem(), getLdapConnectionListItem(),
getTeamCityConnectionListItem(), getTeamCityConnectionListItem(),
getOCIConnectionListItem(), getOCIConnectionListItem(),
getOracleDBConnectionListItem(),
getOnePassConnectionListItem() getOnePassConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -173,6 +181,7 @@ export const validateAppConnectionCredentials = async (
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator, validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureClientSecrets]: [AppConnection.AzureClientSecrets]:
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator, validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -186,6 +195,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator
}; };
@@ -201,6 +211,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case AzureAppConfigurationConnectionMethod.OAuth: case AzureAppConfigurationConnectionMethod.OAuth:
case AzureClientSecretsConnectionMethod.OAuth: case AzureClientSecretsConnectionMethod.OAuth:
case GitHubConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth:
case AzureDevOpsConnectionMethod.OAuth:
return "OAuth"; return "OAuth";
case AwsConnectionMethod.AccessKey: case AwsConnectionMethod.AccessKey:
case OCIConnectionMethod.AccessKey: case OCIConnectionMethod.AccessKey:
@@ -221,10 +232,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
case MySqlConnectionMethod.UsernameAndPassword: case MySqlConnectionMethod.UsernameAndPassword:
case OracleDBConnectionMethod.UsernameAndPassword:
return "Username & Password"; return "Username & Password";
case WindmillConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken:
case HCVaultConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken:
case AzureDevOpsConnectionMethod.AccessToken:
return "Access Token"; return "Access Token";
case Auth0ConnectionMethod.ClientCredentials: case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials"; return "Client Credentials";
@@ -270,6 +283,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.GCP]: platformManagedCredentialsNotSupported, [AppConnection.GCP]: platformManagedCredentialsNotSupported,
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported, [AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported, [AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
[AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported,
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported, [AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
@@ -284,6 +298,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future [AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported, [AppConnection.TeamCity]: platformManagedCredentialsNotSupported,
[AppConnection.OCI]: platformManagedCredentialsNotSupported, [AppConnection.OCI]: platformManagedCredentialsNotSupported,
[AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.OnePass]: platformManagedCredentialsNotSupported [AppConnection.OnePass]: platformManagedCredentialsNotSupported
}; };

View File

@@ -8,6 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AzureKeyVault]: "Azure Key Vault", [AppConnection.AzureKeyVault]: "Azure Key Vault",
[AppConnection.AzureAppConfiguration]: "Azure App Configuration", [AppConnection.AzureAppConfiguration]: "Azure App Configuration",
[AppConnection.AzureClientSecrets]: "Azure Client Secrets", [AppConnection.AzureClientSecrets]: "Azure Client Secrets",
[AppConnection.AzureDevOps]: "Azure DevOps",
[AppConnection.Databricks]: "Databricks", [AppConnection.Databricks]: "Databricks",
[AppConnection.Humanitec]: "Humanitec", [AppConnection.Humanitec]: "Humanitec",
[AppConnection.TerraformCloud]: "Terraform Cloud", [AppConnection.TerraformCloud]: "Terraform Cloud",
@@ -22,6 +23,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.LDAP]: "LDAP", [AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity", [AppConnection.TeamCity]: "TeamCity",
[AppConnection.OCI]: "OCI", [AppConnection.OCI]: "OCI",
[AppConnection.OracleDB]: "OracleDB",
[AppConnection.OnePass]: "1Password" [AppConnection.OnePass]: "1Password"
}; };
@@ -33,6 +35,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular, [AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular,
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular, [AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular, [AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
[AppConnection.AzureDevOps]: AppConnectionPlanType.Regular,
[AppConnection.Databricks]: AppConnectionPlanType.Regular, [AppConnection.Databricks]: AppConnectionPlanType.Regular,
[AppConnection.Humanitec]: AppConnectionPlanType.Regular, [AppConnection.Humanitec]: AppConnectionPlanType.Regular,
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular, [AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
@@ -46,6 +49,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.LDAP]: AppConnectionPlanType.Regular, [AppConnection.LDAP]: AppConnectionPlanType.Regular,
[AppConnection.TeamCity]: AppConnectionPlanType.Regular, [AppConnection.TeamCity]: AppConnectionPlanType.Regular,
[AppConnection.OCI]: AppConnectionPlanType.Enterprise, [AppConnection.OCI]: AppConnectionPlanType.Enterprise,
[AppConnection.OracleDB]: AppConnectionPlanType.Enterprise,
[AppConnection.OnePass]: AppConnectionPlanType.Regular, [AppConnection.OnePass]: AppConnectionPlanType.Regular,
[AppConnection.MySql]: AppConnectionPlanType.Regular [AppConnection.MySql]: AppConnectionPlanType.Regular
}; };

View File

@@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -41,6 +42,8 @@ import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets"; import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service"; import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
import { ValidateAzureDevOpsConnectionCredentialsSchema } from "./azure-devops/azure-devops-schemas";
import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-service";
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault"; import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda"; import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service"; import { camundaConnectionService } from "./camunda/camunda-connection-service";
@@ -84,6 +87,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema, [AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema, [AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema, [AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
[AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema,
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema, [AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema, [AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema, [AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
@@ -99,6 +103,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema, [AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema, [AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema, [AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema,
[AppConnection.OracleDB]: ValidateOracleDBConnectionCredentialsSchema,
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema [AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema
}; };
@@ -498,6 +503,7 @@ export const appConnectionServiceFactory = ({
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
vercel: vercelConnectionService(connectAppConnectionById), vercel: vercelConnectionService(connectAppConnectionById),
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
azureDevOps: azureDevOpsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
hcvault: hcVaultConnectionService(connectAppConnectionById), hcvault: hcVaultConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById), windmill: windmillConnectionService(connectAppConnectionById),

View File

@@ -4,6 +4,11 @@ import {
TOCIConnectionInput, TOCIConnectionInput,
TValidateOCIConnectionCredentialsSchema TValidateOCIConnectionCredentialsSchema
} from "@app/ee/services/app-connections/oci"; } from "@app/ee/services/app-connections/oci";
import {
TOracleDBConnection,
TOracleDBConnectionInput,
TValidateOracleDBConnectionCredentialsSchema
} from "@app/ee/services/app-connections/oracledb";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -39,6 +44,12 @@ import {
TAzureClientSecretsConnectionInput, TAzureClientSecretsConnectionInput,
TValidateAzureClientSecretsConnectionCredentialsSchema TValidateAzureClientSecretsConnectionCredentialsSchema
} from "./azure-client-secrets"; } from "./azure-client-secrets";
import {
TAzureDevOpsConnection,
TAzureDevOpsConnectionConfig,
TAzureDevOpsConnectionInput,
TValidateAzureDevOpsConnectionCredentialsSchema
} from "./azure-devops/azure-devops-types";
import { import {
TAzureKeyVaultConnection, TAzureKeyVaultConnection,
TAzureKeyVaultConnectionConfig, TAzureKeyVaultConnectionConfig,
@@ -132,6 +143,7 @@ export type TAppConnection = { id: string } & (
| TGcpConnection | TGcpConnection
| TAzureKeyVaultConnection | TAzureKeyVaultConnection
| TAzureAppConfigurationConnection | TAzureAppConfigurationConnection
| TAzureDevOpsConnection
| TDatabricksConnection | TDatabricksConnection
| THumanitecConnection | THumanitecConnection
| TTerraformCloudConnection | TTerraformCloudConnection
@@ -147,12 +159,13 @@ export type TAppConnection = { id: string } & (
| TLdapConnection | TLdapConnection
| TTeamCityConnection | TTeamCityConnection
| TOCIConnection | TOCIConnection
| TOracleDBConnection
| TOnePassConnection | TOnePassConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
export type TSqlConnection = TPostgresConnection | TMsSqlConnection | TMySqlConnection; export type TSqlConnection = TPostgresConnection | TMsSqlConnection | TMySqlConnection | TOracleDBConnection;
export type TAppConnectionInput = { id: string } & ( export type TAppConnectionInput = { id: string } & (
| TAwsConnectionInput | TAwsConnectionInput
@@ -161,6 +174,7 @@ export type TAppConnectionInput = { id: string } & (
| TGcpConnectionInput | TGcpConnectionInput
| TAzureKeyVaultConnectionInput | TAzureKeyVaultConnectionInput
| TAzureAppConfigurationConnectionInput | TAzureAppConfigurationConnectionInput
| TAzureDevOpsConnectionInput
| TDatabricksConnectionInput | TDatabricksConnectionInput
| THumanitecConnectionInput | THumanitecConnectionInput
| TTerraformCloudConnectionInput | TTerraformCloudConnectionInput
@@ -176,10 +190,15 @@ export type TAppConnectionInput = { id: string } & (
| TLdapConnectionInput | TLdapConnectionInput
| TTeamCityConnectionInput | TTeamCityConnectionInput
| TOCIConnectionInput | TOCIConnectionInput
| TOracleDBConnectionInput
| TOnePassConnectionInput | TOnePassConnectionInput
); );
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput | TMySqlConnectionInput; export type TSqlConnectionInput =
| TPostgresConnectionInput
| TMsSqlConnectionInput
| TMySqlConnectionInput
| TOracleDBConnectionInput;
export type TCreateAppConnectionDTO = Pick< export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput, TAppConnectionInput,
@@ -197,6 +216,7 @@ export type TAppConnectionConfig =
| TGcpConnectionConfig | TGcpConnectionConfig
| TAzureKeyVaultConnectionConfig | TAzureKeyVaultConnectionConfig
| TAzureAppConfigurationConnectionConfig | TAzureAppConfigurationConnectionConfig
| TAzureDevOpsConnectionConfig
| TAzureClientSecretsConnectionConfig | TAzureClientSecretsConnectionConfig
| TDatabricksConnectionConfig | TDatabricksConnectionConfig
| THumanitecConnectionConfig | THumanitecConnectionConfig
@@ -220,6 +240,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateAzureKeyVaultConnectionCredentialsSchema | TValidateAzureKeyVaultConnectionCredentialsSchema
| TValidateAzureAppConfigurationConnectionCredentialsSchema | TValidateAzureAppConfigurationConnectionCredentialsSchema
| TValidateAzureClientSecretsConnectionCredentialsSchema | TValidateAzureClientSecretsConnectionCredentialsSchema
| TValidateAzureDevOpsConnectionCredentialsSchema
| TValidateDatabricksConnectionCredentialsSchema | TValidateDatabricksConnectionCredentialsSchema
| TValidateHumanitecConnectionCredentialsSchema | TValidateHumanitecConnectionCredentialsSchema
| TValidatePostgresConnectionCredentialsSchema | TValidatePostgresConnectionCredentialsSchema
@@ -234,6 +255,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateLdapConnectionCredentialsSchema | TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema | TValidateTeamCityConnectionCredentialsSchema
| TValidateOCIConnectionCredentialsSchema | TValidateOCIConnectionCredentialsSchema
| TValidateOracleDBConnectionCredentialsSchema
| TValidateOnePassConnectionCredentialsSchema; | TValidateOnePassConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {

View File

@@ -0,0 +1,4 @@
export enum AzureDevOpsConnectionMethod {
OAuth = "oauth",
AccessToken = "access-token"
}

View File

@@ -0,0 +1,269 @@
/* eslint-disable no-case-declarations */
import { AxiosError, AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import {
decryptAppConnectionCredentials,
encryptAppConnectionCredentials,
getAppConnectionMethodName
} from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "../app-connection-dal";
import { AppConnection } from "../app-connection-enums";
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
import {
ExchangeCodeAzureResponse,
TAzureDevOpsConnectionConfig,
TAzureDevOpsConnectionCredentials
} from "./azure-devops-types";
export const getAzureDevopsConnectionListItem = () => {
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
return {
name: "Azure DevOps" as const,
app: AppConnection.AzureDevOps as const,
methods: Object.values(AzureDevOpsConnectionMethod) as [
AzureDevOpsConnectionMethod.OAuth,
AzureDevOpsConnectionMethod.AccessToken
],
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
};
};
export const getAzureDevopsConnection = async (
connectionId: string,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) {
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
}
if (appConnection.app !== AppConnection.AzureDevOps) {
throw new BadRequestError({
message: `Connection with ID '${connectionId}' is not an Azure DevOps connection`
});
}
const credentials = (await decryptAppConnectionCredentials({
orgId: appConnection.orgId,
kmsService,
encryptedCredentials: appConnection.encryptedCredentials
})) as TAzureDevOpsConnectionCredentials;
// Handle different connection methods
switch (appConnection.method) {
case AzureDevOpsConnectionMethod.OAuth:
const appCfg = getConfig();
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
throw new BadRequestError({
message: `Azure environment variables have not been configured`
});
}
if (!("refreshToken" in credentials)) {
throw new BadRequestError({ message: "Invalid OAuth credentials" });
}
const { refreshToken, tenantId } = credentials;
const currentTime = Date.now();
const { data } = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
new URLSearchParams({
grant_type: "refresh_token",
scope: `https://app.vssps.visualstudio.com/.default`,
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
refresh_token: refreshToken
})
);
const updatedCredentials = {
...credentials,
accessToken: data.access_token,
expiresAt: currentTime + data.expires_in * 1000,
refreshToken: data.refresh_token
};
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId: appConnection.orgId,
kmsService
});
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
return data.access_token;
case AzureDevOpsConnectionMethod.AccessToken:
if (!("accessToken" in credentials)) {
throw new BadRequestError({ message: "Invalid API token credentials" });
}
// For access token, return the basic auth token directly
return credentials.accessToken;
default:
throw new BadRequestError({ message: `Unsupported connection method` });
}
};
export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => {
const { credentials: inputCredentials, method } = config;
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
switch (method) {
case AzureDevOpsConnectionMethod.OAuth:
if (!SITE_URL) {
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
}
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
}
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
let tokenError: AxiosError | null = null;
try {
const oauthCredentials = inputCredentials as { code: string; tenantId: string };
tokenResp = await request.post<ExchangeCodeAzureResponse>(
IntegrationUrls.AZURE_TOKEN_URL.replace("common", oauthCredentials.tenantId || "common"),
new URLSearchParams({
grant_type: "authorization_code",
code: oauthCredentials.code,
scope: `https://app.vssps.visualstudio.com/.default`,
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
} catch (e: unknown) {
if (e instanceof AxiosError) {
tokenError = e;
} else {
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
}
}
if (tokenError) {
if (tokenError instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to get access token: ${
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
}`
});
} else {
throw new InternalServerError({
message: "Failed to get access token"
});
}
}
if (!tokenResp) {
throw new InternalServerError({
message: `Failed to get access token: Token was empty with no error`
});
}
const oauthCredentials = inputCredentials as { code: string; tenantId: string; orgName: string };
return {
tenantId: oauthCredentials.tenantId,
orgName: oauthCredentials.orgName,
accessToken: tokenResp.data.access_token,
refreshToken: tokenResp.data.refresh_token,
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
};
case AzureDevOpsConnectionMethod.AccessToken:
const accessTokenCredentials = inputCredentials as { accessToken: string; orgName?: string };
try {
if (accessTokenCredentials.orgName) {
// Validate against specific organization
const response = await request.get(
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(accessTokenCredentials.orgName)}/_apis/projects?api-version=7.2-preview.2&$top=1`,
{
headers: {
Authorization: `Basic ${Buffer.from(`:${accessTokenCredentials.accessToken}`).toString("base64")}`
}
}
);
if (response.status !== 200) {
throw new BadRequestError({
message: `Failed to validate connection: ${response.status}`
});
}
return {
accessToken: accessTokenCredentials.accessToken,
orgName: accessTokenCredentials.orgName
};
}
// Validate via profile and discover organizations
const profileResponse = await request.get<{ displayName: string }>(
`https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1`,
{
headers: {
Authorization: `Basic ${Buffer.from(`:${accessTokenCredentials.accessToken}`).toString("base64")}`
}
}
);
let organizations: Array<{ accountId: string; accountName: string; accountUri: string }> = [];
try {
const orgsResponse = await request.get<{
value: Array<{ accountId: string; accountName: string; accountUri: string }>;
}>(`https://app.vssps.visualstudio.com/_apis/accounts?api-version=7.1`, {
headers: {
Authorization: `Basic ${Buffer.from(`:${accessTokenCredentials.accessToken}`).toString("base64")}`
}
});
organizations = orgsResponse.data.value || [];
} catch (orgError) {
logger.warn(orgError, "Could not fetch organizations automatically:");
}
return {
accessToken: accessTokenCredentials.accessToken,
userDisplayName: profileResponse.data.displayName,
organizations: organizations.map((org) => ({
accountId: org.accountId,
accountName: org.accountName,
accountUri: org.accountUri
}))
};
} catch (error) {
if (error instanceof AxiosError) {
const errorMessage = accessTokenCredentials.orgName
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
`Failed to validate access token for organization '${accessTokenCredentials.orgName}': ${error.response?.data?.message || error.message}`
: `Invalid Azure DevOps Personal Access Token: ${error.response?.status === 401 ? "Token is invalid or expired" : error.message}`;
throw new BadRequestError({ message: errorMessage });
}
throw new BadRequestError({
message: `Unable to validate Azure DevOps token`
});
}
default:
throw new InternalServerError({
message: `Unhandled Azure connection method: ${method as AzureDevOpsConnectionMethod}`
});
}
};

View File

@@ -0,0 +1,112 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
export const AzureDevOpsConnectionOAuthInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "OAuth code required").describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.code),
tenantId: z.string().trim().min(1, "Tenant ID required").describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.tenantId),
orgName: z
.string()
.trim()
.min(1, "Organization name required")
.describe(AppConnections.CREDENTIALS.AZURE_DEVOPS.orgName)
});
export const AzureDevOpsConnectionOAuthOutputCredentialsSchema = z.object({
tenantId: z.string(),
orgName: z.string(),
accessToken: z.string(),
refreshToken: z.string(),
expiresAt: z.number()
});
export const AzureDevOpsConnectionAccessTokenInputCredentialsSchema = z.object({
accessToken: z.string().trim().min(1, "Access Token required"),
orgName: z.string().trim().min(1, "Organization name required")
});
export const AzureDevOpsConnectionAccessTokenOutputCredentialsSchema = z.object({
accessToken: z.string(),
orgName: z.string()
});
export const ValidateAzureDevOpsConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(AzureDevOpsConnectionMethod.OAuth)
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
credentials: AzureDevOpsConnectionOAuthInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
)
}),
z.object({
method: z
.literal(AzureDevOpsConnectionMethod.AccessToken)
.describe(AppConnections.CREATE(AppConnection.AzureDevOps).method),
credentials: AzureDevOpsConnectionAccessTokenInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AzureDevOps).credentials
)
})
]);
export const CreateAzureDevOpsConnectionSchema = ValidateAzureDevOpsConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureDevOps)
);
export const UpdateAzureDevOpsConnectionSchema = z
.object({
credentials: z
.union([AzureDevOpsConnectionOAuthInputCredentialsSchema, AzureDevOpsConnectionAccessTokenInputCredentialsSchema])
.optional()
.describe(AppConnections.UPDATE(AppConnection.AzureDevOps).credentials)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureDevOps));
const BaseAzureDevOpsConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.AzureDevOps)
});
export const AzureDevOpsConnectionSchema = z.intersection(
BaseAzureDevOpsConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(AzureDevOpsConnectionMethod.OAuth),
credentials: AzureDevOpsConnectionOAuthOutputCredentialsSchema
}),
z.object({
method: z.literal(AzureDevOpsConnectionMethod.AccessToken),
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema
})
])
);
export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method", [
BaseAzureDevOpsConnectionSchema.extend({
method: z.literal(AzureDevOpsConnectionMethod.OAuth),
credentials: AzureDevOpsConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true,
orgName: true
})
}),
BaseAzureDevOpsConnectionSchema.extend({
method: z.literal(AzureDevOpsConnectionMethod.AccessToken),
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema.pick({
orgName: true
})
})
]);
export const AzureDevOpsConnectionListItemSchema = z.object({
name: z.literal("Azure DevOps"),
app: z.literal(AppConnection.AzureDevOps),
methods: z.nativeEnum(AzureDevOpsConnectionMethod).array(),
oauthClientId: z.string().optional()
});

View File

@@ -0,0 +1,127 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable no-case-declarations */
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
import { getAzureDevopsConnection } from "./azure-devops-fns";
import { TAzureDevOpsConnection } from "./azure-devops-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TAzureDevOpsConnection>;
type TAzureDevOpsProject = {
id: string;
name: string;
description?: string;
url?: string;
state?: string;
visibility?: string;
lastUpdateTime?: string;
revision?: number;
abbreviation?: string;
defaultTeamImageUrl?: string;
};
type TAzureDevOpsProjectsResponse = {
count: number;
value: TAzureDevOpsProject[];
};
const getAuthHeaders = (appConnection: TAzureDevOpsConnection, accessToken: string) => {
switch (appConnection.method) {
case AzureDevOpsConnectionMethod.OAuth:
return {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
};
case AzureDevOpsConnectionMethod.AccessToken:
// For access token, create Basic auth header
const basicAuthToken = Buffer.from(`user:${accessToken}`).toString("base64");
return {
Authorization: `Basic ${basicAuthToken}`,
Accept: "application/json"
};
default:
throw new BadRequestError({ message: "Unsupported connection method" });
}
};
const listAzureDevOpsProjects = async (
appConnection: TAzureDevOpsConnection,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<TAzureDevOpsProject[]> => {
const accessToken = await getAzureDevopsConnection(appConnection.id, appConnectionDAL, kmsService);
// Both OAuth and access Token methods use organization name from credentials
const credentials = appConnection.credentials as { orgName: string };
const { orgName } = credentials;
// Use the standard Azure DevOps Projects API endpoint
// This endpoint returns only projects that the authenticated user has access to
const devOpsEndpoint = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/projects?api-version=7.1`;
try {
const { data } = await request.get<TAzureDevOpsProjectsResponse>(devOpsEndpoint, {
headers: getAuthHeaders(appConnection, accessToken)
});
return data.value || [];
} catch (error) {
if (error instanceof AxiosError) {
// Provide more specific error messages based on the response
if (error?.response?.status === 401) {
throw new Error(
`Authentication failed for Azure DevOps organization: ${orgName}. Please check your credentials and ensure the token has the required scopes (vso.project or vso.profile).`
);
} else if (error?.response?.status === 403) {
throw new Error(
`Access denied to Azure DevOps organization: ${orgName}. Please ensure the user has access to the organization.`
);
} else if (error?.response?.status === 404) {
throw new Error(`Azure DevOps organization not found: ${orgName}. Please verify the organization name.`);
}
}
throw error;
}
};
export const azureDevOpsConnectionService = (
getAppConnection: TGetAppConnectionFunc,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.AzureDevOps, connectionId, actor);
const projects = await listAzureDevOpsProjects(appConnection, appConnectionDAL, kmsService);
return projects.map((project) => ({
id: project.id,
name: project.name,
appId: project.id,
description: project.description,
url: project.url,
state: project.state,
visibility: project.visibility,
lastUpdateTime: project.lastUpdateTime,
revision: project.revision,
abbreviation: project.abbreviation,
defaultTeamImageUrl: project.defaultTeamImageUrl
}));
};
return {
listProjects
};
};

View File

@@ -0,0 +1,54 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
AzureDevOpsConnectionOAuthOutputCredentialsSchema,
AzureDevOpsConnectionSchema,
CreateAzureDevOpsConnectionSchema,
ValidateAzureDevOpsConnectionCredentialsSchema
} from "./azure-devops-schemas";
export type TAzureDevOpsConnection = z.infer<typeof AzureDevOpsConnectionSchema>;
export type TAzureDevOpsConnectionInput = z.infer<typeof CreateAzureDevOpsConnectionSchema> & {
app: AppConnection.AzureDevOps;
};
export type TValidateAzureDevOpsConnectionCredentialsSchema = typeof ValidateAzureDevOpsConnectionCredentialsSchema;
export type TAzureDevOpsConnectionConfig = DiscriminativePick<
TAzureDevOpsConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TAzureDevOpsConnectionCredentials = z.infer<typeof AzureDevOpsConnectionOAuthOutputCredentialsSchema>;
export interface ExchangeCodeAzureResponse {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: number;
access_token: string;
refresh_token: string;
id_token: string;
}
export interface TAzureRegisteredApp {
id: string;
appId: string;
displayName: string;
description?: string;
createdDateTime: string;
identifierUris?: string[];
signInAudience?: string;
}
export interface TAzureListRegisteredAppsResponse {
"@odata.context": string;
"@odata.nextLink"?: string;
value: TAzureRegisteredApp[];
}

View File

@@ -16,7 +16,8 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const SQL_CONNECTION_CLIENT_MAP = { const SQL_CONNECTION_CLIENT_MAP = {
[AppConnection.Postgres]: "pg", [AppConnection.Postgres]: "pg",
[AppConnection.MsSql]: "mssql", [AppConnection.MsSql]: "mssql",
[AppConnection.MySql]: "mysql2" [AppConnection.MySql]: "mysql2",
[AppConnection.OracleDB]: "oracledb"
}; };
const getConnectionConfig = ({ const getConnectionConfig = ({
@@ -57,6 +58,17 @@ const getConnectionConfig = ({
: false : false
}; };
} }
case AppConnection.OracleDB: {
return {
ssl: sslEnabled
? {
sslCA: sslCertificate,
sslServerDNMatch: sslRejectUnauthorized
}
: false
};
}
default: default:
throw new Error(`Unhandled SQL Connection Config: ${app as AppConnection}`); throw new Error(`Unhandled SQL Connection Config: ${app as AppConnection}`);
} }
@@ -114,7 +126,8 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record<
> = { > = {
[AppConnection.Postgres]: ({ username, password }) => [`ALTER USER ?? WITH PASSWORD '${password}';`, [username]], [AppConnection.Postgres]: ({ username, password }) => [`ALTER USER ?? WITH PASSWORD '${password}';`, [username]],
[AppConnection.MsSql]: ({ username, password }) => [`ALTER LOGIN ?? WITH PASSWORD = '${password}';`, [username]], [AppConnection.MsSql]: ({ username, password }) => [`ALTER LOGIN ?? WITH PASSWORD = '${password}';`, [username]],
[AppConnection.MySql]: ({ username, password }) => [`ALTER USER ??@'%' IDENTIFIED BY '${password}';`, [username]] [AppConnection.MySql]: ({ username, password }) => [`ALTER USER ??@'%' IDENTIFIED BY '${password}';`, [username]],
[AppConnection.OracleDB]: ({ username, password }) => [`ALTER USER ?? IDENTIFIED BY "${password}"`, [username]]
}; };
export const transferSqlConnectionCredentialsToPlatform = async ( export const transferSqlConnectionCredentialsToPlatform = async (

View File

@@ -120,7 +120,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
return docs.map((doc) => { return docs.map((doc) => {
// Determine if this is a secret or folder change based on populated fields // Determine if this is a secret or folder change based on populated fields
if (doc.secretKey && doc.secretVersion && doc.secretId) { if (doc.secretKey && doc.secretVersion !== null && doc.secretId) {
return { return {
...doc, ...doc,
resourceType: "secret", resourceType: "secret",
@@ -168,7 +168,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
); );
return docs return docs
.filter((doc) => doc.secretKey && doc.secretVersion && doc.secretId) .filter((doc) => doc.secretKey && doc.secretVersion !== null && doc.secretId)
.map( .map(
(doc): SecretCommitChange => ({ (doc): SecretCommitChange => ({
...doc, ...doc,
@@ -209,7 +209,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
); );
return docs return docs
.filter((doc) => doc.folderName && doc.folderVersion && doc.folderChangeId) .filter((doc) => doc.folderName && doc.folderVersion !== null && doc.folderChangeId)
.map( .map(
(doc): FolderCommitChange => ({ (doc): FolderCommitChange => ({
...doc, ...doc,

View File

@@ -815,7 +815,7 @@ export const folderCommitServiceFactory = ({
encryptedComment: version1.encryptedComment encryptedComment: version1.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: version1.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: version1.encryptedComment }).toString()
: "", : "",
metadata: version1.metadata as { key: string; value: string }[], metadata: Array.isArray(version1.metadata) ? (version1.metadata as { key: string; value: string }[]) : [],
tags: version1.tags.map((tag) => tag.id) tags: version1.tags.map((tag) => tag.id)
}; };
const version2Reshaped = { const version2Reshaped = {
@@ -826,7 +826,7 @@ export const folderCommitServiceFactory = ({
encryptedComment: version2.encryptedComment encryptedComment: version2.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: version2.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: version2.encryptedComment }).toString()
: "", : "",
metadata: version2.metadata as { key: string; value: string }[], metadata: Array.isArray(version2.metadata) ? (version2.metadata as { key: string; value: string }[]) : [],
tags: version2.tags.map((tag) => tag.id) tags: version2.tags.map((tag) => tag.id)
}; };
return ( return (

View File

@@ -28,6 +28,11 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
`${TableName.IdentityUniversalAuth}.id` `${TableName.IdentityUniversalAuth}.id`
) )
.leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`) .leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`)
.leftJoin(
TableName.IdentityAliCloudAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityAliCloudAuth}.identityId`
)
.leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`) .leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`)
.leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`) .leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
.leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`) .leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`)
@@ -44,6 +49,10 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.select( .select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityGcpAuth).as("accessTokenTrustedIpsGcp"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityGcpAuth).as("accessTokenTrustedIpsGcp"),
db
.ref("accessTokenTrustedIps")
.withSchema(TableName.IdentityAliCloudAuth)
.as("accessTokenTrustedIpsAliCloud"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
@@ -62,6 +71,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
...doc, ...doc,
trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa, trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa,
trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp, trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp,
trustedIpsAliCloudAuth: doc.accessTokenTrustedIpsAliCloud,
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws, trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,

View File

@@ -193,6 +193,7 @@ export const identityAccessTokenServiceFactory = ({
const trustedIpsMap: Record<IdentityAuthMethod, unknown> = { const trustedIpsMap: Record<IdentityAuthMethod, unknown> = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth, [IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth, [IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
[IdentityAuthMethod.ALICLOUD_AUTH]: identityAccessToken.trustedIpsAliCloudAuth,
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth, [IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
[IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth, [IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth,
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,

View File

@@ -0,0 +1,9 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityAliCloudAuthDALFactory = ReturnType<typeof identityAliCloudAuthDALFactory>;
export const identityAliCloudAuthDALFactory = (db: TDbClient) => {
return ormify(db, TableName.IdentityAliCloudAuth);
};

View File

@@ -0,0 +1,361 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { AxiosError } from "axios";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityAliCloudAuthDALFactory } from "./identity-alicloud-auth-dal";
import {
TAliCloudGetUserResponse,
TAttachAliCloudAuthDTO,
TGetAliCloudAuthDTO,
TLoginAliCloudAuthDTO,
TRevokeAliCloudAuthDTO,
TUpdateAliCloudAuthDTO
} from "./identity-alicloud-auth-types";
type TIdentityAliCloudAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
identityAliCloudAuthDAL: Pick<
TIdentityAliCloudAuthDALFactory,
"findOne" | "transaction" | "create" | "updateById" | "delete"
>;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
export type TIdentityAliCloudAuthServiceFactory = ReturnType<typeof identityAliCloudAuthServiceFactory>;
export const identityAliCloudAuthServiceFactory = ({
identityAccessTokenDAL,
identityAliCloudAuthDAL,
identityOrgMembershipDAL,
licenseService,
permissionService
}: TIdentityAliCloudAuthServiceFactoryDep) => {
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => {
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
if (!identityAliCloudAuth) {
throw new NotFoundError({
message: "Alibaba Cloud auth method not found for identity, did you configure Alibaba Cloud auth?"
});
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
identityId: identityAliCloudAuth.identityId
});
const requestUrl = new URL("https://sts.aliyuncs.com");
for (const key of Object.keys(params)) {
requestUrl.searchParams.set(key, (params as Record<string, string>)[key]);
}
const { data } = await request.get<TAliCloudGetUserResponse>(requestUrl.toString()).catch((err: AxiosError) => {
logger.error(err.response, "AliCloudIdentityLogin: Failed to authenticate with Alibaba Cloud");
throw err;
});
if (identityAliCloudAuth.allowedArns) {
// In the future we could do partial checks for role ARNs
const isAccountAllowed = identityAliCloudAuth.allowedArns.split(",").some((arn) => arn.trim() === data.Arn);
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: Alibaba Cloud account ARN not allowed."
});
}
// Generate the token
const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityAliCloudAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityAliCloudAuth.accessTokenTTL,
accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.ALICLOUD_AUTH
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityAliCloudAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return {
identityAliCloudAuth,
accessToken,
identityAccessToken,
identityMembershipOrg
};
};
const attachAliCloudAuth = async ({
identityId,
allowedArns,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId,
isActorSuperAdmin
}: TAttachAliCloudAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) {
throw new BadRequestError({
message: "Failed to add Alibaba Cloud Auth to already configured identity"
});
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const identityAliCloudAuth = await identityAliCloudAuthDAL.transaction(async (tx) => {
const doc = await identityAliCloudAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
type: "iam",
allowedArns,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
return doc;
});
return { ...identityAliCloudAuth, orgId: identityMembershipOrg.orgId };
};
const updateAliCloudAuth = async ({
identityId,
allowedArns,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateAliCloudAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) {
throw new NotFoundError({
message: "The identity does not have Alibaba Cloud Auth attached"
});
}
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityAliCloudAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityAliCloudAuth.accessTokenTTL) >
(accessTokenMaxTTL || identityAliCloudAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updatedAliCloudAuth = await identityAliCloudAuthDAL.updateById(identityAliCloudAuth.id, {
allowedArns,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return { ...updatedAliCloudAuth, orgId: identityMembershipOrg.orgId };
};
const getAliCloudAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAliCloudAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) {
throw new BadRequestError({
message: "The identity does not have Alibaba Cloud Auth attached"
});
}
const alicloudIdentityAuth = await identityAliCloudAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityAliCloudAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeAliCloudAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) {
throw new BadRequestError({
message: "The identity does not have Alibaba Cloud auth"
});
}
const { permission, membership } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke Alibaba Cloud auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityAliCloudAuth = await identityAliCloudAuthDAL.transaction(async (tx) => {
const deletedAliCloudAuth = await identityAliCloudAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.ALICLOUD_AUTH }, tx);
return { ...deletedAliCloudAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityAliCloudAuth;
};
return {
login,
attachAliCloudAuth,
updateAliCloudAuth,
getAliCloudAuth,
revokeIdentityAliCloudAuth
};
};

View File

@@ -0,0 +1,45 @@
import { TProjectPermission } from "@app/lib/types";
export type TLoginAliCloudAuthDTO = {
identityId: string;
Action: string;
Format: string;
Version: string;
AccessKeyId: string;
SignatureMethod: string;
Timestamp: string;
SignatureVersion: string;
SignatureNonce: string;
Signature: string;
};
export type TAttachAliCloudAuthDTO = {
identityId: string;
allowedArns: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAliCloudAuthDTO = {
identityId: string;
allowedArns: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetAliCloudAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeAliCloudAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TAliCloudGetUserResponse = {
Arn: string;
};

View File

@@ -0,0 +1,25 @@
import RE2 from "re2";
import { z } from "zod";
const arnSchema = z
.string()
.refine(
(val) => new RE2("^acs:ram::[0-9]{16}:(user|role)/.*$").test(val),
"Invalid ARN format. Expected format: acs:ram::[0-9]{16}:(user|role)/*"
);
export const validateArns = z
.string()
.trim()
.min(1, "Allowed ARNs required")
.max(500, "Input exceeds the maximum limit of 500 characters")
.transform((val) => {
if (!val) return [];
return val
.split(",")
.map((s) => s.trim())
.filter(Boolean);
})
.refine((arr) => arr.every((name) => arnSchema.safeParse(name).success), {
message: "One or more ARNs are invalid"
})
.transform((arr) => arr.join(", "));

View File

@@ -94,7 +94,9 @@ export const identityAwsAuthServiceFactory = ({
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
const body: string = Buffer.from(iamRequestBody, "base64").toString(); const body: string = Buffer.from(iamRequestBody, "base64").toString();
const region = headers.Authorization ? awsRegionFromHeader(headers.Authorization) : null;
const authHeader = headers.Authorization || headers.authorization;
const region = authHeader ? awsRegionFromHeader(authHeader) : null;
if (!isValidAwsRegion(region)) { if (!isValidAwsRegion(region)) {
throw new BadRequestError({ message: "Invalid AWS region" }); throw new BadRequestError({ message: "Invalid AWS region" });

View File

@@ -40,7 +40,8 @@ export type TAwsGetCallerIdentityHeaders = {
"X-Amz-Date": string; "X-Amz-Date": string;
"Content-Length": number; "Content-Length": number;
"x-amz-security-token": string; "x-amz-security-token": string;
Authorization: string; Authorization?: string;
authorization?: string;
}; };
export type TGetCallerIdentityResponse = { export type TGetCallerIdentityResponse = {

View File

@@ -72,8 +72,8 @@ export const identityKubernetesAuthServiceFactory = ({
const $gatewayProxyWrapper = async <T>( const $gatewayProxyWrapper = async <T>(
inputs: { inputs: {
gatewayId: string; gatewayId: string;
targetHost: string; targetHost?: string;
targetPort: number; targetPort?: number;
caCert?: string; caCert?: string;
reviewTokenThroughGateway: boolean; reviewTokenThroughGateway: boolean;
}, },
@@ -104,12 +104,16 @@ export const identityKubernetesAuthServiceFactory = ({
cert: relayDetails.certificate, cert: relayDetails.certificate,
key: relayDetails.privateKey.toString() key: relayDetails.privateKey.toString()
}, },
// we always pass this, because its needed for both tcp and http protocol // only needed for TCP protocol, because the gateway as reviewer will use the pod's CA cert for auth directly
...(!inputs.reviewTokenThroughGateway
? {
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
ca: inputs.caCert, ca: inputs.caCert,
rejectUnauthorized: Boolean(inputs.caCert) rejectUnauthorized: Boolean(inputs.caCert)
}) })
} }
: {})
}
); );
return callbackResult; return callbackResult;
@@ -142,8 +146,15 @@ export const identityKubernetesAuthServiceFactory = ({
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
} }
const tokenReviewCallbackRaw = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => { const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => {
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API"); logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
if (!host || !identityKubernetesAuth.kubernetesHost) {
throw new BadRequestError({
message: "Kubernetes host is required when token review mode is set to API"
});
}
let tokenReviewerJwt = ""; let tokenReviewerJwt = "";
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
tokenReviewerJwt = decryptor({ tokenReviewerJwt = decryptor({
@@ -211,11 +222,7 @@ export const identityKubernetesAuthServiceFactory = ({
return res.data; return res.data;
}; };
const tokenReviewCallbackThroughGateway = async ( const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => {
host: string = identityKubernetesAuth.kubernetesHost,
port?: number,
httpsAgent?: https.Agent
) => {
logger.info( logger.info(
{ {
host, host,
@@ -224,11 +231,9 @@ export const identityKubernetesAuthServiceFactory = ({
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway" "tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
); );
const baseUrl = port ? `${host}:${port}` : host;
const res = await axios const res = await axios
.post<TCreateTokenReviewResponse>( .post<TCreateTokenReviewResponse>(
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`, `${host}:${port}/apis/authentication.k8s.io/v1/tokenreviews`,
{ {
apiVersion: "authentication.k8s.io/v1", apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview", kind: "TokenReview",
@@ -240,11 +245,10 @@ export const identityKubernetesAuthServiceFactory = ({
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount
}, },
signal: AbortSignal.timeout(10000), signal: AbortSignal.timeout(10000),
timeout: 10000, timeout: 10000
...(httpsAgent ? { httpsAgent } : {})
} }
) )
.catch((err) => { .catch((err) => {
@@ -273,29 +277,6 @@ export const identityKubernetesAuthServiceFactory = ({
let data: TCreateTokenReviewResponse | undefined; let data: TCreateTokenReviewResponse | undefined;
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) { if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
const { kubernetesHost } = identityKubernetesAuth;
let urlString = kubernetesHost;
if (!kubernetesHost.startsWith("http://") && !kubernetesHost.startsWith("https://")) {
urlString = `https://${kubernetesHost}`;
}
const url = new URL(urlString);
let { port: k8sPort } = url;
const { protocol, hostname: k8sHost } = url;
const cleanedProtocol = new RE2(/[^a-zA-Z0-9]/g).replace(protocol, "").toLowerCase();
if (!["https", "http"].includes(cleanedProtocol)) {
throw new BadRequestError({
message: "Invalid Kubernetes host URL, must start with http:// or https://"
});
}
if (!k8sPort) {
k8sPort = cleanedProtocol === "https" ? "443" : "80";
}
if (!identityKubernetesAuth.gatewayId) { if (!identityKubernetesAuth.gatewayId) {
throw new BadRequestError({ throw new BadRequestError({
message: "Gateway ID is required when token review mode is set to Gateway" message: "Gateway ID is required when token review mode is set to Gateway"
@@ -305,14 +286,17 @@ export const identityKubernetesAuthServiceFactory = ({
data = await $gatewayProxyWrapper( data = await $gatewayProxyWrapper(
{ {
gatewayId: identityKubernetesAuth.gatewayId, gatewayId: identityKubernetesAuth.gatewayId,
targetHost: `${cleanedProtocol}://${k8sHost}`, // note(daniel): must include the protocol (https|http)
targetPort: k8sPort ? Number(k8sPort) : 443,
caCert,
reviewTokenThroughGateway: true reviewTokenThroughGateway: true
}, },
tokenReviewCallbackThroughGateway tokenReviewCallbackThroughGateway
); );
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) { } else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
if (!identityKubernetesAuth.kubernetesHost) {
throw new BadRequestError({
message: "Kubernetes host is required when token review mode is set to API"
});
}
let { kubernetesHost } = identityKubernetesAuth; let { kubernetesHost } = identityKubernetesAuth;
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) { if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, ""); kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");

View File

@@ -12,7 +12,7 @@ export enum IdentityKubernetesAuthTokenReviewMode {
export type TAttachKubernetesAuthDTO = { export type TAttachKubernetesAuthDTO = {
identityId: string; identityId: string;
kubernetesHost: string; kubernetesHost: string | null;
caCert: string; caCert: string;
tokenReviewerJwt?: string; tokenReviewerJwt?: string;
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode; tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
@@ -29,7 +29,7 @@ export type TAttachKubernetesAuthDTO = {
export type TUpdateKubernetesAuthDTO = { export type TUpdateKubernetesAuthDTO = {
identityId: string; identityId: string;
kubernetesHost?: string; kubernetesHost?: string | null;
caCert?: string; caCert?: string;
tokenReviewerJwt?: string | null; tokenReviewerJwt?: string | null;
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode; tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;

View File

@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { import {
TableName, TableName,
TIdentities, TIdentities,
TIdentityAlicloudAuths,
TIdentityAwsAuths, TIdentityAwsAuths,
TIdentityAzureAuths, TIdentityAzureAuths,
TIdentityGcpAuths, TIdentityGcpAuths,
@@ -57,6 +58,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.identityId`, `${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityGcpAuth}.identityId` `${TableName.IdentityGcpAuth}.identityId`
) )
.leftJoin(
TableName.IdentityAliCloudAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityAliCloudAuth}.identityId`
)
.leftJoin( .leftJoin(
TableName.IdentityAwsAuth, TableName.IdentityAwsAuth,
`${TableName.IdentityProjectMembership}.identityId`, `${TableName.IdentityProjectMembership}.identityId`,
@@ -111,6 +117,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("type").as("projectType").withSchema(TableName.Project), db.ref("type").as("projectType").withSchema(TableName.Project),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
@@ -267,6 +274,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.Identity}.id`, `${TableName.Identity}.id`,
`${TableName.IdentityGcpAuth}.identityId` `${TableName.IdentityGcpAuth}.identityId`
) )
.leftJoin<TIdentityAlicloudAuths>(
TableName.IdentityAliCloudAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityAliCloudAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>( .leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth, TableName.IdentityAwsAuth,
`${TableName.Identity}.id`, `${TableName.Identity}.id`,
@@ -319,6 +331,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
@@ -346,6 +359,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
identityId, identityId,
identityName, identityName,
uaId, uaId,
alicloudId,
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
@@ -367,6 +381,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
name: identityName, name: identityName,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
alicloudId,
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,

View File

@@ -122,9 +122,9 @@ export const identityUaServiceFactory = ({
} }
: { : {
accessTokenTTL: identityUa.accessTokenPeriod, accessTokenTTL: identityUa.accessTokenPeriod,
// Setting Max TTL to 2 × period ensures that clients can always renew their token // We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
// at least once, and matches client logic that checks if renewing would exceed Max TTL. // without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
accessTokenMaxTTL: 2 * identityUa.accessTokenPeriod accessTokenMaxTTL: 1000000000
}; };
const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const identityAccessToken = await identityUaDAL.transaction(async (tx) => {

View File

@@ -3,6 +3,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
export const buildAuthMethods = ({ export const buildAuthMethods = ({
uaId, uaId,
gcpId, gcpId,
alicloudId,
awsId, awsId,
kubernetesId, kubernetesId,
ociId, ociId,
@@ -14,6 +15,7 @@ export const buildAuthMethods = ({
}: { }: {
uaId?: string; uaId?: string;
gcpId?: string; gcpId?: string;
alicloudId?: string;
awsId?: string; awsId?: string;
kubernetesId?: string; kubernetesId?: string;
ociId?: string; ociId?: string;
@@ -26,6 +28,7 @@ export const buildAuthMethods = ({
return [ return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null], ...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null], ...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
...[alicloudId ? IdentityAuthMethod.ALICLOUD_AUTH : null],
...[awsId ? IdentityAuthMethod.AWS_AUTH : null], ...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null], ...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
...[ociId ? IdentityAuthMethod.OCI_AUTH : null], ...[ociId ? IdentityAuthMethod.OCI_AUTH : null],

View File

@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { import {
TableName, TableName,
TIdentityAlicloudAuths,
TIdentityAwsAuths, TIdentityAwsAuths,
TIdentityAzureAuths, TIdentityAzureAuths,
TIdentityGcpAuths, TIdentityGcpAuths,
@@ -53,6 +54,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityGcpAuth}.identityId` `${TableName.IdentityGcpAuth}.identityId`
) )
.leftJoin<TIdentityAlicloudAuths>(
TableName.IdentityAliCloudAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityAliCloudAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>( .leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth, TableName.IdentityAwsAuth,
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
@@ -99,6 +105,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
@@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId", "paginatedIdentity.identityId",
`${TableName.IdentityGcpAuth}.identityId` `${TableName.IdentityGcpAuth}.identityId`
) )
.leftJoin<TIdentityAlicloudAuths>(
TableName.IdentityAliCloudAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityAliCloudAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>( .leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth, TableName.IdentityAwsAuth,
"paginatedIdentity.identityId", "paginatedIdentity.identityId",
@@ -236,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
@@ -278,6 +291,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
id, id,
orgId, orgId,
uaId, uaId,
alicloudId,
awsId, awsId,
gcpId, gcpId,
jwtId, jwtId,
@@ -312,6 +326,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
name: identityName, name: identityName,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
alicloudId,
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
@@ -459,6 +474,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
@@ -502,6 +518,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
total_count, total_count,
id, id,
uaId, uaId,
alicloudId,
awsId, awsId,
gcpId, gcpId,
jwtId, jwtId,
@@ -536,6 +553,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
name: identityName, name: identityName,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
alicloudId,
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,

View File

@@ -1211,8 +1211,8 @@ export const orgServiceFactory = ({
subjectLine: "Infisical organization invitation", subjectLine: "Infisical organization invitation",
recipients: [el.email], recipients: [el.email],
substitutions: { substitutions: {
inviterFirstName: invitingUser.firstName, inviterFirstName: invitingUser?.firstName,
inviterUsername: invitingUser.email, inviterUsername: invitingUser?.email,
organizationName: org?.name, organizationName: org?.name,
email: el.email, email: el.email,
organizationId: org?.id.toString(), organizationId: org?.id.toString(),

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const AZURE_DEVOPS_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Azure DevOps",
destination: SecretSync.AzureDevOps,
connection: AppConnection.AzureDevOps,
canImportSecrets: false
};

View File

@@ -0,0 +1,233 @@
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AzureDevOpsConnectionMethod } from "@app/services/app-connection/azure-devops/azure-devops-enums";
import { getAzureDevopsConnection } from "@app/services/app-connection/azure-devops/azure-devops-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAzureDevOpsSyncWithCredentials } from "./azure-devops-sync-types";
type TAzureDevOpsSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
interface AzureDevOpsVariableGroup {
id: number;
name: string;
description: string;
type: string;
variables: Record<string, { value: string; isSecret: boolean }>;
variableGroupProjectReferences: Array<{
description: string;
name: string;
projectReference: { id: string; name: string };
}>;
}
interface AzureDevOpsVariableGroupList {
count: number;
value: AzureDevOpsVariableGroup[];
}
export const azureDevOpsSyncFactory = ({ kmsService, appConnectionDAL }: TAzureDevOpsSyncFactoryDeps) => {
const getConnectionAuth = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
const { credentials } = secretSync.connection;
const isOAuth = secretSync.connection.method === AzureDevOpsConnectionMethod.OAuth;
const { orgName } = credentials;
if (!orgName) {
throw new BadRequestError({
message: "Azure DevOps: organization name is required"
});
}
const accessToken = await getAzureDevopsConnection(secretSync.connectionId, appConnectionDAL, kmsService);
return { accessToken, orgName, isOAuth };
};
const getAuthHeader = (accessToken: string, isOAuth: boolean) => {
if (isOAuth) {
return `Bearer ${accessToken}`;
}
const basicAuth = Buffer.from(`:${accessToken}`).toString("base64");
return `Basic ${basicAuth}`;
};
const $getEnvGroupId = async (
accessToken: string,
orgName: string,
projectId: string,
environmentName: string,
isOAuth: boolean
) => {
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/${encodeURIComponent(projectId)}/_apis/distributedtask/variablegroups?api-version=7.1`;
const response = await request.get<AzureDevOpsVariableGroupList>(url, {
headers: {
Authorization: getAuthHeader(accessToken, isOAuth)
}
});
for (const group of response.data.value) {
if (group.name === environmentName) {
return { groupId: group.id.toString(), groupName: group.name };
}
}
return { groupId: "", groupName: "" };
};
const syncSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials, secretMap: TSecretMap) => {
if (!secretSync.destinationConfig.devopsProjectId) {
throw new BadRequestError({
message: "Azure DevOps: project ID is required"
});
}
if (!secretSync.environment?.name) {
throw new BadRequestError({
message: "Azure DevOps: environment name is required"
});
}
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
const { groupId, groupName } = await $getEnvGroupId(
accessToken,
orgName,
secretSync.destinationConfig.devopsProjectId,
secretSync.environment.name,
isOAuth
);
const variables: Record<string, { value: string; isSecret: boolean }> = {};
for (const [key, secret] of Object.entries(secretMap)) {
if (secret?.value !== undefined) {
variables[key] = { value: secret.value, isSecret: true };
}
}
if (!groupId) {
// Create new variable group - API endpoint is organization-level
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups?api-version=7.1`;
await request.post(
url,
{
name: secretSync.environment.name,
description: secretSync.environment.name,
type: "Vsts",
variables,
variableGroupProjectReferences: [
{
description: secretSync.environment.name,
name: secretSync.environment.name,
projectReference: {
id: secretSync.destinationConfig.devopsProjectId,
name: secretSync.destinationConfig.devopsProjectId
}
}
]
},
{
headers: {
Authorization: getAuthHeader(accessToken, isOAuth),
"Content-Type": "application/json"
}
}
);
} else {
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.1`;
await request.put(
url,
{
name: groupName,
description: groupName,
type: "Vsts",
variables,
variableGroupProjectReferences: [
{
description: groupName,
name: groupName,
projectReference: {
id: secretSync.destinationConfig.devopsProjectId,
name: secretSync.destinationConfig.devopsProjectId
}
}
]
},
{
headers: {
Authorization: getAuthHeader(accessToken, isOAuth),
"Content-Type": "application/json"
}
}
);
}
};
const removeSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
const { groupId } = await $getEnvGroupId(
accessToken,
orgName,
secretSync.destinationConfig.devopsProjectId,
secretSync.environment?.name || "",
isOAuth
);
if (groupId) {
// Delete the variable group entirely using the DELETE API
const deleteUrl = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${encodeURIComponent(orgName)}/_apis/distributedtask/variablegroups/${groupId}?projectIds=${secretSync.destinationConfig.devopsProjectId}&api-version=7.1`;
await request.delete(deleteUrl, {
headers: {
Authorization: getAuthHeader(accessToken, isOAuth)
}
});
}
};
const getSecrets = async (secretSync: TAzureDevOpsSyncWithCredentials) => {
const { accessToken, orgName, isOAuth } = await getConnectionAuth(secretSync);
const { groupId } = await $getEnvGroupId(
accessToken,
orgName,
secretSync.destinationConfig.devopsProjectId,
secretSync.environment?.name || "",
isOAuth
);
const secretMap: TSecretMap = {};
if (groupId) {
const url = `${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.1`;
const response = await request.get<AzureDevOpsVariableGroup>(url, {
headers: {
Authorization: getAuthHeader(accessToken, isOAuth)
}
});
if (response?.data?.variables) {
Object.entries(response.data.variables).forEach(([key, variable]) => {
secretMap[key] = {
value: variable.value || ""
};
});
}
}
return secretMap;
};
return {
syncSecrets,
removeSecrets,
getSecrets
};
};

View File

@@ -0,0 +1,50 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
export const AzureDevOpsSyncDestinationConfigSchema = z.object({
devopsProjectId: z
.string()
.min(1, "Project ID required")
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectId || "Azure DevOps Project ID"),
devopsProjectName: z
.string()
.min(1, "Project name required")
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectName || "Azure DevOps Project Name")
});
const AzureDevOpsSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const AzureDevOpsSyncSchema = BaseSecretSyncSchema(SecretSync.AzureDevOps, AzureDevOpsSyncOptionsConfig).extend({
destination: z.literal(SecretSync.AzureDevOps),
destinationConfig: AzureDevOpsSyncDestinationConfigSchema
});
export const CreateAzureDevOpsSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.AzureDevOps,
AzureDevOpsSyncOptionsConfig
).extend({
destinationConfig: AzureDevOpsSyncDestinationConfigSchema
});
export const UpdateAzureDevOpsSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.AzureDevOps,
AzureDevOpsSyncOptionsConfig
).extend({
destinationConfig: AzureDevOpsSyncDestinationConfigSchema.optional()
});
export const AzureDevOpsSyncListItemSchema = z.object({
name: z.literal("Azure DevOps"),
connection: z.literal(AppConnection.AzureDevOps),
destination: z.literal(SecretSync.AzureDevOps),
canImportSecrets: z.literal(false)
});

View File

@@ -0,0 +1,22 @@
import { z } from "zod";
import { TAzureDevOpsConnection } from "@app/services/app-connection/azure-devops/azure-devops-types";
import {
AzureDevOpsSyncDestinationConfigSchema,
AzureDevOpsSyncListItemSchema,
AzureDevOpsSyncSchema,
CreateAzureDevOpsSyncSchema
} from "./azure-devops-sync-schemas";
export type TAzureDevOpsSync = z.infer<typeof AzureDevOpsSyncSchema>;
export type TAzureDevOpsSyncInput = z.infer<typeof CreateAzureDevOpsSyncSchema>;
export type TAzureDevOpsSyncListItem = z.infer<typeof AzureDevOpsSyncListItemSchema>;
export type TAzureDevOpsSyncDestinationConfig = z.infer<typeof AzureDevOpsSyncDestinationConfigSchema>;
export type TAzureDevOpsSyncWithCredentials = TAzureDevOpsSync & {
connection: TAzureDevOpsConnection;
};

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