Compare commits

..

86 Commits

Author SHA1 Message Date
Sheen
1b9eecc8f4 doc: add scope breakdown section 2025-06-13 17:11:02 +00:00
x032205
23a7c1b8cc Merge pull request #3792 from Infisical/fix/alibaba-cloud
Add left join
2025-06-13 11:08:00 -04:00
x032205
f304024235 add left join 2025-06-13 10:56:30 -04:00
carlosmonastyrski
b43ecef112 Merge pull request #3786 from Infisical/fix/auth0SamlMappingsTipWarning
fix(docs): replace tip to warning on Auth0 Auth
2025-06-13 10:37:03 -03:00
Akhil Mohan
b26e56c97e Merge pull request #3777 from akhilmhdh/feat/seq-access-request
feat: Sequentail access approval request
2025-06-13 16:10:17 +05:30
=
7cced29c74 feat: resolved scim failure 2025-06-13 16:04:11 +05:30
Maidul Islam
0f00474243 Merge pull request #3735 from Infisical/misc/add-checks-for-helm-verification
misc: add verification pipelines for helm charts
2025-06-12 22:29:44 -04:00
Maidul Islam
3df010f266 Merge branch 'main' into misc/add-checks-for-helm-verification 2025-06-12 22:22:17 -04:00
x032205
333ce9d164 Merge pull request #3755 from Infisical/ENG-2773
feat(secret-rotation): Oracle Database
2025-06-12 21:06:57 -04:00
carlosmonastyrski
9621df4f8b Merge pull request #3736 from Infisical/feat/azureDevopsSecretSync
Feat/azure devops secret sync
2025-06-12 22:06:05 -03:00
x032205
b2b1c13393 Lint 2025-06-12 20:24:09 -04:00
Maidul Islam
1fb0c638d6 Merge pull request #3787 from Infisical/ENG-2909
Update wording for service tokens
2025-06-12 19:32:54 -04:00
x032205
c1ad49a532 Update wording for service tokens 2025-06-12 19:28:41 -04:00
x032205
d1fcc739c9 Merge pull request #3552 from Infisical/ENG-2705
feat(dynamic-secrets): GCP IAM
2025-06-12 18:01:17 -04:00
carlosmonastyrski
8c0287681b fix(docs): replace tip to warning on Auth0 Auth 2025-06-12 18:15:44 -03:00
x032205
c7458d94aa Warning about tokens 2025-06-12 15:45:30 -04:00
x032205
93570df318 TForm update 2025-06-12 15:39:52 -04:00
x032205
e798b4a7ba Merge branch 'main' into ENG-2705 2025-06-12 15:18:00 -04:00
x032205
36c93f47d9 Review fixes 2025-06-12 15:17:22 -04:00
x032205
dbbcb157ef Merge branch 'main' into ENG-2773 2025-06-12 15:09:38 -04:00
=
d5f0b4dad9 feat: fullstop 2025-06-13 00:30:08 +05:30
x032205
bdc23d22e7 Merge pull request #3775 from Infisical/ENG-2861
feat(machine-identity): Alibaba Cloud
2025-06-12 13:57:14 -04:00
=
0fd1b1c9d7 feat: resolved type issue 2025-06-12 23:24:12 +05:30
=
79df946f02 feat: fixed migration issue 2025-06-12 23:00:12 +05:30
=
da2fa7f3ca feat: fixed sort by sequence 2025-06-12 21:42:35 +05:30
x032205
08c1740afc Merge pull request #3782 from Infisical/ENG-2900
improvement(secret-scanning): Multi-select actions
2025-06-12 11:56:28 -04:00
x032205
3cac4ef927 Reviews 2025-06-12 11:43:32 -04:00
carlosmonastyrski
2667f8f0f2 Merge pull request #3785 from Infisical/fix/auth0SamlMappingsTip
fix(docs): add a tip on Auth0 SAML doc tip
2025-06-12 12:03:10 -03:00
carlosmonastyrski
b39537472b fix(docs): fix indentation issue 2025-06-12 11:56:19 -03:00
carlosmonastyrski
6b60b2562d Merge pull request #3784 from Infisical/fix/pitBannerImprovements
feat(pit): improve banner messaging
2025-06-12 11:46:39 -03:00
carlosmonastyrski
c2a7827080 fix(docs): add a tip on Auth0 SAML doc to remind that the mappings could be adapted to the custom settings of the organization 2025-06-12 11:42:41 -03:00
carlosmonastyrski
64e09b0dcd feat(pit): improve banner messaging 2025-06-12 11:28:56 -03:00
Daniel Hougaard
a7176d44dd Merge pull request #3762 from Infisical/daniel/aws-auth-eks
docs(identities/aws-auth): eks pod auth
2025-06-12 18:11:59 +04:00
Daniel Hougaard
09d4cdc634 requested changes 2025-06-12 18:03:30 +04:00
=
547ef17c10 feat: corrected validation 2025-06-12 16:01:48 +05:30
=
841408042e feat: ui resolved sequence grouping error 2025-06-12 16:00:45 +05:30
=
e5fb1ac808 feat: updated ui based on review 2025-06-12 15:31:41 +05:30
x032205
8a93c0bd59 Cap array 2025-06-12 02:16:07 -04:00
x032205
c0f8f50981 lint 2025-06-12 02:04:01 -04:00
x032205
fec47ef81c Mass-update endpoint 2025-06-12 01:59:47 -04:00
x032205
348f4b9787 Greptile review fixes + pagination tweaks 2025-06-12 01:39:23 -04:00
x032205
aa577b095c improvement(secret-scanning): Multi-select actions 2025-06-12 01:25:53 -04:00
x032205
dbf7ecc9b6 Merge pull request #3763 from Infisical/docs/add-packer-plugin-docs
feat(docs): Packer Plugin Docs
2025-06-11 17:44:35 -04:00
x032205
1ef9885062 Review fixes 2025-06-11 17:09:17 -04:00
carlosmonastyrski
de48c3e161 Merge pull request #3781 from Infisical/fix/inviteUsersWithIdentities
feat(invite-users): fix issue where invitations were not sent when the actor was an identity
2025-06-11 16:42:04 -03:00
carlosmonastyrski
852664e2cb feat(invite-users): fix issue where invitations were not sent when the actor was an identity 2025-06-11 16:11:34 -03:00
Sheen
fbc8264732 Merge pull request #3779 from Infisical/misc/cli-dynamic-secret-and-agent-improvements
misc: added project slug flag support to dynamic secret commands
2025-06-12 02:08:17 +08:00
Sheen Capadngan
4303547d8c misc: added more descriptive comment 2025-06-12 01:58:56 +08:00
Sheen Capadngan
f1c8a66d31 misc: converted flags to dash 2025-06-12 01:39:16 +08:00
Sheen Capadngan
0c21c19c95 misc: agent improvements 2025-06-12 01:25:47 +08:00
Sheen Capadngan
62308fb0a3 misc: added project slug flag support to dynamic secret commands 2025-06-11 23:06:27 +08:00
=
2d4476f99c feat: resolved ts error in rhf 2025-06-11 15:33:32 +05:30
=
81df491d5e feat: reptile feedback and resolved type failure 2025-06-11 15:20:46 +05:30
Akhil Mohan
d2c5603664 Update frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-11 15:20:46 +05:30
=
096930cb8f feat: updated doc and fixed overflow in model for access policy 2025-06-11 15:20:46 +05:30
=
f9c00cf442 feat: ui changes for approval to work 2025-06-11 15:20:46 +05:30
=
d32b6ad41d feat: updated policy to have sequence order 2025-06-11 15:20:45 +05:30
x032205
53968e07d0 Lint + greptile review fixes 2025-06-11 02:59:04 -04:00
x032205
c315eed4d4 feat(machine-identity): Alibaba Cloud 2025-06-11 02:44:53 -04:00
x032205
2be56f6a70 Greptile review fix 2025-06-09 16:57:39 -04:00
x032205
1ff1f3fad3 feat(docs): Packer Plugin Docs 2025-06-09 16:55:41 -04:00
x032205
0ae96dfff4 Proper quote escaping 2025-06-09 13:26:47 -04:00
x032205
8ad6488bd9 Bug fix 2025-06-09 13:17:59 -04:00
x032205
e264b68b7e Merge branch 'check-non-re2-regex-workflow' into ENG-2773 2025-06-09 13:12:24 -04:00
x032205
9e881534ec Merge branch 'check-non-re2-regex-workflow' into ENG-2773 2025-06-09 12:31:45 -04:00
x032205
2832ff5c76 Merge RE2 regex workflow for performance testing 2025-06-09 12:11:49 -04:00
x032205
4c6cca0864 Greptile review fixes 2025-06-09 12:10:47 -04:00
x032205
c06bbf0b9b Merge branch 'main' into ENG-2773 2025-06-09 12:03:54 -04:00
Daniel Hougaard
69392a4a51 fix(identity/aws-auth): allow for lowercase authoriazation header 2025-06-09 19:45:05 +04:00
Daniel Hougaard
130f1a167e docs: add docs for eks pod auth 2025-06-09 19:44:36 +04:00
x032205
8ab710817d Fixes 2025-06-09 10:01:56 -04:00
x032205
ca39e75434 Merge 2025-06-09 09:20:13 -04:00
x032205
265b25a4c6 Update some username stuff 2025-06-07 01:44:58 -04:00
x032205
54f6e0b5c6 docs 2025-06-07 01:08:32 -04:00
x032205
f2cdefaeec Remove comment 2025-06-07 00:08:43 -04:00
x032205
2d588d87ac Tweaks 2025-06-07 00:08:32 -04:00
x032205
5ee2eb1aa2 feat(secret-rotation): Oracle DB 2025-06-07 00:07:34 -04:00
Sheen Capadngan
141d0ede2d misc: add pr checks for gateway 2025-06-05 22:29:54 +08:00
Sheen Capadngan
ab78a79415 misc: add test workflow for gateway helm 2025-06-05 22:25:24 +08:00
Sheen Capadngan
8fa6af9ba4 misc: added checks for infisical standalone helm 2025-06-05 21:26:53 +08:00
Sheen Capadngan
f0a2845637 Merge remote-tracking branch 'origin/main' into misc/add-checks-for-helm-verification 2025-06-05 21:24:46 +08:00
Sheen Capadngan
8ffc88ba28 misc: add verification check for secret operator 2025-06-05 03:28:04 +08:00
x032205
05d132a1bb lint fix 2025-05-06 16:32:36 -04:00
x032205
bd7c4fc4eb review fixes 2025-05-06 16:26:51 -04:00
x032205
45c84d4936 Merge branch 'main' into ENG-2705 2025-05-06 15:28:16 -04:00
x032205
8e8e2e0dfe feat(dynamic-secrets): GCP IAM 2025-05-06 15:27:55 -04:00
215 changed files with 6137 additions and 830 deletions

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 }}

