mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
108 Commits
help-fix-f
...
misc/add-c
Author | SHA1 | Date | |
---|---|---|---|
|
06a7e804eb | ||
|
0f00474243 | ||
|
3df010f266 | ||
|
333ce9d164 | ||
|
9621df4f8b | ||
|
3f2de2c5ef | ||
|
b2b1c13393 | ||
|
1fb0c638d6 | ||
|
c1ad49a532 | ||
|
d1fcc739c9 | ||
|
c7458d94aa | ||
|
93570df318 | ||
|
e798b4a7ba | ||
|
36c93f47d9 | ||
|
dbbcb157ef | ||
|
bdc23d22e7 | ||
|
08c1740afc | ||
|
3cac4ef927 | ||
|
2667f8f0f2 | ||
|
b39537472b | ||
|
6b60b2562d | ||
|
c2a7827080 | ||
|
64e09b0dcd | ||
|
a7176d44dd | ||
|
09d4cdc634 | ||
|
8a93c0bd59 | ||
|
c0f8f50981 | ||
|
fec47ef81c | ||
|
348f4b9787 | ||
|
aa577b095c | ||
|
f515cc83d7 | ||
|
17bbdbe7bb | ||
|
427de068d5 | ||
|
dbf7ecc9b6 | ||
|
1ef9885062 | ||
|
de48c3e161 | ||
|
852664e2cb | ||
|
fbc8264732 | ||
|
4303547d8c | ||
|
f1c8a66d31 | ||
|
baa05714ab | ||
|
0c21c19c95 | ||
|
c487614c38 | ||
|
a55c8cacea | ||
|
62308fb0a3 | ||
|
55aa1e87c0 | ||
|
c5c7adbc42 | ||
|
f686882ce6 | ||
|
e35417e11b | ||
|
ff0f4cf46a | ||
|
53968e07d0 | ||
|
64093e9175 | ||
|
c315eed4d4 | ||
|
78fd852588 | ||
|
0c1f761a9a | ||
|
c363f485eb | ||
|
433d83641d | ||
|
35bb7f299c | ||
|
160e2b773b | ||
|
f0a70e23ac | ||
|
a6271a6187 | ||
|
b2fbec740f | ||
|
86e5f46d89 | ||
|
720789025c | ||
|
811b3d5934 | ||
|
cac702415f | ||
|
dbe7acdc80 | ||
|
b33985b338 | ||
|
670376336e | ||
|
c59eddb00a | ||
|
fe40ba497b | ||
|
c5b7e3d8be | ||
|
47e778a0b8 | ||
|
8b443e0957 | ||
|
f7fb015bd8 | ||
|
0d7cd357c3 | ||
|
e40f65836f | ||
|
2be56f6a70 | ||
|
1ff1f3fad3 | ||
|
0ae96dfff4 | ||
|
8ad6488bd9 | ||
|
e264b68b7e | ||
|
9e881534ec | ||
|
2832ff5c76 | ||
|
4c6cca0864 | ||
|
c06bbf0b9b | ||
|
69392a4a51 | ||
|
130f1a167e | ||
|
8ab710817d | ||
|
ca39e75434 | ||
|
265b25a4c6 | ||
|
54f6e0b5c6 | ||
|
f2cdefaeec | ||
|
2d588d87ac | ||
|
5ee2eb1aa2 | ||
|
ff5f66a75f | ||
|
bf72638600 | ||
|
d9bc4da6f1 | ||
|
7f8d5ec11a | ||
|
141d0ede2d | ||
|
ab78a79415 | ||
|
8fa6af9ba4 | ||
|
f0a2845637 | ||
|
8ffc88ba28 | ||
|
05d132a1bb | ||
|
bd7c4fc4eb | ||
|
45c84d4936 | ||
|
8e8e2e0dfe |
@@ -3,7 +3,62 @@ name: Release Infisical Core Helm chart
|
||||
on: [workflow_dispatch]
|
||||
|
||||
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:
|
||||
needs: test-helm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -19,4 +74,4 @@ jobs:
|
||||
- name: Build and push helm package to Cloudsmith
|
||||
run: cd helm-charts && sh upload-infisical-core-helm-cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
|
70
.github/workflows/release-k8-operator-helm.yml
vendored
70
.github/workflows/release-k8-operator-helm.yml
vendored
@@ -1,27 +1,59 @@
|
||||
name: Release K8 Operator Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-helm:
|
||||
name: Release Helm Chart
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
test-helm:
|
||||
name: Test Helm Chart
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4.2.0
|
||||
with:
|
||||
version: v3.17.0
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
check-latest: true
|
||||
|
||||
- name: Install Cloudsmith CLI
|
||||
run: pip install --upgrade cloudsmith-cli
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.7.0
|
||||
|
||||
- name: Build and push helm package to CloudSmith
|
||||
run: cd helm-charts && sh upload-k8s-operator-cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
- 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:
|
||||
name: Release Helm Chart
|
||||
needs: test-helm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
|
||||
- name: Install Cloudsmith CLI
|
||||
run: pip install --upgrade cloudsmith-cli
|
||||
|
||||
- name: Build and push helm package to CloudSmith
|
||||
run: cd helm-charts && sh upload-k8s-operator-cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
|
81
.github/workflows/release_helm_gateway.yaml
vendored
81
.github/workflows/release_helm_gateway.yaml
vendored
@@ -1,27 +1,70 @@
|
||||
name: Release Gateway Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-helm:
|
||||
name: Release Helm Chart
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
test-helm:
|
||||
name: Test Helm Chart
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4.2.0
|
||||
with:
|
||||
version: v3.17.0
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
check-latest: true
|
||||
|
||||
- name: Install Cloudsmith CLI
|
||||
run: pip install --upgrade cloudsmith-cli
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.7.0
|
||||
|
||||
- name: Build and push helm package to CloudSmith
|
||||
run: cd helm-charts && sh upload-gateway-cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
- 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:
|
||||
name: Release Helm Chart
|
||||
needs: test-helm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
|
||||
- name: Install Cloudsmith CLI
|
||||
run: pip install --upgrade cloudsmith-cli
|
||||
|
||||
- name: Build and push helm package to CloudSmith
|
||||
run: cd helm-charts && sh upload-gateway-cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
|
49
.github/workflows/run-helm-chart-tests-infisical-gateway.yml
vendored
Normal file
49
.github/workflows/run-helm-chart-tests-infisical-gateway.yml
vendored
Normal 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
|
61
.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml
vendored
Normal file
61
.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml
vendored
Normal 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
|
38
.github/workflows/run-helm-chart-tests-secret-operator.yml
vendored
Normal file
38
.github/workflows/run-helm-chart-tests-secret-operator.yml
vendored
Normal 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
|
@@ -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:581
|
||||
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
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -65,6 +65,7 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p
|
||||
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-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 { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
|
||||
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
||||
@@ -218,6 +219,7 @@ declare module "fastify" {
|
||||
identityUa: TIdentityUaServiceFactory;
|
||||
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
identityOciAuth: TIdentityOciAuthServiceFactory;
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -125,6 +125,9 @@ import {
|
||||
TIdentityAccessTokens,
|
||||
TIdentityAccessTokensInsert,
|
||||
TIdentityAccessTokensUpdate,
|
||||
TIdentityAlicloudAuths,
|
||||
TIdentityAlicloudAuthsInsert,
|
||||
TIdentityAlicloudAuthsUpdate,
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
TIdentityAwsAuthsUpdate,
|
||||
@@ -786,6 +789,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityGcpAuthsInsert,
|
||||
TIdentityGcpAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityAliCloudAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityAlicloudAuths,
|
||||
TIdentityAlicloudAuthsInsert,
|
||||
TIdentityAlicloudAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
|
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -16,7 +16,8 @@ export const DynamicSecretLeasesSchema = z.object({
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
dynamicSecretId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
config: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>;
|
||||
|
25
backend/src/db/schemas/identity-alicloud-auths.ts
Normal file
25
backend/src/db/schemas/identity-alicloud-auths.ts
Normal 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>>;
|
@@ -18,7 +18,7 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid(),
|
||||
kubernetesHost: z.string(),
|
||||
kubernetesHost: z.string().nullable().optional(),
|
||||
encryptedCaCert: z.string().nullable().optional(),
|
||||
caCertIV: z.string().nullable().optional(),
|
||||
caCertTag: z.string().nullable().optional(),
|
||||
|
@@ -39,6 +39,7 @@ export * from "./group-project-memberships";
|
||||
export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identity-access-tokens";
|
||||
export * from "./identity-alicloud-auths";
|
||||
export * from "./identity-aws-auths";
|
||||
export * from "./identity-azure-auths";
|
||||
export * from "./identity-gcp-auths";
|
||||
|
@@ -80,6 +80,7 @@ export enum TableName {
|
||||
IdentityGcpAuth = "identity_gcp_auths",
|
||||
IdentityAzureAuth = "identity_azure_auths",
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityAliCloudAuth = "identity_alicloud_auths",
|
||||
IdentityAwsAuth = "identity_aws_auths",
|
||||
IdentityOciAuth = "identity_oci_auths",
|
||||
IdentityOidcAuth = "identity_oidc_auths",
|
||||
@@ -247,6 +248,7 @@ export enum IdentityAuthMethod {
|
||||
UNIVERSAL_AUTH = "universal-auth",
|
||||
KUBERNETES_AUTH = "kubernetes-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
ALICLOUD_AUTH = "alicloud-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth",
|
||||
OCI_AUTH = "oci-auth",
|
||||
|
@@ -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
|
||||
});
|
||||
};
|
@@ -36,7 +36,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
||||
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.path)
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||
config: z.any().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
@@ -48,7 +48,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
|
||||
id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
|
||||
}),
|
||||
response: {
|
||||
200: GroupsSchema
|
||||
200: GroupsSchema.extend({
|
||||
customRoleSlug: z.string().nullable()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
|
@@ -6,6 +6,7 @@ import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
|
||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||
import { registerCaCrlRouter } from "./certificate-authority-crl-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 { registerExternalKmsRouter } from "./external-kms-router";
|
||||
import { registerGatewayRouter } from "./gateway-router";
|
||||
@@ -71,6 +72,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
async (dynamicSecretRouter) => {
|
||||
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
|
||||
await dynamicSecretRouter.register(registerKubernetesDynamicSecretLeaseRouter, { prefix: "/leases/kubernetes" });
|
||||
},
|
||||
{ prefix: "/dynamic-secrets" }
|
||||
);
|
||||
|
@@ -6,6 +6,7 @@ import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-r
|
||||
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||
import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router";
|
||||
import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
|
||||
export * from "./secret-rotation-v2-router";
|
||||
@@ -17,6 +18,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||
[SecretRotation.MySqlCredentials]: registerMySqlCredentialsRotationRouter,
|
||||
[SecretRotation.OracleDBCredentials]: registerOracleDBCredentialsRotationRouter,
|
||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
|
||||
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
|
||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
|
||||
|
@@ -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
|
||||
});
|
@@ -7,6 +7,7 @@ import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret
|
||||
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-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 { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs";
|
||||
@@ -18,6 +19,7 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
MsSqlCredentialsRotationListItemSchema,
|
||||
MySqlCredentialsRotationListItemSchema,
|
||||
OracleDBCredentialsRotationListItemSchema,
|
||||
Auth0ClientSecretRotationListItemSchema,
|
||||
AzureClientSecretRotationListItemSchema,
|
||||
AwsIamUserSecretRotationListItemSchema,
|
||||
|
@@ -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({
|
||||
method: "GET",
|
||||
url: "/configs",
|
||||
|
@@ -0,0 +1,4 @@
|
||||
export * from "./oracledb-connection-enums";
|
||||
export * from "./oracledb-connection-fns";
|
||||
export * from "./oracledb-connection-schemas";
|
||||
export * from "./oracledb-connection-types";
|
@@ -0,0 +1,3 @@
|
||||
export enum OracleDBConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
@@ -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
|
||||
};
|
||||
};
|
@@ -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)
|
||||
});
|
@@ -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;
|
@@ -170,6 +170,12 @@ export enum EventType {
|
||||
REVOKE_IDENTITY_GCP_AUTH = "revoke-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",
|
||||
ADD_IDENTITY_AWS_AUTH = "add-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 {
|
||||
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
@@ -3272,6 +3325,11 @@ export type Event =
|
||||
| UpdateIdentityAwsAuthEvent
|
||||
| GetIdentityAwsAuthEvent
|
||||
| DeleteIdentityAwsAuthEvent
|
||||
| LoginIdentityAliCloudAuthEvent
|
||||
| AddIdentityAliCloudAuthEvent
|
||||
| UpdateIdentityAliCloudAuthEvent
|
||||
| GetIdentityAliCloudAuthEvent
|
||||
| DeleteIdentityAliCloudAuthEvent
|
||||
| LoginIdentityOciAuthEvent
|
||||
| AddIdentityOciAuthEvent
|
||||
| UpdateIdentityOciAuthEvent
|
||||
|
@@ -10,6 +10,7 @@ import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
||||
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
||||
import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types";
|
||||
|
||||
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
@@ -134,10 +135,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
|
||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||
await Promise.all(
|
||||
dynamicSecretLeases.map(({ externalEntityId }) =>
|
||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId, {
|
||||
projectId: folder.projectId
|
||||
})
|
||||
dynamicSecretLeases.map(({ externalEntityId, config }) =>
|
||||
selectedProvider.revoke(
|
||||
decryptedStoredInput,
|
||||
externalEntityId,
|
||||
{
|
||||
projectId: folder.projectId
|
||||
},
|
||||
config as TDynamicSecretLeaseConfig
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ import {
|
||||
TCreateDynamicSecretLeaseDTO,
|
||||
TDeleteDynamicSecretLeaseDTO,
|
||||
TDetailsDynamicSecretLeaseDTO,
|
||||
TDynamicSecretLeaseConfig,
|
||||
TListDynamicSecretLeasesDTO,
|
||||
TRenewDynamicSecretLeaseDTO
|
||||
} from "./dynamic-secret-lease-types";
|
||||
@@ -77,7 +78,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
ttl
|
||||
ttl,
|
||||
config
|
||||
}: TCreateDynamicSecretLeaseDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
@@ -163,7 +165,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
expireAt: expireAt.getTime(),
|
||||
usernameTemplate: dynamicSecretCfg.usernameTemplate,
|
||||
identity,
|
||||
metadata: { projectId }
|
||||
metadata: { projectId },
|
||||
config
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
||||
@@ -177,8 +180,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
expireAt,
|
||||
version: 1,
|
||||
dynamicSecretId: dynamicSecretCfg.id,
|
||||
externalEntityId: entityId
|
||||
externalEntityId: entityId,
|
||||
config
|
||||
});
|
||||
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
||||
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
||||
};
|
||||
@@ -259,7 +264,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
|
||||
if (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(
|
||||
@@ -342,7 +350,12 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
) as object;
|
||||
|
||||
const revokeResponse = await selectedProvider
|
||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId })
|
||||
.revoke(
|
||||
decryptedStoredInput,
|
||||
dynamicSecretLease.externalEntityId,
|
||||
{ projectId },
|
||||
dynamicSecretLease.config as TDynamicSecretLeaseConfig
|
||||
)
|
||||
.catch(async (err) => {
|
||||
// only propogate this error if forced is false
|
||||
if (!isForced) return { error: err as Error };
|
||||
|
@@ -10,6 +10,7 @@ export type TCreateDynamicSecretLeaseDTO = {
|
||||
environmentSlug: string;
|
||||
ttl?: string;
|
||||
projectSlug: string;
|
||||
config?: TDynamicSecretLeaseConfig;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDetailsDynamicSecretLeaseDTO = {
|
||||
@@ -41,3 +42,9 @@ export type TRenewDynamicSecretLeaseDTO = {
|
||||
ttl?: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDynamicSecretKubernetesLeaseConfig = {
|
||||
namespace?: string;
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;
|
||||
|
105
backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts
Normal file
105
backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts
Normal 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
|
||||
};
|
||||
};
|
@@ -6,6 +6,7 @@ import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
import { GcpIamProvider } from "./gcp-iam";
|
||||
import { KubernetesProvider } from "./kubernetes";
|
||||
import { LdapProvider } from "./ldap";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
|
||||
@@ -42,5 +43,6 @@ export const buildDynamicSecretProviders = ({
|
||||
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService })
|
||||
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.GcpIam]: GcpIamProvider()
|
||||
});
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import axios from "axios";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import handlebars from "handlebars";
|
||||
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 { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
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 {
|
||||
DynamicSecretKubernetesSchema,
|
||||
@@ -19,6 +20,9 @@ import {
|
||||
|
||||
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 = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
};
|
||||
@@ -36,7 +40,7 @@ const generateUsername = (usernameTemplate?: string | null) => {
|
||||
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
||||
if (!providerInputs.gatewayId) {
|
||||
if (!providerInputs.gatewayId && providerInputs.url) {
|
||||
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
||||
}
|
||||
|
||||
@@ -103,135 +107,173 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
const serviceAccountName = generateUsername();
|
||||
const roleBindingName = `${serviceAccountName}-role-binding`;
|
||||
|
||||
// 1. Create a test service account
|
||||
await axios.post(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`,
|
||||
{
|
||||
metadata: {
|
||||
name: serviceAccountName,
|
||||
namespace: providerInputs.namespace
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
const namespaces = providerInputs.namespace.split(",").map((namespace) => namespace.trim());
|
||||
|
||||
// 2. Create a test role binding
|
||||
const roleBindingUrl =
|
||||
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
||||
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`;
|
||||
|
||||
const roleBindingMetadata = {
|
||||
name: roleBindingName,
|
||||
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace })
|
||||
};
|
||||
|
||||
await axios.post(
|
||||
roleBindingUrl,
|
||||
{
|
||||
metadata: roleBindingMetadata,
|
||||
roleRef: {
|
||||
kind: providerInputs.roleType === KubernetesRoleType.ClusterRole ? "ClusterRole" : "Role",
|
||||
name: providerInputs.role,
|
||||
apiGroup: "rbac.authorization.k8s.io"
|
||||
},
|
||||
subjects: [
|
||||
// Test each namespace sequentially instead of in parallel to simplify cleanup
|
||||
for await (const namespace of namespaces) {
|
||||
try {
|
||||
// 1. Create a test service account
|
||||
await axios.post(
|
||||
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts`,
|
||||
{
|
||||
kind: "ServiceAccount",
|
||||
name: serviceAccountName,
|
||||
namespace: providerInputs.namespace
|
||||
metadata: {
|
||||
name: serviceAccountName,
|
||||
namespace
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
// 3. Request a token for the test service account
|
||||
await axios.post(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||
{
|
||||
spec: {
|
||||
expirationSeconds: 600, // 10 minutes
|
||||
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
||||
// 2. Create a test role binding
|
||||
const roleBindingUrl =
|
||||
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
||||
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings`;
|
||||
|
||||
const roleBindingMetadata = {
|
||||
name: roleBindingName,
|
||||
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace })
|
||||
};
|
||||
|
||||
await axios.post(
|
||||
roleBindingUrl,
|
||||
{
|
||||
metadata: roleBindingMetadata,
|
||||
roleRef: {
|
||||
kind: providerInputs.roleType === KubernetesRoleType.ClusterRole ? "ClusterRole" : "Role",
|
||||
name: providerInputs.role,
|
||||
apiGroup: "rbac.authorization.k8s.io"
|
||||
},
|
||||
subjects: [
|
||||
{
|
||||
kind: "ServiceAccount",
|
||||
name: serviceAccountName,
|
||||
namespace
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// 3. Request a token for the test service account
|
||||
await axios.post(
|
||||
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||
{
|
||||
spec: {
|
||||
expirationSeconds: 600, // 10 minutes
|
||||
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// 4. Cleanup: delete role binding and service account
|
||||
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||
await axios.delete(
|
||||
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings/${roleBindingName}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
// 4. Cleanup: delete role binding and service account
|
||||
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||
await axios.delete(
|
||||
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`,
|
||||
{
|
||||
await axios.delete(`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
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;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
});
|
||||
}
|
||||
|
||||
await axios.delete(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
throw new Error(`${mainErrorMessage}. ${cleanupInfo}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const serviceAccountStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
||||
@@ -247,17 +289,23 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
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 k8sPort = url.port ? Number(url.port) : 443;
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
@@ -315,11 +363,13 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
const create = async ({
|
||||
inputs,
|
||||
expireAt,
|
||||
usernameTemplate
|
||||
usernameTemplate,
|
||||
config
|
||||
}: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
config?: TDynamicSecretKubernetesLeaseConfig;
|
||||
}) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
@@ -331,26 +381,44 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
const serviceAccountName = generateUsername(usernameTemplate);
|
||||
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
|
||||
await axios.post(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`,
|
||||
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts`,
|
||||
{
|
||||
metadata: {
|
||||
name: serviceAccountName,
|
||||
namespace: providerInputs.namespace
|
||||
namespace
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
@@ -358,11 +426,11 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
const roleBindingUrl =
|
||||
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||
? `${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 = {
|
||||
name: roleBindingName,
|
||||
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace })
|
||||
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace })
|
||||
};
|
||||
|
||||
await axios.post(
|
||||
@@ -378,7 +446,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
{
|
||||
kind: "ServiceAccount",
|
||||
name: serviceAccountName,
|
||||
namespace: providerInputs.namespace
|
||||
namespace
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -386,18 +454,22 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// 3. Request a token for the service account
|
||||
const res = await axios.post<TKubernetesTokenRequest>(
|
||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||
{
|
||||
spec: {
|
||||
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
|
||||
@@ -408,12 +480,16 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
@@ -425,6 +501,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
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 res = await axios.post<TKubernetesTokenRequest>(
|
||||
@@ -439,19 +521,25 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
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 k8sGatewayHost = url.hostname;
|
||||
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 serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
||||
@@ -522,19 +616,25 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
const roleBindingName = `${entityId}-role-binding`;
|
||||
|
||||
const namespace = config?.namespace ?? providerInputs.namespace.split(",")[0].trim();
|
||||
|
||||
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||
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: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@@ -542,31 +642,44 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
}
|
||||
|
||||
// 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: {
|
||||
"Content-Type": "application/json",
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
||||
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||
},
|
||||
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||
? {
|
||||
httpsAgent
|
||||
}
|
||||
: {}),
|
||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
httpsAgent
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
};
|
||||
|
||||
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 k8sPort = url.port ? Number(url.port) : 443;
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
|
@@ -1,5 +1,10 @@
|
||||
import RE2 from "re2";
|
||||
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 = {
|
||||
length: number;
|
||||
required: {
|
||||
@@ -323,24 +328,54 @@ export const LdapSchema = z.union([
|
||||
export const DynamicSecretKubernetesSchema = z
|
||||
.discriminatedUnion("credentialType", [
|
||||
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(),
|
||||
ca: z.string().optional(),
|
||||
sslEnabled: z.boolean().default(false),
|
||||
credentialType: z.literal(KubernetesCredentialType.Static),
|
||||
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(),
|
||||
audiences: z.array(z.string().trim().min(1)),
|
||||
authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api)
|
||||
}),
|
||||
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(),
|
||||
ca: z.string().optional(),
|
||||
sslEnabled: z.boolean().default(false),
|
||||
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(),
|
||||
audiences: z.array(z.string().trim().min(1)),
|
||||
roleType: z.nativeEnum(KubernetesRoleType),
|
||||
@@ -356,12 +391,21 @@ export const DynamicSecretKubernetesSchema = z
|
||||
message: "When auth method is set to Gateway, a gateway must be selected"
|
||||
});
|
||||
}
|
||||
if ((data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) && !data.clusterToken) {
|
||||
ctx.addIssue({
|
||||
path: ["clusterToken"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "When auth method is set to Manual Token, a cluster token must be provided"
|
||||
});
|
||||
if (data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) {
|
||||
if (!data.clusterToken) {
|
||||
ctx.addIssue({
|
||||
path: ["clusterToken"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
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"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra",
|
||||
@@ -443,7 +491,8 @@ export enum DynamicSecretProviders {
|
||||
Totp = "totp",
|
||||
SapAse = "sap-ase",
|
||||
Kubernetes = "kubernetes",
|
||||
Vertica = "vertica"
|
||||
Vertica = "vertica",
|
||||
GcpIam = "gcp-iam"
|
||||
}
|
||||
|
||||
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.Totp), inputs: DynamicSecretTotpSchema }),
|
||||
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 = {
|
||||
@@ -475,10 +525,16 @@ export type TDynamicProviderFns = {
|
||||
name: string;
|
||||
};
|
||||
metadata: { projectId: string };
|
||||
config?: TDynamicSecretLeaseConfig;
|
||||
}) => Promise<{ entityId: string; data: unknown }>;
|
||||
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
|
||||
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: (
|
||||
inputs: unknown,
|
||||
entityId: string,
|
||||
|
@@ -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 {
|
||||
...groupOrm,
|
||||
findGroups,
|
||||
findByOrgId,
|
||||
findAllGroupPossibleMembers,
|
||||
findGroupsByProjectId,
|
||||
...groupOrm
|
||||
findById
|
||||
};
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import {
|
||||
@@ -246,7 +247,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
const { policy } = secretApprovalRequest;
|
||||
const { hasRole } = await permissionService.getProjectPermission({
|
||||
const { hasRole, permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -262,6 +263,12 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||
}
|
||||
|
||||
const hasSecretReadAccess = permission.can(
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
const hiddenSecretValue = "******";
|
||||
|
||||
let secrets;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
@@ -278,9 +285,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.version,
|
||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||
secretValue:
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
el.secret && el.secret.isRotatedSecret
|
||||
secretValue: !hasSecretReadAccess
|
||||
? hiddenSecretValue
|
||||
: el.secret && el.secret.isRotatedSecret
|
||||
? undefined
|
||||
: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
@@ -293,9 +300,11 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretKey: el.secret.key,
|
||||
id: el.secret.id,
|
||||
version: el.secret.version,
|
||||
secretValue: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
: "",
|
||||
secretValue: !hasSecretReadAccess
|
||||
? hiddenSecretValue
|
||||
: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
: "",
|
||||
secretComment: el.secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
||||
: ""
|
||||
@@ -306,9 +315,11 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretKey: el.secretVersion.key,
|
||||
id: el.secretVersion.id,
|
||||
version: el.secretVersion.version,
|
||||
secretValue: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
: "",
|
||||
secretValue: !hasSecretReadAccess
|
||||
? hiddenSecretValue
|
||||
: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
: "",
|
||||
secretComment: el.secretVersion.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||
: "",
|
||||
|
@@ -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.
|
||||
* First checks if the credential exists before attempting revocation.
|
||||
*/
|
||||
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 endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
export * from "./oracledb-credentials-rotation-constants";
|
||||
export * from "./oracledb-credentials-rotation-schemas";
|
||||
export * from "./oracledb-credentials-rotation-types";
|
@@ -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"
|
||||
}
|
||||
}
|
||||
};
|
@@ -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
|
||||
});
|
@@ -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;
|
||||
};
|
@@ -2,6 +2,7 @@ export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials",
|
||||
MySqlCredentials = "mysql-credentials",
|
||||
OracleDBCredentials = "oracledb-credentials",
|
||||
Auth0ClientSecret = "auth0-client-secret",
|
||||
AzureClientSecret = "azure-client-secret",
|
||||
AwsIamUserSecret = "aws-iam-user-secret",
|
||||
|
@@ -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 { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-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 { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||
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.MsSqlCredentials]: MSSQL_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.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
|
||||
|
@@ -5,6 +5,7 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
|
||||
[SecretRotation.MySqlCredentials]: "MySQL Credentials",
|
||||
[SecretRotation.OracleDBCredentials]: "OracleDB Credentials",
|
||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
|
||||
[SecretRotation.AzureClientSecret]: "Azure Client 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.MsSqlCredentials]: AppConnection.MsSql,
|
||||
[SecretRotation.MySqlCredentials]: AppConnection.MySql,
|
||||
[SecretRotation.OracleDBCredentials]: AppConnection.OracleDB,
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
|
||||
|
@@ -123,6 +123,7 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
|
||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.MySqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.OracleDBCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
|
||||
|
@@ -45,6 +45,12 @@ import {
|
||||
TMySqlCredentialsRotationListItem,
|
||||
TMySqlCredentialsRotationWithConnection
|
||||
} from "./mysql-credentials";
|
||||
import {
|
||||
TOracleDBCredentialsRotation,
|
||||
TOracleDBCredentialsRotationInput,
|
||||
TOracleDBCredentialsRotationListItem,
|
||||
TOracleDBCredentialsRotationWithConnection
|
||||
} from "./oracledb-credentials";
|
||||
import {
|
||||
TPostgresCredentialsRotation,
|
||||
TPostgresCredentialsRotationInput,
|
||||
@@ -58,6 +64,7 @@ export type TSecretRotationV2 =
|
||||
| TPostgresCredentialsRotation
|
||||
| TMsSqlCredentialsRotation
|
||||
| TMySqlCredentialsRotation
|
||||
| TOracleDBCredentialsRotation
|
||||
| TAuth0ClientSecretRotation
|
||||
| TAzureClientSecretRotation
|
||||
| TLdapPasswordRotation
|
||||
@@ -67,6 +74,7 @@ export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
| TMsSqlCredentialsRotationWithConnection
|
||||
| TMySqlCredentialsRotationWithConnection
|
||||
| TOracleDBCredentialsRotationWithConnection
|
||||
| TAuth0ClientSecretRotationWithConnection
|
||||
| TAzureClientSecretRotationWithConnection
|
||||
| TLdapPasswordRotationWithConnection
|
||||
@@ -83,6 +91,7 @@ export type TSecretRotationV2Input =
|
||||
| TPostgresCredentialsRotationInput
|
||||
| TMsSqlCredentialsRotationInput
|
||||
| TMySqlCredentialsRotationInput
|
||||
| TOracleDBCredentialsRotationInput
|
||||
| TAuth0ClientSecretRotationInput
|
||||
| TAzureClientSecretRotationInput
|
||||
| TLdapPasswordRotationInput
|
||||
@@ -92,6 +101,7 @@ export type TSecretRotationV2ListItem =
|
||||
| TPostgresCredentialsRotationListItem
|
||||
| TMsSqlCredentialsRotationListItem
|
||||
| TMySqlCredentialsRotationListItem
|
||||
| TOracleDBCredentialsRotationListItem
|
||||
| TAuth0ClientSecretRotationListItem
|
||||
| TAzureClientSecretRotationListItem
|
||||
| TLdapPasswordRotationListItem
|
||||
|
@@ -1,18 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
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 { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-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 { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret";
|
||||
|
||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema,
|
||||
MySqlCredentialsRotationSchema,
|
||||
OracleDBCredentialsRotationSchema,
|
||||
Auth0ClientSecretRotationSchema,
|
||||
AzureClientSecretRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
|
@@ -2,14 +2,15 @@ import { z } from "zod";
|
||||
|
||||
import { TMsSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/mssql-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 { SqlCredentialsRotationGeneratedCredentialsSchema } from "./sql-credentials-rotation-schemas";
|
||||
import { SqlCredentialsRotationGeneratedCredentialsSchema } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas";
|
||||
|
||||
export type TSqlCredentialsRotationWithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
| TMsSqlCredentialsRotationWithConnection
|
||||
| TMySqlCredentialsRotationWithConnection;
|
||||
| TMySqlCredentialsRotationWithConnection
|
||||
| TOracleDBCredentialsRotationWithConnection;
|
||||
|
||||
export type TSqlCredentialsRotationGeneratedCredentials = z.infer<
|
||||
typeof SqlCredentialsRotationGeneratedCredentialsSchema
|
||||
|
@@ -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
|
||||
return {
|
||||
query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,
|
||||
|
@@ -10,7 +10,8 @@ export enum TDbProviderClients {
|
||||
// mysql and maria db
|
||||
MySql = "mysql",
|
||||
|
||||
MsSqlServer = "mssql"
|
||||
MsSqlServer = "mssql",
|
||||
OracleDB = "oracledb"
|
||||
}
|
||||
|
||||
export enum TAwsProviderSystems {
|
||||
|
@@ -21,6 +21,7 @@ export enum ApiDocsTags {
|
||||
TokenAuth = "Token Auth",
|
||||
UniversalAuth = "Universal Auth",
|
||||
GcpAuth = "GCP Auth",
|
||||
AliCloudAuth = "Alibaba Cloud Auth",
|
||||
AwsAuth = "AWS Auth",
|
||||
OciAuth = "OCI Auth",
|
||||
AzureAuth = "Azure Auth",
|
||||
@@ -243,6 +244,43 @@ export const LDAP_AUTH = {
|
||||
}
|
||||
} 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 = {
|
||||
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.",
|
||||
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."
|
||||
},
|
||||
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;
|
||||
export const SECRET_TAGS = {
|
||||
@@ -2162,6 +2208,11 @@ export const AppConnections = {
|
||||
code: "The OAuth code 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: {
|
||||
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.",
|
||||
@@ -2276,6 +2327,10 @@ export const SecretSyncs = {
|
||||
"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."
|
||||
},
|
||||
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: {
|
||||
scope: "The Google project scope that secrets should be synced to.",
|
||||
projectId: "The ID of the Google project secrets should be synced to.",
|
||||
|
@@ -149,8 +149,8 @@ const setupProxyServer = async ({
|
||||
protocol = GatewayProxyProtocol.Tcp,
|
||||
httpsAgent
|
||||
}: {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
targetHost?: string;
|
||||
targetPort?: number;
|
||||
relayPort: number;
|
||||
relayHost: string;
|
||||
tlsOptions: TGatewayTlsOptions;
|
||||
@@ -183,27 +183,44 @@ const setupProxyServer = async ({
|
||||
let command: string;
|
||||
|
||||
if (protocol === GatewayProxyProtocol.Http) {
|
||||
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
|
||||
command = `FORWARD-HTTP ${targetUrl}`;
|
||||
logger.debug(`Using HTTP proxy mode: ${command.trim()}`);
|
||||
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`
|
||||
});
|
||||
}
|
||||
|
||||
// extract ca certificate from httpsAgent if present
|
||||
if (httpsAgent && targetHost.startsWith("https://")) {
|
||||
const agentOptions = httpsAgent.options;
|
||||
if (agentOptions && agentOptions.ca) {
|
||||
const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca;
|
||||
const caB64 = Buffer.from(caCert as string).toString("base64");
|
||||
command += ` ca=${caB64}`;
|
||||
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
|
||||
command = `FORWARD-HTTP ${targetUrl}`;
|
||||
logger.debug(`Using HTTP proxy mode, custom target URL provided [command=${command.trim()}]`);
|
||||
|
||||
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
|
||||
command += ` verify=${rejectUnauthorized}`;
|
||||
// extract ca certificate from httpsAgent if present
|
||||
if (httpsAgent && targetHost.startsWith("https://")) {
|
||||
const agentOptions = httpsAgent.options;
|
||||
if (agentOptions && agentOptions.ca) {
|
||||
const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca;
|
||||
const caB64 = Buffer.from(caCert as string).toString("base64");
|
||||
command += ` ca=${caB64}`;
|
||||
|
||||
logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`);
|
||||
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
|
||||
command += ` verify=${rejectUnauthorized}`;
|
||||
|
||||
logger.debug(`Using HTTP proxy mode, custom target URL provided [command=${command.trim()}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command += "\n";
|
||||
} 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
|
||||
command = `FORWARD-TCP ${targetHost}:${targetPort}\n`;
|
||||
logger.debug(`Using TCP proxy mode: ${command.trim()}`);
|
||||
|
@@ -10,12 +10,13 @@ export enum GatewayProxyProtocol {
|
||||
}
|
||||
|
||||
export enum GatewayHttpProxyActions {
|
||||
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token"
|
||||
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token",
|
||||
UseGatewayK8sServiceAccount = "use-k8s-sa"
|
||||
}
|
||||
|
||||
export interface IGatewayProxyOptions {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
targetHost?: string;
|
||||
targetPort?: number;
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: TGatewayTlsOptions;
|
||||
|
@@ -172,6 +172,8 @@ import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||
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 { 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 { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||
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 identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAliCloudAuthDAL = identityAliCloudAuthDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||
const identityOciAuthDAL = identityOciAuthDALFactory(db);
|
||||
@@ -1482,6 +1485,14 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityAliCloudAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
@@ -1931,6 +1942,7 @@ export const registerRoutes = async (
|
||||
identityUa: identityUaService,
|
||||
identityKubernetesAuth: identityKubernetesAuthService,
|
||||
identityGcpAuth: identityGcpAuthService,
|
||||
identityAliCloudAuth: identityAliCloudAuthService,
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
identityOciAuth: identityOciAuthService,
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
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 { ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -19,6 +23,10 @@ import {
|
||||
AzureClientSecretsConnectionListItemSchema,
|
||||
SanitizedAzureClientSecretsConnectionSchema
|
||||
} from "@app/services/app-connection/azure-client-secrets";
|
||||
import {
|
||||
AzureDevOpsConnectionListItemSchema,
|
||||
SanitizedAzureDevOpsConnectionSchema
|
||||
} from "@app/services/app-connection/azure-devops/azure-devops-schemas";
|
||||
import {
|
||||
AzureKeyVaultConnectionListItemSchema,
|
||||
SanitizedAzureKeyVaultConnectionSchema
|
||||
@@ -75,6 +83,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedGcpConnectionSchema.options,
|
||||
...SanitizedAzureKeyVaultConnectionSchema.options,
|
||||
...SanitizedAzureAppConfigurationConnectionSchema.options,
|
||||
...SanitizedAzureDevOpsConnectionSchema.options,
|
||||
...SanitizedDatabricksConnectionSchema.options,
|
||||
...SanitizedHumanitecConnectionSchema.options,
|
||||
...SanitizedTerraformCloudConnectionSchema.options,
|
||||
@@ -90,6 +99,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedLdapConnectionSchema.options,
|
||||
...SanitizedTeamCityConnectionSchema.options,
|
||||
...SanitizedOCIConnectionSchema.options,
|
||||
...SanitizedOracleDBConnectionSchema.options,
|
||||
...SanitizedOnePassConnectionSchema.options
|
||||
]);
|
||||
|
||||
@@ -100,6 +110,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
GcpConnectionListItemSchema,
|
||||
AzureKeyVaultConnectionListItemSchema,
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
AzureDevOpsConnectionListItemSchema,
|
||||
DatabricksConnectionListItemSchema,
|
||||
HumanitecConnectionListItemSchema,
|
||||
TerraformCloudConnectionListItemSchema,
|
||||
@@ -115,6 +126,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
LdapConnectionListItemSchema,
|
||||
TeamCityConnectionListItemSchema,
|
||||
OCIConnectionListItemSchema,
|
||||
OracleDBConnectionListItemSchema,
|
||||
OnePassConnectionListItemSchema
|
||||
]);
|
||||
|
||||
|
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,4 +1,5 @@
|
||||
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 { registerOnePassConnectionRouter } from "./1password-connection-router";
|
||||
@@ -6,6 +7,7 @@ import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-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 { registerCamundaConnectionRouter } from "./camunda-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.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
|
||||
[AppConnection.AzureDevOps]: registerAzureDevOpsConnectionRouter,
|
||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
||||
@@ -48,5 +51,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.LDAP]: registerLdapConnectionRouter,
|
||||
[AppConnection.TeamCity]: registerTeamCityConnectionRouter,
|
||||
[AppConnection.OCI]: registerOCIConnectionRouter,
|
||||
[AppConnection.OracleDB]: registerOracleDBConnectionRouter,
|
||||
[AppConnection.OnePass]: registerOnePassConnectionRouter
|
||||
};
|
||||
|
381
backend/src/server/routes/v1/identity-alicloud-auth-router.ts
Normal file
381
backend/src/server/routes/v1/identity-alicloud-auth-router.ts
Normal 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 };
|
||||
}
|
||||
});
|
||||
};
|
@@ -108,17 +108,21 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.nullable()
|
||||
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
(val) => {
|
||||
if (val === null) return true;
|
||||
|
||||
return characterValidator([
|
||||
CharacterType.Alphabets,
|
||||
CharacterType.Numbers,
|
||||
CharacterType.Colon,
|
||||
CharacterType.Period,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Hyphen
|
||||
])(val),
|
||||
])(val);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"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)
|
||||
})
|
||||
.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) {
|
||||
ctx.addIssue({
|
||||
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"
|
||||
});
|
||||
}
|
||||
|
||||
if (data.accessTokenTTL > data.accessTokenMaxTTL) {
|
||||
ctx.addIssue({
|
||||
path: ["accessTokenTTL"],
|
||||
@@ -203,7 +215,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH,
|
||||
metadata: {
|
||||
identityId: identityKubernetesAuth.identityId,
|
||||
kubernetesHost: identityKubernetesAuth.kubernetesHost,
|
||||
kubernetesHost: identityKubernetesAuth.kubernetesHost ?? "",
|
||||
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
||||
allowedNames: identityKubernetesAuth.allowedNames,
|
||||
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||
@@ -243,6 +255,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
|
||||
.refine(
|
||||
@@ -345,7 +358,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH,
|
||||
metadata: {
|
||||
identityId: identityKubernetesAuth.identityId,
|
||||
kubernetesHost: identityKubernetesAuth.kubernetesHost,
|
||||
kubernetesHost: identityKubernetesAuth.kubernetesHost ?? "",
|
||||
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
||||
allowedNames: identityKubernetesAuth.allowedNames,
|
||||
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||
|
@@ -15,6 +15,7 @@ import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAliCloudAuthRouter } from "./identity-alicloud-auth-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-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(registerIdentityGcpAuthRouter);
|
||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||
await authRouter.register(registerIdentityAliCloudAuthRouter);
|
||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||
await authRouter.register(registerIdentityAzureAuthRouter);
|
||||
await authRouter.register(registerIdentityOciAuthRouter);
|
||||
|
@@ -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
|
||||
});
|
@@ -5,6 +5,7 @@ import { registerOnePassSyncRouter } from "./1password-sync-router";
|
||||
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
||||
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-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 { registerCamundaSyncRouter } from "./camunda-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.AzureKeyVault]: registerAzureKeyVaultSyncRouter,
|
||||
[SecretSync.AzureAppConfiguration]: registerAzureAppConfigurationSyncRouter,
|
||||
[SecretSync.AzureDevOps]: registerAzureDevOpsSyncRouter,
|
||||
[SecretSync.Databricks]: registerDatabricksSyncRouter,
|
||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||
|
@@ -19,6 +19,7 @@ import {
|
||||
AzureAppConfigurationSyncListItemSchema,
|
||||
AzureAppConfigurationSyncSchema
|
||||
} 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 { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
|
||||
@@ -38,6 +39,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
GcpSyncSchema,
|
||||
AzureKeyVaultSyncSchema,
|
||||
AzureAppConfigurationSyncSchema,
|
||||
AzureDevOpsSyncSchema,
|
||||
DatabricksSyncSchema,
|
||||
HumanitecSyncSchema,
|
||||
TerraformCloudSyncSchema,
|
||||
@@ -57,6 +59,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
GcpSyncListItemSchema,
|
||||
AzureKeyVaultSyncListItemSchema,
|
||||
AzureAppConfigurationSyncListItemSchema,
|
||||
AzureDevOpsSyncListItemSchema,
|
||||
DatabricksSyncListItemSchema,
|
||||
HumanitecSyncListItemSchema,
|
||||
TerraformCloudSyncListItemSchema,
|
||||
|
@@ -7,6 +7,7 @@ export enum AppConnection {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
AzureClientSecrets = "azure-client-secrets",
|
||||
AzureDevOps = "azure-devops",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Vercel = "vercel",
|
||||
@@ -20,6 +21,7 @@ export enum AppConnection {
|
||||
LDAP = "ldap",
|
||||
TeamCity = "teamcity",
|
||||
OCI = "oci",
|
||||
OracleDB = "oracledb",
|
||||
OnePass = "1password"
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
OCIConnectionMethod,
|
||||
validateOCIConnectionCredentials
|
||||
} 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 { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@@ -39,6 +40,11 @@ import {
|
||||
getAzureClientSecretsConnectionListItem,
|
||||
validateAzureClientSecretsConnectionCredentials
|
||||
} from "./azure-client-secrets";
|
||||
import { AzureDevOpsConnectionMethod } from "./azure-devops/azure-devops-enums";
|
||||
import {
|
||||
getAzureDevopsConnectionListItem,
|
||||
validateAzureDevOpsConnectionCredentials
|
||||
} from "./azure-devops/azure-devops-fns";
|
||||
import {
|
||||
AzureKeyVaultConnectionMethod,
|
||||
getAzureKeyVaultConnectionListItem,
|
||||
@@ -98,6 +104,7 @@ export const listAppConnectionOptions = () => {
|
||||
getGcpConnectionListItem(),
|
||||
getAzureKeyVaultConnectionListItem(),
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getAzureDevopsConnectionListItem(),
|
||||
getDatabricksConnectionListItem(),
|
||||
getHumanitecConnectionListItem(),
|
||||
getTerraformCloudConnectionListItem(),
|
||||
@@ -113,6 +120,7 @@ export const listAppConnectionOptions = () => {
|
||||
getLdapConnectionListItem(),
|
||||
getTeamCityConnectionListItem(),
|
||||
getOCIConnectionListItem(),
|
||||
getOracleDBConnectionListItem(),
|
||||
getOnePassConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
@@ -173,6 +181,7 @@ export const validateAppConnectionCredentials = async (
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureClientSecrets]:
|
||||
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
@@ -186,6 +195,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
@@ -201,6 +211,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
case AzureDevOpsConnectionMethod.OAuth:
|
||||
return "OAuth";
|
||||
case AwsConnectionMethod.AccessKey:
|
||||
case OCIConnectionMethod.AccessKey:
|
||||
@@ -221,10 +232,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
case OracleDBConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
case AzureDevOpsConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
@@ -270,6 +283,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Postgres]: 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.TeamCity]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.OCI]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.OnePass]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
|
@@ -8,6 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
||||
[AppConnection.AzureDevOps]: "Azure DevOps",
|
||||
[AppConnection.Databricks]: "Databricks",
|
||||
[AppConnection.Humanitec]: "Humanitec",
|
||||
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||
@@ -22,6 +23,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.LDAP]: "LDAP",
|
||||
[AppConnection.TeamCity]: "TeamCity",
|
||||
[AppConnection.OCI]: "OCI",
|
||||
[AppConnection.OracleDB]: "OracleDB",
|
||||
[AppConnection.OnePass]: "1Password"
|
||||
};
|
||||
|
||||
@@ -33,6 +35,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureDevOps]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
|
||||
@@ -46,6 +49,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.LDAP]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.TeamCity]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.OCI]: AppConnectionPlanType.Enterprise,
|
||||
[AppConnection.OracleDB]: AppConnectionPlanType.Enterprise,
|
||||
[AppConnection.OnePass]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.MySql]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
@@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
|
||||
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 { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
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 { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
||||
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 { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
@@ -84,6 +87,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
||||
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
||||
@@ -99,6 +103,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
|
||||
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema,
|
||||
[AppConnection.OracleDB]: ValidateOracleDBConnectionCredentialsSchema,
|
||||
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
@@ -498,6 +503,7 @@ export const appConnectionServiceFactory = ({
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
azureDevOps: azureDevOpsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
|
@@ -4,6 +4,11 @@ import {
|
||||
TOCIConnectionInput,
|
||||
TValidateOCIConnectionCredentialsSchema
|
||||
} 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 { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -39,6 +44,12 @@ import {
|
||||
TAzureClientSecretsConnectionInput,
|
||||
TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||
} from "./azure-client-secrets";
|
||||
import {
|
||||
TAzureDevOpsConnection,
|
||||
TAzureDevOpsConnectionConfig,
|
||||
TAzureDevOpsConnectionInput,
|
||||
TValidateAzureDevOpsConnectionCredentialsSchema
|
||||
} from "./azure-devops/azure-devops-types";
|
||||
import {
|
||||
TAzureKeyVaultConnection,
|
||||
TAzureKeyVaultConnectionConfig,
|
||||
@@ -132,6 +143,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TGcpConnection
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
| TAzureDevOpsConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TTerraformCloudConnection
|
||||
@@ -147,12 +159,13 @@ export type TAppConnection = { id: string } & (
|
||||
| TLdapConnection
|
||||
| TTeamCityConnection
|
||||
| TOCIConnection
|
||||
| TOracleDBConnection
|
||||
| TOnePassConnection
|
||||
);
|
||||
|
||||
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 } & (
|
||||
| TAwsConnectionInput
|
||||
@@ -161,6 +174,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TGcpConnectionInput
|
||||
| TAzureKeyVaultConnectionInput
|
||||
| TAzureAppConfigurationConnectionInput
|
||||
| TAzureDevOpsConnectionInput
|
||||
| TDatabricksConnectionInput
|
||||
| THumanitecConnectionInput
|
||||
| TTerraformCloudConnectionInput
|
||||
@@ -176,10 +190,15 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TLdapConnectionInput
|
||||
| TTeamCityConnectionInput
|
||||
| TOCIConnectionInput
|
||||
| TOracleDBConnectionInput
|
||||
| TOnePassConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput | TMySqlConnectionInput;
|
||||
export type TSqlConnectionInput =
|
||||
| TPostgresConnectionInput
|
||||
| TMsSqlConnectionInput
|
||||
| TMySqlConnectionInput
|
||||
| TOracleDBConnectionInput;
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnectionInput,
|
||||
@@ -197,6 +216,7 @@ export type TAppConnectionConfig =
|
||||
| TGcpConnectionConfig
|
||||
| TAzureKeyVaultConnectionConfig
|
||||
| TAzureAppConfigurationConnectionConfig
|
||||
| TAzureDevOpsConnectionConfig
|
||||
| TAzureClientSecretsConnectionConfig
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
@@ -220,6 +240,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||
| TValidateAzureDevOpsConnectionCredentialsSchema
|
||||
| TValidateDatabricksConnectionCredentialsSchema
|
||||
| TValidateHumanitecConnectionCredentialsSchema
|
||||
| TValidatePostgresConnectionCredentialsSchema
|
||||
@@ -234,6 +255,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateLdapConnectionCredentialsSchema
|
||||
| TValidateTeamCityConnectionCredentialsSchema
|
||||
| TValidateOCIConnectionCredentialsSchema
|
||||
| TValidateOracleDBConnectionCredentialsSchema
|
||||
| TValidateOnePassConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
|
@@ -0,0 +1,4 @@
|
||||
export enum AzureDevOpsConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
AccessToken = "access-token"
|
||||
}
|
@@ -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}`
|
||||
});
|
||||
}
|
||||
};
|
@@ -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()
|
||||
});
|
@@ -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
|
||||
};
|
||||
};
|
@@ -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[];
|
||||
}
|
@@ -16,7 +16,8 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
const SQL_CONNECTION_CLIENT_MAP = {
|
||||
[AppConnection.Postgres]: "pg",
|
||||
[AppConnection.MsSql]: "mssql",
|
||||
[AppConnection.MySql]: "mysql2"
|
||||
[AppConnection.MySql]: "mysql2",
|
||||
[AppConnection.OracleDB]: "oracledb"
|
||||
};
|
||||
|
||||
const getConnectionConfig = ({
|
||||
@@ -57,6 +58,17 @@ const getConnectionConfig = ({
|
||||
: false
|
||||
};
|
||||
}
|
||||
|
||||
case AppConnection.OracleDB: {
|
||||
return {
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
sslCA: sslCertificate,
|
||||
sslServerDNMatch: sslRejectUnauthorized
|
||||
}
|
||||
: false
|
||||
};
|
||||
}
|
||||
default:
|
||||
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.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 (
|
||||
|
@@ -120,7 +120,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||
|
||||
return docs.map((doc) => {
|
||||
// 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 {
|
||||
...doc,
|
||||
resourceType: "secret",
|
||||
@@ -168,7 +168,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||
);
|
||||
|
||||
return docs
|
||||
.filter((doc) => doc.secretKey && doc.secretVersion && doc.secretId)
|
||||
.filter((doc) => doc.secretKey && doc.secretVersion !== null && doc.secretId)
|
||||
.map(
|
||||
(doc): SecretCommitChange => ({
|
||||
...doc,
|
||||
@@ -209,7 +209,7 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||
);
|
||||
|
||||
return docs
|
||||
.filter((doc) => doc.folderName && doc.folderVersion && doc.folderChangeId)
|
||||
.filter((doc) => doc.folderName && doc.folderVersion !== null && doc.folderChangeId)
|
||||
.map(
|
||||
(doc): FolderCommitChange => ({
|
||||
...doc,
|
||||
|
@@ -815,7 +815,7 @@ export const folderCommitServiceFactory = ({
|
||||
encryptedComment: version1.encryptedComment
|
||||
? 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)
|
||||
};
|
||||
const version2Reshaped = {
|
||||
@@ -826,7 +826,7 @@ export const folderCommitServiceFactory = ({
|
||||
encryptedComment: version2.encryptedComment
|
||||
? 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)
|
||||
};
|
||||
return (
|
||||
|
@@ -28,6 +28,11 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityUniversalAuth}.id`
|
||||
)
|
||||
.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.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`)
|
||||
@@ -44,6 +49,10 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
.select(
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
|
||||
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.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
|
||||
@@ -62,6 +71,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
...doc,
|
||||
trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa,
|
||||
trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp,
|
||||
trustedIpsAliCloudAuth: doc.accessTokenTrustedIpsAliCloud,
|
||||
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
|
||||
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
|
||||
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
|
||||
|
@@ -193,6 +193,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
const trustedIpsMap: Record<IdentityAuthMethod, unknown> = {
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
|
||||
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
|
||||
[IdentityAuthMethod.ALICLOUD_AUTH]: identityAccessToken.trustedIpsAliCloudAuth,
|
||||
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
|
||||
[IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth,
|
||||
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
|
||||
|
@@ -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);
|
||||
};
|
@@ -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
|
||||
};
|
||||
};
|
@@ -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;
|
||||
};
|
@@ -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(", "));
|
@@ -94,7 +94,9 @@ export const identityAwsAuthServiceFactory = ({
|
||||
|
||||
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "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)) {
|
||||
throw new BadRequestError({ message: "Invalid AWS region" });
|
||||
|
@@ -40,7 +40,8 @@ export type TAwsGetCallerIdentityHeaders = {
|
||||
"X-Amz-Date": string;
|
||||
"Content-Length": number;
|
||||
"x-amz-security-token": string;
|
||||
Authorization: string;
|
||||
Authorization?: string;
|
||||
authorization?: string;
|
||||
};
|
||||
|
||||
export type TGetCallerIdentityResponse = {
|
||||
|
@@ -72,8 +72,8 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
const $gatewayProxyWrapper = async <T>(
|
||||
inputs: {
|
||||
gatewayId: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
targetHost?: string;
|
||||
targetPort?: number;
|
||||
caCert?: string;
|
||||
reviewTokenThroughGateway: boolean;
|
||||
},
|
||||
@@ -104,11 +104,15 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey.toString()
|
||||
},
|
||||
// we always pass this, because its needed for both tcp and http protocol
|
||||
httpsAgent: new https.Agent({
|
||||
ca: inputs.caCert,
|
||||
rejectUnauthorized: Boolean(inputs.caCert)
|
||||
})
|
||||
// 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({
|
||||
ca: inputs.caCert,
|
||||
rejectUnauthorized: Boolean(inputs.caCert)
|
||||
})
|
||||
}
|
||||
: {})
|
||||
}
|
||||
);
|
||||
|
||||
@@ -142,8 +146,15 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
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");
|
||||
|
||||
if (!host || !identityKubernetesAuth.kubernetesHost) {
|
||||
throw new BadRequestError({
|
||||
message: "Kubernetes host is required when token review mode is set to API"
|
||||
});
|
||||
}
|
||||
|
||||
let tokenReviewerJwt = "";
|
||||
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
||||
tokenReviewerJwt = decryptor({
|
||||
@@ -211,11 +222,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const tokenReviewCallbackThroughGateway = async (
|
||||
host: string = identityKubernetesAuth.kubernetesHost,
|
||||
port?: number,
|
||||
httpsAgent?: https.Agent
|
||||
) => {
|
||||
const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => {
|
||||
logger.info(
|
||||
{
|
||||
host,
|
||||
@@ -224,11 +231,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
|
||||
);
|
||||
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
|
||||
const res = await axios
|
||||
.post<TCreateTokenReviewResponse>(
|
||||
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||
`${host}:${port}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||
{
|
||||
apiVersion: "authentication.k8s.io/v1",
|
||||
kind: "TokenReview",
|
||||
@@ -240,11 +245,10 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken
|
||||
"x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
timeout: 10000,
|
||||
...(httpsAgent ? { httpsAgent } : {})
|
||||
timeout: 10000
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
@@ -273,29 +277,6 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
let data: TCreateTokenReviewResponse | undefined;
|
||||
|
||||
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) {
|
||||
throw new BadRequestError({
|
||||
message: "Gateway ID is required when token review mode is set to Gateway"
|
||||
@@ -305,14 +286,17 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
data = await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: identityKubernetesAuth.gatewayId,
|
||||
targetHost: `${cleanedProtocol}://${k8sHost}`, // note(daniel): must include the protocol (https|http)
|
||||
targetPort: k8sPort ? Number(k8sPort) : 443,
|
||||
caCert,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
tokenReviewCallbackThroughGateway
|
||||
);
|
||||
} 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;
|
||||
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
|
||||
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
|
||||
|
@@ -12,7 +12,7 @@ export enum IdentityKubernetesAuthTokenReviewMode {
|
||||
|
||||
export type TAttachKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
kubernetesHost: string;
|
||||
kubernetesHost: string | null;
|
||||
caCert: string;
|
||||
tokenReviewerJwt?: string;
|
||||
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
|
||||
@@ -29,7 +29,7 @@ export type TAttachKubernetesAuthDTO = {
|
||||
|
||||
export type TUpdateKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
kubernetesHost?: string;
|
||||
kubernetesHost?: string | null;
|
||||
caCert?: string;
|
||||
tokenReviewerJwt?: string | null;
|
||||
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
|
||||
|
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
||||
import {
|
||||
TableName,
|
||||
TIdentities,
|
||||
TIdentityAlicloudAuths,
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAzureAuths,
|
||||
TIdentityGcpAuths,
|
||||
@@ -57,6 +58,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityAliCloudAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityAliCloudAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityAwsAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
@@ -111,6 +117,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project),
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
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("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
@@ -267,6 +274,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAlicloudAuths>(
|
||||
TableName.IdentityAliCloudAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityAliCloudAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAwsAuths>(
|
||||
TableName.IdentityAwsAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
@@ -319,6 +331,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
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("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
@@ -346,6 +359,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
identityId,
|
||||
identityName,
|
||||
uaId,
|
||||
alicloudId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
@@ -367,6 +381,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
name: identityName,
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
alicloudId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
|
@@ -122,9 +122,9 @@ export const identityUaServiceFactory = ({
|
||||
}
|
||||
: {
|
||||
accessTokenTTL: identityUa.accessTokenPeriod,
|
||||
// Setting Max TTL to 2 × period ensures that clients can always renew their token
|
||||
// at least once, and matches client logic that checks if renewing would exceed Max TTL.
|
||||
accessTokenMaxTTL: 2 * identityUa.accessTokenPeriod
|
||||
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
|
||||
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
|
||||
accessTokenMaxTTL: 1000000000
|
||||
};
|
||||
|
||||
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||
|
@@ -3,6 +3,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
export const buildAuthMethods = ({
|
||||
uaId,
|
||||
gcpId,
|
||||
alicloudId,
|
||||
awsId,
|
||||
kubernetesId,
|
||||
ociId,
|
||||
@@ -14,6 +15,7 @@ export const buildAuthMethods = ({
|
||||
}: {
|
||||
uaId?: string;
|
||||
gcpId?: string;
|
||||
alicloudId?: string;
|
||||
awsId?: string;
|
||||
kubernetesId?: string;
|
||||
ociId?: string;
|
||||
@@ -26,6 +28,7 @@ export const buildAuthMethods = ({
|
||||
return [
|
||||
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
|
||||
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
|
||||
...[alicloudId ? IdentityAuthMethod.ALICLOUD_AUTH : null],
|
||||
...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
|
||||
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
|
||||
...[ociId ? IdentityAuthMethod.OCI_AUTH : null],
|
||||
|
@@ -3,6 +3,7 @@ import { Knex } from "knex";
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
TableName,
|
||||
TIdentityAlicloudAuths,
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAzureAuths,
|
||||
TIdentityGcpAuths,
|
||||
@@ -53,6 +54,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAlicloudAuths>(
|
||||
TableName.IdentityAliCloudAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityAliCloudAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAwsAuths>(
|
||||
TableName.IdentityAwsAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
@@ -99,6 +105,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
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("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
@@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAlicloudAuths>(
|
||||
TableName.IdentityAliCloudAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityAliCloudAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAwsAuths>(
|
||||
TableName.IdentityAwsAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
@@ -236,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
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("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
@@ -278,6 +291,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
orgId,
|
||||
uaId,
|
||||
alicloudId,
|
||||
awsId,
|
||||
gcpId,
|
||||
jwtId,
|
||||
@@ -312,6 +326,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
name: identityName,
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
alicloudId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
@@ -459,6 +474,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
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("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
@@ -502,6 +518,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
total_count,
|
||||
id,
|
||||
uaId,
|
||||
alicloudId,
|
||||
awsId,
|
||||
gcpId,
|
||||
jwtId,
|
||||
@@ -536,6 +553,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
name: identityName,
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
alicloudId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
|
@@ -1211,8 +1211,8 @@ export const orgServiceFactory = ({
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [el.email],
|
||||
substitutions: {
|
||||
inviterFirstName: invitingUser.firstName,
|
||||
inviterUsername: invitingUser.email,
|
||||
inviterFirstName: invitingUser?.firstName,
|
||||
inviterUsername: invitingUser?.email,
|
||||
organizationName: org?.name,
|
||||
email: el.email,
|
||||
organizationId: org?.id.toString(),
|
||||
|
@@ -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
|
||||
};
|
@@ -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
|
||||
};
|
||||
};
|
@@ -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)
|
||||
});
|
@@ -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
Reference in New Issue
Block a user