View File

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

View File

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

View File

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

View File

@@ -40,4 +40,8 @@ cli/detect/config/gitleaks.toml:gcp-api-key:578
cli/detect/config/gitleaks.toml:gcp-api-key:579
cli/detect/config/gitleaks.toml:gcp-api-key: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

View File

@@ -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;

View File

@@ -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,

View File

@@ -0,0 +1,44 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasStepColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "sequence");
const hasApprovalRequiredColumn = await knex.schema.hasColumn(
TableName.AccessApprovalPolicyApprover,
"approvalsRequired"
);
if (!hasStepColumn || !hasApprovalRequiredColumn) {
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (t) => {
if (!hasStepColumn) t.integer("sequence").defaultTo(1);
if (!hasApprovalRequiredColumn) t.integer("approvalsRequired").nullable();
});
}
// set rejected status for all access request that was rejected and still has status pending
const subquery = knex(TableName.AccessApprovalRequest)
.leftJoin(
TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequestReviewer}.requestId`,
`${TableName.AccessApprovalRequest}.id`
)
.where(`${TableName.AccessApprovalRequest}.status` as "status", "pending")
.where(`${TableName.AccessApprovalRequestReviewer}.status` as "status", "rejected")
.select(`${TableName.AccessApprovalRequest}.id`);
await knex(TableName.AccessApprovalRequest).where("id", "in", subquery).update("status", "rejected");
}
export async function down(knex: Knex): Promise<void> {
const hasStepColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "sequence");
const hasApprovalRequiredColumn = await knex.schema.hasColumn(
TableName.AccessApprovalPolicyApprover,
"approvalsRequired"
);
if (hasStepColumn || hasApprovalRequiredColumn) {
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (t) => {
if (hasStepColumn) t.dropColumn("sequence");
if (hasApprovalRequiredColumn) t.dropColumn("approvalsRequired");
});
}
}

View File

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

View File

@@ -13,7 +13,9 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
approverUserId: z.string().uuid().nullable().optional(),
approverGroupId: z.string().uuid().nullable().optional()
approverGroupId: z.string().uuid().nullable().optional(),
sequence: z.number().default(0).nullable().optional(),
approvalsRequired: z.number().default(1).nullable().optional()
});
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;

View File

@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityAlicloudAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
type: z.string(),
allowedArns: z.string()
});
export type TIdentityAlicloudAuths = z.infer<typeof IdentityAlicloudAuthsSchema>;
export type TIdentityAlicloudAuthsInsert = Omit<z.input<typeof IdentityAlicloudAuthsSchema>, TImmutableDBKeys>;
export type TIdentityAlicloudAuthsUpdate = Partial<Omit<z.input<typeof IdentityAlicloudAuthsSchema>, TImmutableDBKeys>>;

View File

@@ -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";

View File

@@ -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",

View File

@@ -23,12 +23,26 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
environment: z.string(),
approvers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
z.object({
type: z.literal(ApproverType.Group),
id: z.string(),
sequence: z.number().int().default(1)
}),
z.object({
type: z.literal(ApproverType.User),
id: z.string().optional(),
username: z.string().optional(),
sequence: z.number().int().default(1)
})
])
.array()
.max(100, "Cannot have more than 100 approvers")
.min(1, { message: "At least one approver should be provided" }),
.min(1, { message: "At least one approver should be provided" })
.refine(
// @ts-expect-error this is ok
(el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)),
"Must provide either username or id"
),
bypassers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
@@ -37,6 +51,13 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.array()
.max(100, "Cannot have more than 100 bypassers")
.optional(),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
stepNumber: z.number().int()
})
.array()
.optional(),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true)
@@ -78,7 +99,12 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
approvals: sapPubSchema
.extend({
approvers: z
.object({ type: z.nativeEnum(ApproverType), id: z.string().nullable().optional() })
.object({
type: z.nativeEnum(ApproverType),
id: z.string().nullable().optional(),
sequence: z.number().nullable().optional(),
approvalsRequired: z.number().nullable().optional()
})
.array()
.nullable()
.optional(),
@@ -152,12 +178,26 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.transform((val) => (val === "" ? "/" : val)),
approvers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
z.object({
type: z.literal(ApproverType.Group),
id: z.string(),
sequence: z.number().int().default(1)
}),
z.object({
type: z.literal(ApproverType.User),
id: z.string().optional(),
username: z.string().optional(),
sequence: z.number().int().default(1)
})
])
.array()
.min(1, { message: "At least one approver should be provided" })
.max(100, "Cannot have more than 100 approvers"),
.max(100, "Cannot have more than 100 approvers")
.refine(
// @ts-expect-error this is ok
(el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)),
"Must provide either username or id"
),
bypassers: z
.discriminatedUnion("type", [
z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
@@ -168,7 +208,14 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.optional(),
approvals: z.number().min(1).optional(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true)
allowedSelfApprovals: z.boolean().default(true),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
stepNumber: z.number().int()
})
.array()
.optional()
}),
response: {
200: z.object({
@@ -235,7 +282,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.object({
type: z.nativeEnum(ApproverType),
id: z.string().nullable().optional(),
name: z.string().nullable().optional()
name: z.string().nullable().optional(),
approvalsRequired: z.number().nullable().optional()
})
.array()
.nullable()

View File

@@ -112,7 +112,15 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
id: z.string(),
name: z.string(),
approvals: z.number(),
approvers: z.string().array(),
approvers: z
.object({
userId: z.string().nullable().optional(),
sequence: z.number().nullable().optional(),
approvalsRequired: z.number().nullable().optional(),
email: z.string().nullable().optional(),
username: z.string().nullable().optional()
})
.array(),
bypassers: z.string().array(),
secretPath: z.string().nullish(),
envId: z.string(),

View File

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

View File

@@ -270,7 +270,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z
.object({
@@ -278,7 +277,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
givenName: z.string().trim().optional()
})
.optional(),
displayName: z.string().trim(),
emails: z
.array(
z.object({

View File

@@ -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,

View File

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

View File

@@ -7,6 +7,7 @@ import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { 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,

View File

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

View File

@@ -48,6 +48,8 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
.select(tx.ref("username").withSchema("bypasserUsers").as("bypasserUsername"))
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
.select(tx.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
@@ -80,23 +82,31 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId: id }) => ({
mapper: ({ approverUserId: id, approverSequence, approvalsRequired }) => ({
id,
type: "user"
type: "user",
sequence: approverSequence,
approvalsRequired
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
mapper: ({ approverGroupId: id, approverSequence, approvalsRequired }) => ({
id,
type: "group"
type: "group",
sequence: approverSequence,
approvalsRequired
})
}
]
});
if (!formattedDoc?.[0]) return;
return formattedDoc?.[0];
return {
...formattedDoc?.[0],
approvers: formattedDoc?.[0]?.approvers.sort((a, b) => (a.sequence || 1) - (b.sequence || 1))
};
} catch (error) {
throw new DatabaseError({ error, name: "FindById" });
}
@@ -129,18 +139,22 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId: id, approverUsername }) => ({
mapper: ({ approverUserId: id, approverUsername, approverSequence, approvalsRequired }) => ({
id,
type: ApproverType.User,
name: approverUsername
name: approverUsername,
sequence: approverSequence,
approvalsRequired
})
},
{
key: "approverGroupId",
label: "approvers" as const,
mapper: ({ approverGroupId: id }) => ({
mapper: ({ approverGroupId: id, approverSequence, approvalsRequired }) => ({
id,
type: ApproverType.Group
type: ApproverType.Group,
sequence: approverSequence,
approvalsRequired
})
},
{
@@ -163,7 +177,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
]
});
return formattedDocs;
return formattedDocs.map((el) => ({
...el,
approvers: el?.approvers.sort((a, b) => (a.sequence || 1) - (b.sequence || 1))
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find" });
}

View File

@@ -4,6 +4,7 @@ import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@@ -41,9 +42,9 @@ type TAccessApprovalPolicyServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory;
userDAL: Pick<TUserDALFactory, "find">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find" | "resetReviewByPolicyId">;
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update">;
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update" | "delete">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
};
@@ -76,27 +77,23 @@ export const accessApprovalPolicyServiceFactory = ({
projectSlug,
environment,
enforcementLevel,
allowedSelfApprovals
allowedSelfApprovals,
approvalsRequired
}: TCreateAccessApprovalPolicy) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
// If there is a group approver people might be added to the group later to meet the approvers quota
const groupApprovers = approvers
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id) as string[];
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
const userApprovers = approvers
.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApprovers = approvers.filter((approver) => approver.type === ApproverType.User && approver.id) as {
id: string;
sequence?: number;
}[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
.filter(Boolean) as string[];
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const userApproverNames = approvers.filter(
(approver) => approver.type === ApproverType.User && approver.username
) as { username: string; sequence?: number }[];
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -116,14 +113,13 @@ export const accessApprovalPolicyServiceFactory = ({
let approverUserIds = userApprovers;
if (userApproverNames.length) {
const approverUsers = await userDAL.find({
const approverUsersInDB = await userDAL.find({
$in: {
username: userApproverNames
username: userApproverNames.map((el) => el.username)
}
});
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
const approverUsersInDBGroupByUsername = groupBy(approverUsersInDB, (i) => i.username);
const invalidUsernames = userApproverNames.filter((el) => !approverUsersInDBGroupByUsername?.[el.username]?.[0]);
if (invalidUsernames.length) {
throw new BadRequestError({
@@ -131,32 +127,13 @@ export const accessApprovalPolicyServiceFactory = ({
});
}
approverUserIds = approverUserIds.concat(approverUsers.map((user) => user.id));
}
const usersPromises: Promise<
{
id: string;
email: string | null | undefined;
username: string;
firstName: string | null | undefined;
lastName: string | null | undefined;
isPartOfGroup: boolean;
}[]
>[] = [];
const verifyAllApprovers = [...approverUserIds];
for (const groupId of groupApprovers) {
usersPromises.push(
groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }).then((group) => group.members)
approverUserIds = approverUserIds.concat(
userApproverNames.map((el) => ({
id: approverUsersInDBGroupByUsername[el.username]?.[0].id,
sequence: el.sequence
}))
);
}
const verifyGroupApprovers = (await Promise.all(usersPromises))
.flat()
.filter((user) => user.isPartOfGroup)
.map((user) => user.id);
verifyAllApprovers.push(...verifyGroupApprovers);
let groupBypassers: string[] = [];
let bypasserUserIds: string[] = [];
@@ -195,6 +172,7 @@ export const accessApprovalPolicyServiceFactory = ({
}
}
const approvalsRequiredGroupByStepNumber = groupBy(approvalsRequired || [], (i) => i.stepNumber);
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create(
{
@@ -210,9 +188,13 @@ export const accessApprovalPolicyServiceFactory = ({
if (approverUserIds.length) {
await accessApprovalPolicyApproverDAL.insertMany(
approverUserIds.map((userId) => ({
approverUserId: userId,
policyId: doc.id
approverUserIds.map((el) => ({
approverUserId: el.id,
policyId: doc.id,
sequence: el.sequence,
approvalsRequired: el.sequence
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
: approvals
})),
tx
);
@@ -220,9 +202,13 @@ export const accessApprovalPolicyServiceFactory = ({
if (groupApprovers) {
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
groupApprovers.map((el) => ({
approverGroupId: el.id,
policyId: doc.id,
sequence: el.sequence,
approvalsRequired: el.sequence
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
: approvals
})),
tx
);
@@ -290,22 +276,22 @@ export const accessApprovalPolicyServiceFactory = ({
actorAuthMethod,
approvals,
enforcementLevel,
allowedSelfApprovals
allowedSelfApprovals,
approvalsRequired
}: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers
.filter((approver) => approver.type === ApproverType.Group)
.map((approver) => approver.id) as string[];
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
const userApprovers = approvers
.filter((approver) => approver.type === ApproverType.User)
.map((approver) => approver.id)
.filter(Boolean) as string[];
const userApproverNames = approvers
.map((approver) => (approver.type === ApproverType.User ? approver.username : undefined))
.filter(Boolean) as string[];
const userApprovers = approvers.filter((approver) => approver.type === ApproverType.User && approver.id) as {
id: string;
sequence?: number;
}[];
const userApproverNames = approvers.filter(
(approver) => approver.type === ApproverType.User && approver.username
) as { username: string; sequence?: number }[];
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Approval policy not found" });
const currentApprovals = approvals || accessApprovalPolicy.approvals;
if (
groupApprovers?.length === 0 &&
@@ -401,6 +387,7 @@ export const accessApprovalPolicyServiceFactory = ({
}
}
const approvalsRequiredGroupByStepNumber = groupBy(approvalsRequired || [], (i) => i.stepNumber);
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.updateById(
accessApprovalPolicy.id,
@@ -417,16 +404,18 @@ export const accessApprovalPolicyServiceFactory = ({
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
if (userApprovers.length || userApproverNames.length) {
let userApproverIds = userApprovers;
let approverUserIds = userApprovers;
if (userApproverNames.length) {
const approverUsers = await userDAL.find({
const approverUsersInDB = await userDAL.find({
$in: {
username: userApproverNames
username: userApproverNames.map((el) => el.username)
}
});
const approverUsersInDBGroupByUsername = groupBy(approverUsersInDB, (i) => i.username);
const approverNamesFromDb = approverUsers.map((user) => user.username);
const invalidUsernames = userApproverNames.filter((username) => !approverNamesFromDb.includes(username));
const invalidUsernames = userApproverNames.filter(
(el) => !approverUsersInDBGroupByUsername?.[el.username]?.[0]
);
if (invalidUsernames.length) {
throw new BadRequestError({
@@ -434,13 +423,21 @@ export const accessApprovalPolicyServiceFactory = ({
});
}
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
approverUserIds = approverUserIds.concat(
userApproverNames.map((el) => ({
id: approverUsersInDBGroupByUsername[el.username]?.[0].id,
sequence: el.sequence
}))
);
}
await accessApprovalPolicyApproverDAL.insertMany(
userApproverIds.map((userId) => ({
approverUserId: userId,
policyId: doc.id
approverUserIds.map((el) => ({
approverUserId: el.id,
policyId: doc.id,
sequence: el.sequence,
approvalsRequired: el.sequence
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
: approvals
})),
tx
);
@@ -448,9 +445,13 @@ export const accessApprovalPolicyServiceFactory = ({
if (groupApprovers) {
await accessApprovalPolicyApproverDAL.insertMany(
groupApprovers.map((groupId) => ({
approverGroupId: groupId,
policyId: doc.id
groupApprovers.map((el) => ({
approverGroupId: el.id,
policyId: doc.id,
sequence: el.sequence,
approvalsRequired: el.sequence
? approvalsRequiredGroupByStepNumber?.[el.sequence]?.[0]?.numberOfApprovals
: approvals
})),
tx
);
@@ -478,6 +479,8 @@ export const accessApprovalPolicyServiceFactory = ({
);
}
await accessApprovalRequestDAL.resetReviewByPolicyId(doc.id, tx);
return doc;
});
return {

View File

@@ -27,7 +27,10 @@ export type TCreateAccessApprovalPolicy = {
approvals: number;
secretPath: string;
environment: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
approvers: (
| { type: ApproverType.Group; id: string; sequence?: number }
| { type: ApproverType.User; id?: string; username?: string; sequence?: number }
)[];
bypassers?: (
| { type: BypasserType.Group; id: string }
| { type: BypasserType.User; id?: string; username?: string }
@@ -36,12 +39,16 @@ export type TCreateAccessApprovalPolicy = {
name: string;
enforcementLevel: EnforcementLevel;
allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAccessApprovalPolicy = {
policyId: string;
approvals?: number;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
approvers: (
| { type: ApproverType.Group; id: string; sequence?: number }
| { type: ApproverType.User; id?: string; username?: string; sequence?: number }
)[];
bypassers?: (
| { type: BypasserType.Group; id: string }
| { type: BypasserType.User; id?: string; username?: string }
@@ -50,6 +57,7 @@ export type TUpdateAccessApprovalPolicy = {
name?: string;
enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
} & Omit<TProjectPermission, "projectId">;
export type TDeleteAccessApprovalPolicy = {

View File

@@ -39,12 +39,16 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId`
)
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
"accessApprovalPolicyApproverUser.id"
)
.leftJoin(
TableName.UserGroupMembership,
`${TableName.AccessApprovalPolicyApprover}.approverGroupId`,
@@ -82,13 +86,18 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
)
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
.select(db.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"))
.select(db.ref("bypasserUserId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(db.ref("userId").withSchema("bypasserUserGroupMembership").as("bypasserGroupUserId"))
.select(
db.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
db.ref("email").withSchema(TableName.Users).as("approverGroupEmail"),
db.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
db.ref("username").withSchema(TableName.Users).as("approverGroupUsername")
)
.select(
db.ref("projectId").withSchema(TableName.Environment),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
@@ -164,8 +173,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
permissions: doc.privilegePermissions
}
: null,
isApproved: !!doc.policyDeletedAt || !!doc.privilegeId || doc.status !== ApprovalStatus.PENDING
isApproved: doc.status === ApprovalStatus.APPROVED
}),
childrenMapper: [
{
@@ -173,11 +181,33 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
label: "reviewers" as const,
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
},
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId },
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({
userId: approverUserId,
sequence: approverSequence,
approvalsRequired,
email: approverEmail,
username: approverUsername
})
},
{
key: "approverGroupUserId",
label: "approvers" as const,
mapper: ({ approverGroupUserId }) => approverGroupUserId
mapper: ({
approverGroupUserId,
approverSequence,
approvalsRequired,
approverGroupEmail,
approverGroupUsername
}) => ({
userId: approverGroupUserId,
sequence: approverSequence,
approvalsRequired,
email: approverGroupEmail,
username: approverGroupUsername
})
},
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
{
@@ -192,7 +222,11 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
return formattedDocs.map((doc) => ({
...doc,
policy: { ...doc.policy, approvers: doc.approvers, bypassers: doc.bypassers }
policy: {
...doc.policy,
approvers: doc.approvers.filter((el) => el.userId).sort((a, b) => (a.sequence || 0) - (b.sequence || 0)),
bypassers: doc.bypassers
}
}));
} catch (error) {
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
@@ -272,6 +306,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"),
tx.ref("approvalsRequired").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("userId").withSchema(TableName.UserGroupMembership),
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("email").withSchema("accessApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
@@ -367,13 +403,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
approverEmail: email,
approverUsername: username,
approverLastName: lastName,
approverFirstName: firstName
approverFirstName: firstName,
approverSequence,
approvalsRequired
}) => ({
userId: approverUserId,
email,
firstName,
lastName,
username
username,
sequence: approverSequence,
approvalsRequired
})
},
{
@@ -384,13 +424,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
approverGroupEmail: email,
approverGroupUsername: username,
approverGroupLastName: lastName,
approverFirstName: firstName
approverFirstName: firstName,
approverSequence,
approvalsRequired
}) => ({
userId,
email,
firstName,
lastName,
username
username,
sequence: approverSequence,
approvalsRequired
})
},
{
@@ -434,7 +478,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
...formattedDoc[0],
policy: {
...formattedDoc[0].policy,
approvers: formattedDoc[0].approvers,
approvers: formattedDoc[0].approvers
.filter((el) => el.userId)
.sort((a, b) => (a.sequence || 0) - (b.sequence || 0)),
bypassers: formattedDoc[0].bypassers
}
};
@@ -495,7 +541,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
req.status === ApprovalStatus.PENDING
);
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
const finalizedApprovals = formattedRequests.filter(
(req) =>
req.privilegeId ||
@@ -509,5 +555,27 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
}
};
return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount };
const resetReviewByPolicyId = async (policyId: string, tx?: Knex) => {
try {
await (tx || db)(TableName.AccessApprovalRequestReviewer)
.leftJoin(
TableName.AccessApprovalRequest,
`${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId`
)
.where(`${TableName.AccessApprovalRequest}.status` as "status", ApprovalStatus.PENDING)
.where(`${TableName.AccessApprovalRequest}.policyId` as "policyId", policyId)
.del();
} catch (error) {
throw new DatabaseError({ error, name: "ResetReviewByPolicyId" });
}
};
return {
...accessApprovalRequestOrm,
findById,
findRequestsWithPrivilegeByPolicyIds,
getCount,
resetReviewByPolicyId
};
};

View File

@@ -4,6 +4,7 @@ import msFn from "ms";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types";
@@ -358,7 +359,6 @@ export const accessApprovalRequestServiceFactory = ({
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass);
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
// If user is (not an approver OR cant self approve) AND can't bypass policy
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
throw new BadRequestError({
@@ -380,8 +380,44 @@ export const accessApprovalRequestServiceFactory = ({
}
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
if (accessApprovalRequest.status !== ApprovalStatus.PENDING) {
throw new BadRequestError({ message: "The request has been closed" });
}
const reviewsGroupById = groupBy(
existingReviews.filter((review) => review.status === ApprovalStatus.APPROVED),
(i) => i.reviewerUserId
);
const approvedSequences = policy.approvers.reduce(
(acc, curr) => {
const hasApproved = reviewsGroupById?.[curr.userId as string]?.[0];
if (acc?.[acc.length - 1]?.step === curr.sequence) {
if (hasApproved) {
acc[acc.length - 1].approvals += 1;
}
return acc;
}
acc.push({
step: curr.sequence || 1,
approvals: hasApproved ? 1 : 0,
requiredApprovals: curr.approvalsRequired || 1
});
return acc;
},
[] as { step: number; approvals: number; requiredApprovals: number }[]
);
const presentSequence = approvedSequences.find((el) => el.approvals < el.requiredApprovals) || {
step: 1,
approvals: 0,
requiredApprovals: 1
};
if (presentSequence) {
const isApproverOfTheSequence = policy.approvers.find(
(el) => el.sequence === presentSequence.step && el.userId === actorId
);
if (!isApproverOfTheSequence) throw new BadRequestError({ message: "You are not reviewer in this step" });
}
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
@@ -426,11 +462,14 @@ export const accessApprovalRequestServiceFactory = ({
);
}
const otherReviews = existingReviews.filter((er) => er.reviewerUserId !== actorId);
const allUniqueReviews = [...otherReviews, reviewForThisActorProcessing];
if (status === ApprovalStatus.REJECTED) {
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { status: ApprovalStatus.REJECTED }, tx);
return reviewForThisActorProcessing;
}
const approvedReviews = allUniqueReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
const meetsStandardApprovalThreshold = approvedReviews.length >= policy.approvals;
const meetsStandardApprovalThreshold =
(presentSequence?.approvals || 0) + 1 >= presentSequence.requiredApprovals &&
approvedSequences.at(-1)?.step === presentSequence?.step;
if (
reviewForThisActorProcessing.status === ApprovalStatus.APPROVED &&

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -170,6 +170,12 @@ export enum EventType {
REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth",
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

View File

@@ -264,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(

View File

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

View File

@@ -6,6 +6,7 @@ import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id";
import { 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()
});

View File

@@ -470,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",
@@ -487,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", [
@@ -507,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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret"
import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password";
import { 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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

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

View File

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

View File

@@ -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.",

View File

@@ -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,

View File

@@ -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";
@@ -95,6 +99,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options,
...SanitizedOCIConnectionSchema.options,
...SanitizedOracleDBConnectionSchema.options,
...SanitizedOnePassConnectionSchema.options
]);
@@ -121,6 +126,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema,
OCIConnectionListItemSchema,
OracleDBConnectionListItemSchema,
OnePassConnectionListItemSchema
]);

View File

@@ -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";
@@ -50,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
};

View File

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

View File

@@ -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);

View File

@@ -21,6 +21,7 @@ export enum AppConnection {
LDAP = "ldap",
TeamCity = "teamcity",
OCI = "oci",
OracleDB = "oracledb",
OnePass = "1password"
}

View File

@@ -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";
@@ -119,6 +120,7 @@ export const listAppConnectionOptions = () => {
getLdapConnectionListItem(),
getTeamCityConnectionListItem(),
getOCIConnectionListItem(),
getOracleDBConnectionListItem(),
getOnePassConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -193,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
};
@@ -229,6 +232,7 @@ 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:
@@ -294,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
};

View File

@@ -23,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"
};
@@ -48,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
};

View File

@@ -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";
@@ -102,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
};

View File

@@ -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";
@@ -154,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
@@ -184,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,
@@ -244,6 +255,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema
| TValidateOCIConnectionCredentialsSchema
| TValidateOracleDBConnectionCredentialsSchema
| TValidateOnePassConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {

View File

@@ -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 (

View File

@@ -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,

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -94,7 +94,9 @@ export const identityAwsAuthServiceFactory = ({
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
const 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" });

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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],

View File

@@ -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,
@@ -406,6 +421,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin(
TableName.IdentityAliCloudAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityAliCloudAuth}.identityId`
)
.leftJoin(
TableName.IdentityAwsAuth,
`${TableName.IdentityOrgMembership}.identityId`,
@@ -459,6 +479,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 +523,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
total_count,
id,
uaId,
alicloudId,
awsId,
gcpId,
jwtId,
@@ -536,6 +558,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
name: identityName,
authMethods: buildAuthMethods({
uaId,
alicloudId,
awsId,
gcpId,
kubernetesId,

View File

@@ -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(),

View File

@@ -5,8 +5,8 @@ import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface OrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
metadata?: string;
inviterFirstName: string;
inviterUsername: string;
inviterFirstName?: string;
inviterUsername?: string;
organizationName: string;
email: string;
organizationId: string;
@@ -38,11 +38,19 @@ export const OrganizationInvitationTemplate = ({
</Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]">
<strong>{inviterFirstName}</strong> (
<Link href={`mailto:${inviterUsername}`} className="text-slate-700 no-underline">
{inviterUsername}
</Link>
) has invited you to collaborate on <strong>{organizationName}</strong>.
{inviterFirstName && inviterUsername ? (
<>
<strong>{inviterFirstName}</strong> (
<Link href={`mailto:${inviterUsername}`} className="text-slate-700 no-underline">
{inviterUsername}
</Link>
) has invited you to collaborate on <strong>{organizationName}</strong>.
</>
) : (
<>
You have been invited to collaborate on <strong>{organizationName}</strong>.
</>
)}
</Text>
</Section>
<Section className="text-center mt-[28px]">

View File

@@ -106,9 +106,11 @@ export const userServiceFactory = ({
code
});
const userEmails = user?.email ? await userDAL.find({ email: user.email }) : [];
await userDAL.updateById(user.id, {
isEmailVerified: true,
username: usersByusername.length === 1 && user.email ? user.email.toLowerCase() : undefined
username: userEmails?.length === 1 && userEmails?.[0]?.id === user.id ? user.email.toLowerCase() : undefined
});
};

View File

@@ -576,7 +576,7 @@ func (tm *AgentManager) FetchUniversalAuthAccessToken() (credential infisicalSdk
}
tm.cachedUniversalAuthClientSecret = clientSecret
if tm.removeUniversalAuthClientSecretOnRead {
if universalAuthConfig.RemoveClientSecretOnRead {
defer os.Remove(universalAuthConfig.ClientSecretPath)
}
@@ -718,7 +718,7 @@ func (tm *AgentManager) FetchNewAccessToken() error {
}
// Refreshes the existing access token
func (tm *AgentManager) RefreshAccessToken() error {
func (tm *AgentManager) RefreshAccessToken(accessToken string) error {
httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
return err
@@ -728,7 +728,6 @@ func (tm *AgentManager) RefreshAccessToken() error {
SetRetryMaxWaitTime(20 * time.Second).
SetRetryWaitTime(5 * time.Second)
accessToken := tm.GetToken()
response, err := api.CallMachineIdentityRefreshAccessToken(httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken})
if err != nil {
return err
@@ -752,18 +751,37 @@ func (tm *AgentManager) ManageTokenLifecycle() {
accessTokenRefreshedTime = tm.accessTokenFetchedTime
}
nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(tm.accessTokenTTL - (5 * time.Second))
// Calculate next expiry time at 2/3 of the TTL
nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(tm.accessTokenTTL * 2 / 3)
if tm.accessTokenFetchedTime.IsZero() && tm.accessTokenRefreshedTime.IsZero() {
// case: init login to get access token
log.Info().Msg("attempting to authenticate...")
err := tm.FetchNewAccessToken()
if err != nil {
log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err)
// try to fetch token from sink files first
// if token is found, refresh the token right away and continue from there
isSavedTokenValid := false
token := tm.FetchTokenFromFiles()
if token != "" {
log.Info().Msg("found existing token in file, attempting to refresh...")
err := tm.RefreshAccessToken(token)
isSavedTokenValid = err == nil
if isSavedTokenValid {
log.Info().Msg("token refreshed successfully from saved file")
tm.accessTokenFetchedTime = time.Now()
} else {
log.Error().Msg("unable to refresh token from saved file")
}
}
// wait a bit before trying again
time.Sleep((30 * time.Second))
continue
if !isSavedTokenValid {
// case: init login to get access token
log.Info().Msg("attempting to authenticate...")
err := tm.FetchNewAccessToken()
if err != nil {
log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err)
// wait a bit before trying again
time.Sleep((30 * time.Second))
continue
}
}
} else if time.Now().After(accessTokenMaxTTLExpiresInTime) {
// case: token has reached max ttl and we should re-authenticate entirely (cannot refresh)
@@ -779,7 +797,7 @@ func (tm *AgentManager) ManageTokenLifecycle() {
} else {
// case: token ttl has expired, but the token is still within max ttl, so we can refresh
log.Info().Msgf("attempting to refresh existing token...")
err := tm.RefreshAccessToken()
err := tm.RefreshAccessToken(tm.GetToken())
if err != nil {
log.Error().Msgf("unable to refresh token because %v. Will retry in 30 seconds", err)
@@ -800,15 +818,18 @@ func (tm *AgentManager) ManageTokenLifecycle() {
accessTokenRefreshedTime = tm.accessTokenRefreshedTime
}
nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(tm.accessTokenTTL - (5 * time.Second))
// Recalculate next expiry time at 2/3 of the TTL
nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(tm.accessTokenTTL * 2 / 3)
accessTokenMaxTTLExpiresInTime = tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) {
// case: Refreshed so close that the next refresh would occur beyond max ttl (this is because currently, token renew tries to add +access-token-ttl amount of time)
// example: access token ttl is 11 sec and max ttl is 30 sec. So it will start with 11 seconds, then 22 seconds but the next time you call refresh it would try to extend it to 33 but max ttl only allows 30, so the token will be valid until 30 before we need to reauth
time.Sleep(tm.accessTokenTTL - nextAccessTokenExpiresInTime.Sub(accessTokenMaxTTLExpiresInTime))
// case: Refreshed so close that the next refresh would occur beyond max ttl
// Sleep until we're at 2/3 of the remaining time to max TTL
remainingTime := accessTokenMaxTTLExpiresInTime.Sub(time.Now())
time.Sleep(remainingTime * 2 / 3)
} else {
time.Sleep(tm.accessTokenTTL - (5 * time.Second))
// Sleep until we're at 2/3 of the TTL
time.Sleep(tm.accessTokenTTL * 2 / 3)
}
}
}
@@ -830,6 +851,24 @@ func (tm *AgentManager) WriteTokenToFiles() {
}
}
func (tm *AgentManager) FetchTokenFromFiles() string {
for _, sinkFile := range tm.filePaths {
if sinkFile.Type == "file" {
tokenBytes, err := ioutil.ReadFile(sinkFile.Config.Path)
if err != nil {
log.Debug().Msgf("unable to read token from file '%s' because %v", sinkFile.Config.Path, err)
continue
}
token := string(tokenBytes)
if token != "" {
return token
}
}
}
return ""
}
func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) {
if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil {
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)

View File

@@ -49,6 +49,11 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
projectSlug, err := cmd.Flags().GetString("project-slug")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse path flag")
@@ -60,10 +65,10 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to get resty client with custom headers")
}
if projectId == "" {
if projectId == "" && projectSlug == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag")
}
projectId = workspaceFile.WorkspaceId
}
@@ -100,13 +105,16 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
})
infisicalClient.Auth().SetAccessToken(infisicalToken)
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
if projectSlug == "" {
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
}
projectSlug = projectDetails.Slug
}
dynamicSecretRootCredentials, err := infisicalClient.DynamicSecrets().List(infisicalSdk.ListDynamicSecretsRootCredentialsOptions{
ProjectSlug: projectDetails.Slug,
ProjectSlug: projectSlug,
SecretPath: secretsPath,
EnvironmentSlug: environmentName,
})
@@ -156,6 +164,11 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
projectSlug, err := cmd.Flags().GetString("project-slug")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
ttl, err := cmd.Flags().GetString("ttl")
if err != nil {
util.HandleError(err, "Unable to parse flag")
@@ -177,10 +190,10 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to get resty client with custom headers")
}
if projectId == "" {
if projectId == "" && projectSlug == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project id with --projectId flag, or pass in project slug with --project-slug flag")
}
projectId = workspaceFile.WorkspaceId
}
@@ -216,14 +229,17 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
})
infisicalClient.Auth().SetAccessToken(infisicalToken)
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
if projectSlug == "" {
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
}
projectSlug = projectDetails.Slug
}
dynamicSecretRootCredential, err := infisicalClient.DynamicSecrets().GetByName(infisicalSdk.GetDynamicSecretRootCredentialByNameOptions{
DynamicSecretName: dynamicSecretRootCredentialName,
ProjectSlug: projectDetails.Slug,
ProjectSlug: projectSlug,
SecretPath: secretsPath,
EnvironmentSlug: environmentName,
})
@@ -233,7 +249,7 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
}
// for Kubernetes dynamic secrets only
kubernetesNamespace, err := cmd.Flags().GetString("kubernetesNamespace")
kubernetesNamespace, err := cmd.Flags().GetString("kubernetes-namespace")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
@@ -245,7 +261,7 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{
DynamicSecretName: dynamicSecretRootCredential.Name,
ProjectSlug: projectDetails.Slug,
ProjectSlug: projectSlug,
TTL: ttl,
SecretPath: secretsPath,
EnvironmentSlug: environmentName,
@@ -304,6 +320,11 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
projectSlug, err := cmd.Flags().GetString("project-slug")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
ttl, err := cmd.Flags().GetString("ttl")
if err != nil {
util.HandleError(err, "Unable to parse flag")
@@ -320,10 +341,10 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to get resty client with custom headers")
}
if projectId == "" {
if projectId == "" && projectSlug == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag")
}
projectId = workspaceFile.WorkspaceId
}
@@ -360,9 +381,12 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
})
infisicalClient.Auth().SetAccessToken(infisicalToken)
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
if projectSlug == "" {
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
}
projectSlug = projectDetails.Slug
}
if err != nil {
@@ -370,7 +394,7 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
}
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().RenewById(infisicalSdk.RenewDynamicSecretLeaseOptions{
ProjectSlug: projectDetails.Slug,
ProjectSlug: projectSlug,
TTL: ttl,
SecretPath: secretsPath,
EnvironmentSlug: environmentName,
@@ -416,6 +440,11 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
projectSlug, err := cmd.Flags().GetString("project-slug")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse path flag")
@@ -427,10 +456,10 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to get resty client with custom headers")
}
if projectId == "" {
if projectId == "" && projectSlug == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag")
}
projectId = workspaceFile.WorkspaceId
}
@@ -467,9 +496,12 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
})
infisicalClient.Auth().SetAccessToken(infisicalToken)
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
if projectSlug == "" {
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
}
projectSlug = projectDetails.Slug
}
if err != nil {
@@ -477,11 +509,12 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
}
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{
ProjectSlug: projectDetails.Slug,
ProjectSlug: projectSlug,
SecretPath: secretsPath,
EnvironmentSlug: environmentName,
LeaseId: dynamicSecretLeaseId,
})
if err != nil {
util.HandleError(err, "To revoke dynamic secret lease")
}
@@ -522,6 +555,11 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
projectSlug, err := cmd.Flags().GetString("project-slug")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secretsPath, err := cmd.Flags().GetString("path")
if err != nil {
util.HandleError(err, "Unable to parse path flag")
@@ -533,10 +571,10 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to get resty client with custom headers")
}
if projectId == "" {
if projectId == "" && projectSlug == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag")
}
projectId = workspaceFile.WorkspaceId
}
@@ -572,14 +610,17 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
})
infisicalClient.Auth().SetAccessToken(infisicalToken)
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
if projectSlug == "" {
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
if err != nil {
util.HandleError(err, "To fetch project details")
}
projectSlug = projectDetails.Slug
}
dynamicSecretLeases, err := infisicalClient.DynamicSecrets().Leases().List(infisicalSdk.ListDynamicSecretLeasesOptions{
DynamicSecretName: dynamicSecretRootCredentialName,
ProjectSlug: projectDetails.Slug,
ProjectSlug: projectSlug,
SecretPath: secretsPath,
EnvironmentSlug: environmentName,
})
@@ -596,34 +637,39 @@ func init() {
dynamicSecretLeaseCreateCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
dynamicSecretLeaseCreateCmd.Flags().String("token", "", "Create dynamic secret leases using machine identity access token")
dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
dynamicSecretLeaseCreateCmd.Flags().String("project-slug", "", "Manually set the project-slug to create lease in")
dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line")
// Kubernetes specific flags
dynamicSecretLeaseCreateCmd.Flags().String("kubernetesNamespace", "", "The namespace to create the lease in. Only used for Kubernetes dynamic secrets.")
dynamicSecretLeaseCreateCmd.Flags().String("kubernetes-namespace", "", "The namespace to create the lease in. Only used for Kubernetes dynamic secrets.")
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd)
dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
dynamicSecretLeaseListCmd.Flags().String("token", "", "Fetch dynamic secret leases machine identity access token")
dynamicSecretLeaseListCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
dynamicSecretLeaseListCmd.Flags().String("project-slug", "", "Manually set the project-slug to list leases from")
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseListCmd)
dynamicSecretLeaseRenewCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
dynamicSecretLeaseRenewCmd.Flags().String("token", "", "Renew dynamic secrets machine identity access token")
dynamicSecretLeaseRenewCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
dynamicSecretLeaseRenewCmd.Flags().String("project-slug", "", "Manually set the project-slug to renew lease in")
dynamicSecretLeaseRenewCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRenewCmd)
dynamicSecretLeaseRevokeCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
dynamicSecretLeaseRevokeCmd.Flags().String("token", "", "Delete dynamic secrets using machine identity access token")
dynamicSecretLeaseRevokeCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
dynamicSecretLeaseRevokeCmd.Flags().String("project-slug", "", "Manually set the project-slug to revoke lease from")
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRevokeCmd)
dynamicSecretCmd.AddCommand(dynamicSecretLeaseCmd)
dynamicSecretCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
dynamicSecretCmd.Flags().String("projectId", "", "Manually set the projectId to fetch dynamic-secret when using machine identity based auth")
dynamicSecretCmd.Flags().String("project-slug", "", "Manually set the project-slug to fetch dynamic-secret from")
dynamicSecretCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
dynamicSecretCmd.Flags().String("path", "/", "get dynamic secret within a folder path")
rootCmd.AddCommand(dynamicSecretCmd)

View File

@@ -10,12 +10,12 @@ Writing a design document helps you efficiently solve broad, complex engineering
**Writing a design will help you:**
- **Understand the problem space:** Deeply understand the problem youre solving to make sure it is well scoped.
- **Understand the problem space:** Deeply understand the problem you're solving to make sure it is well scoped.
- **Stay on the right path:** Without proper planning, you risk cycling between partial implementation and replanning, encountering roadblocks that force you back to square one. A solid plan minimizes wasted engineering hours.
- **An opportunity to collaborate:** Bring relevant engineers into the discussion to develop well-thought-out solutions and catch potential issues you might have overlooked.
- **Faster implementation:** A well-thought-out plan will help you catch roadblocks early and ship quickly because you know exactly what needs to get implemented.
**When to write a design document:**
**When to write a design document:**
- **Write a design doc**: If the feature is not well defined, high-security, or will take more than **1 full engineering week** to build.
- **Skip the design doc**: For small, straightforward features that can be built quickly with informal discussions.
@@ -27,21 +27,31 @@ If you are unsure when to create a design doc, chat with @maidul.
Every feature/problem is unique, but your design docs should generally include the following sections. If you need to include additional sections, feel free to do so.
1. **Title**
- A descriptive title.
- Name of document owner and name of reviewer(s).
- A descriptive title.
- Name of document owner and name of reviewer(s).
2. **Overview**
- A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs).
- A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs).
3. **Context**
- Explain the problems background, why its important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.).
- Explain the problem's background, why it's important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.).
4. **Solution**
- Provide a big-picture explanation of the solution, followed by detailed technical architecture.
- Use diagrams/charts where needed.
- Write clearly so that another engineer could implement the solution in your absence.
5. **Milestones**
- Break the project into phases with clear start and end dates estimates. Use a table or bullet points.
6. **FAQ**
- Common questions or concerns someone might have while reading your document that can be quickly addressed.
- Provide a big-picture explanation of the solution, followed by detailed technical architecture.
- Use diagrams/charts where needed.
- Write clearly so that another engineer could implement the solution in your absence.
- Break down the solution into meaningful, logical chunks that can be implemented independently. Consider:
- Core infrastructure vs feature-specific implementations
- Backend services that can be built and tested in isolation
- Data model changes that can be implemented in phases
- API endpoints that can be versioned and rolled out gradually
This breakdown will inform your milestones and help reviewers understand the implementation strategy. If you find yourself planning PRs that are 3,000+ lines long, it might be a sign that you need to break down the work further. While PR size isn't the primary factor in determining breakdown, it can be a useful indicator that a chunk of work might be too large to review effectively.
5. **Milestones**
- Break the project into phases with clear start and end dates estimates. Use a table or bullet points.
- Each milestone should align with the logical chunks identified in the Solution section.
6. **FAQ**
- Common questions or concerns someone might have while reading your document that can be quickly addressed.
## **How to Write a Design Doc**
@@ -51,17 +61,18 @@ Every feature/problem is unique, but your design docs should generally include t
Before sharing your design docs with others, review your design doc as if you were a teammate seeing it for the first time. Anticipate questions and address them.
## **Process from start to finish**
1. **Research/Discuss**
- Before you start writing, take some time to research and get a solid understanding of the problem space. Look into how other well-established companies are tackling similar challenges, if they are.
Talk through the problem and your initial solution with other engineers on the team—bounce ideas around and get their feedback. If you have ideas on how the system could if implemented in Infisical, would it effect any downstream features/systems, etc?
Once youve got a general direction, you might need to test a some theories. This is where quick proof of concepts (POCs) come in handy, but dont get too caught up in the details. The goal of a POC is simply to validate a core idea or concept so you can get to the rest of your planning.
- Before you start writing, take some time to research and get a solid understanding of the problem space. Look into how other well-established companies are tackling similar challenges, if they are.
Talk through the problem and your initial solution with other engineers on the team—bounce ideas around and get their feedback. If you have ideas on how the system could if implemented in Infisical, would it effect any downstream features/systems, etc?
Once you've got a general direction, you might need to test a some theories. This is where quick proof of concepts (POCs) come in handy, but don't get too caught up in the details. The goal of a POC is simply to validate a core idea or concept so you can get to the rest of your planning.
2. **Write the Doc**
- Based on your research/discussions, write the design doc and include all relevant sections. Your goal is to come up with a convincing plan on why this is the correct why to solve the problem at hand.
- Based on your research/discussions, write the design doc and include all relevant sections. Your goal is to come up with a convincing plan on why this is the correct why to solve the problem at hand.
3. **Assign Reviewers**
- Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up.
- Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up.
4. **Team Review and Feedback**
- Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on.
- Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on.

14
ct.yaml Normal file
View File

@@ -0,0 +1,14 @@
# Chart testing configuration
chart-dirs:
- helm-charts
# Test against these Kubernetes versions
kube-versions:
- v1.30.0
- v1.31.0
- v1.32.0
- v1.33.0
validate-maintainers: false
kubectl-timeout: 300s

View File

@@ -0,0 +1,4 @@
---
title: "Attach"
openapi: "POST /api/v1/auth/alicloud-auth/identities/{identityId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Login"
openapi: "POST /api/v1/auth/alicloud-auth/login"
---

View File

@@ -0,0 +1,4 @@
---
title: "Retrieve"
openapi: "GET /api/v1/auth/alicloud-auth/identities/{identityId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Revoke"
openapi: "DELETE /api/v1/auth/alicloud-auth/identities/{identityId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/auth/alicloud-auth/identities/{identityId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/oracledb/available"
---

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/oracledb"
---
<Note>
Check out the configuration docs for [OracleDB Connections](/integrations/app-connections/oracledb) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/oracledb/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/oracledb/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/oracledb/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/oracledb"
---

View File

@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/oracledb/{connectionId}"
---
<Note>
Check out the configuration docs for [OracleDB Connections](/integrations/app-connections/oracledb) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/oracledb-credentials"
---
<Note>
Check out the configuration docs for [OracleDB Credentials Rotations](/documentation/platform/secret-rotation/oracledb-credentials) to learn how to obtain the required parameters.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/oracledb-credentials/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/oracledb-credentials/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/oracledb-credentials/rotation-name/{rotationName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/oracledb-credentials/{rotationId}/generated-credentials"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/oracledb-credentials"
---

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