Compare commits
15 Commits
vmatsiiako
...
daniel/mul
Author | SHA1 | Date | |
---|---|---|---|
|
860ebb73a9 | ||
|
1cd17a451c | ||
|
7245aaa9ec | ||
|
d32f69e052 | ||
|
726477e3d7 | ||
|
a4ca996a1b | ||
|
303312fe91 | ||
|
f3f2879d6d | ||
|
d0f3d96b3e | ||
|
70d2a21fbc | ||
|
418ae42d94 | ||
|
273c6b3842 | ||
|
6be8d5d2a7 | ||
|
9eb7640755 | ||
|
741138c4bd |
@@ -7,12 +7,12 @@ permissions:
|
||||
|
||||
jobs:
|
||||
infisical-tests:
|
||||
name: Integration tests
|
||||
name: Run tests before deployment
|
||||
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
|
||||
uses: ./.github/workflows/run-backend-tests.yml
|
||||
|
||||
infisical-image:
|
||||
name: Build
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infisical-tests]
|
||||
steps:
|
||||
@@ -102,10 +102,10 @@ jobs:
|
||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||
service: infisical-core-gamma-stage
|
||||
cluster: infisical-gamma-stage
|
||||
wait-for-service-stability: true
|
||||
wait-for-service-stability: false
|
||||
|
||||
production-us:
|
||||
name: US production deploy
|
||||
production-postgres-deployment:
|
||||
name: Deploy to production
|
||||
runs-on: ubuntu-latest
|
||||
needs: [gamma-deployment]
|
||||
environment:
|
||||
@@ -159,54 +159,3 @@ jobs:
|
||||
service: infisical-core-platform
|
||||
cluster: infisical-core-platform
|
||||
wait-for-service-stability: true
|
||||
|
||||
production-eu:
|
||||
name: EU production deploy
|
||||
runs-on: ubuntu-latest
|
||||
needs: [production-us]
|
||||
environment:
|
||||
name: production-eu
|
||||
steps:
|
||||
- uses: twingate/github-action@v1
|
||||
with:
|
||||
service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Change directory to backend and install dependencies
|
||||
env:
|
||||
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||
run: |
|
||||
cd backend
|
||||
npm install
|
||||
npm run migration:latest
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
audience: sts.eu-central-1.amazonaws.com
|
||||
aws-region: eu-central-1
|
||||
role-to-assume: arn:aws:iam::345594589636:role/gha-make-prod-deployment
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
|
||||
- name: Render Amazon ECS task definition
|
||||
id: render-web-container
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: infisical-core-platform
|
||||
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||
environment-variables: "LOG_LEVEL=info"
|
||||
- name: Deploy to Amazon ECS service
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||
service: infisical-core-platform
|
||||
cluster: infisical-core-platform
|
||||
wait-for-service-stability: true
|
@@ -34,7 +34,7 @@ describe("Identity v1", async () => {
|
||||
test("Create identity", async () => {
|
||||
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
|
||||
expect(newIdentity.name).toBe("mac1");
|
||||
expect(newIdentity.authMethod).toBeNull();
|
||||
expect(newIdentity.authMethods).toEqual([]);
|
||||
|
||||
await deleteIdentity(newIdentity.id);
|
||||
});
|
||||
@@ -42,7 +42,7 @@ describe("Identity v1", async () => {
|
||||
test("Update identity", async () => {
|
||||
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
|
||||
expect(newIdentity.name).toBe("mac1");
|
||||
expect(newIdentity.authMethod).toBeNull();
|
||||
expect(newIdentity.authMethods).toEqual([]);
|
||||
|
||||
const updatedIdentity = await testServer.inject({
|
||||
method: "PATCH",
|
||||
|
253
backend/package-lock.json
generated
@@ -51,12 +51,11 @@
|
||||
"connect-redis": "^7.1.1",
|
||||
"cron": "^3.1.7",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify": "^4.28.1",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"google-auth-library": "^9.9.0",
|
||||
"googleapis": "^137.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"hdb": "^0.19.10",
|
||||
"ioredis": "^5.3.2",
|
||||
"jmespath": "^0.16.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@@ -5853,12 +5852,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@probot/pino": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@probot/pino/-/pino-2.5.0.tgz",
|
||||
"integrity": "sha512-I7zI6MWP1wz9qvTY8U3wOWeRXY2NiuTDqf91v/LQl9oiffUHl+Z1YelRvNcvHbaUo/GK7E1mJr+Sw4dHuSGxpg==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@probot/pino/-/pino-2.4.0.tgz",
|
||||
"integrity": "sha512-KUJ3eK2zLrPny7idWm9eQbBNhCJUjm1A1ttA6U4qiR2/ONWSffVlvr8oR26L59sVhoDkv1DOGmGPZS/bvSFisw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/node": "^7.119.2",
|
||||
"@sentry/node": "^6.0.0",
|
||||
"pino-pretty": "^6.0.0",
|
||||
"pump": "^3.0.0",
|
||||
"readable-stream": "^3.6.0",
|
||||
@@ -6148,85 +6147,118 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sentry-internal/tracing": {
|
||||
"version": "7.119.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.119.2.tgz",
|
||||
"integrity": "sha512-V2W+STWrafyGJhQv3ulMFXYDwWHiU6wHQAQBShsHVACiFaDrJ2kPRet38FKv4dMLlLlP2xN+ss2e5zv3tYlTiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.119.2",
|
||||
"@sentry/types": "7.119.2",
|
||||
"@sentry/utils": "7.119.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "7.119.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.119.2.tgz",
|
||||
"integrity": "sha512-hQr3d2yWq/2lMvoyBPOwXw1IHqTrCjOsU1vYKhAa6w9vGbJZFGhKGGE2KEi/92c3gqGn+gW/PC7cV6waCTDuVA==",
|
||||
"license": "MIT",
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.19.7.tgz",
|
||||
"integrity": "sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.119.2",
|
||||
"@sentry/utils": "7.119.2"
|
||||
"@sentry/hub": "6.19.7",
|
||||
"@sentry/minimal": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/integrations": {
|
||||
"version": "7.119.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.119.2.tgz",
|
||||
"integrity": "sha512-dCuXKvbUE3gXVVa696SYMjlhSP6CxpMH/gl4Jk26naEB8Xjsn98z/hqEoXLg6Nab73rjR9c/9AdKqBbwVMHyrQ==",
|
||||
"license": "MIT",
|
||||
"node_modules/@sentry/core/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/hub": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.19.7.tgz",
|
||||
"integrity": "sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.119.2",
|
||||
"@sentry/types": "7.119.2",
|
||||
"@sentry/utils": "7.119.2",
|
||||
"localforage": "^1.8.1"
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/hub/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/minimal": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.19.7.tgz",
|
||||
"integrity": "sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ==",
|
||||
"dependencies": {
|
||||
"@sentry/hub": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/minimal/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/node": {
|
||||
"version": "7.119.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.119.2.tgz",
|
||||
"integrity": "sha512-TPNnqxh+Myooe4jTyRiXrzrM2SH08R4+nrmBls4T7lKp2E5R/3mDSe/YTn5rRcUt1k1hPx1NgO/taG0DoS5cXA==",
|
||||
"license": "MIT",
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-6.19.7.tgz",
|
||||
"integrity": "sha512-gtmRC4dAXKODMpHXKfrkfvyBL3cI8y64vEi3fDD046uqYcrWdgoQsffuBbxMAizc6Ez1ia+f0Flue6p15Qaltg==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.119.2",
|
||||
"@sentry/core": "7.119.2",
|
||||
"@sentry/integrations": "7.119.2",
|
||||
"@sentry/types": "7.119.2",
|
||||
"@sentry/utils": "7.119.2"
|
||||
"@sentry/core": "6.19.7",
|
||||
"@sentry/hub": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"cookie": "^0.4.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"lru_map": "^0.3.3",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "7.119.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.119.2.tgz",
|
||||
"integrity": "sha512-ydq1tWsdG7QW+yFaTp0gFaowMLNVikIqM70wxWNK+u98QzKnVY/3XTixxNLsUtnAB4Y+isAzFhrc6Vb5GFdFeg==",
|
||||
"license": "MIT",
|
||||
"node_modules/@sentry/node/node_modules/cookie": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz",
|
||||
"integrity": "sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "7.119.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.119.2.tgz",
|
||||
"integrity": "sha512-TLdUCvcNgzKP0r9YD7tgCL1PEUp42TObISridsPJ5rhpVGQJvpr+Six0zIkfDUxerLYWZoK8QMm9KgFlPLNQzA==",
|
||||
"license": "MIT",
|
||||
"version": "6.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.19.7.tgz",
|
||||
"integrity": "sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.119.2"
|
||||
"@sentry/types": "6.19.7",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@serdnam/pino-cloudwatch-transport": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@serdnam/pino-cloudwatch-transport/-/pino-cloudwatch-transport-1.0.4.tgz",
|
||||
@@ -9696,9 +9728,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -10831,9 +10863,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
@@ -10841,7 +10873,7 @@
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -10873,13 +10905,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
|
||||
"integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
|
||||
"license": "MIT",
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
|
||||
"integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.7",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
@@ -10913,15 +10944,6 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
@@ -12215,28 +12237,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hdb": {
|
||||
"version": "0.19.10",
|
||||
"resolved": "https://registry.npmjs.org/hdb/-/hdb-0.19.10.tgz",
|
||||
"integrity": "sha512-er0oyute1aMjf6v41JU7z1a6Zo8lqj3muC7C4Uoi81Xf4WNdjPb424wUnXIhaf4HS8H9ARDyWrMGJTvPU2jjPw==",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.4.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/hdb/node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz",
|
||||
@@ -12424,12 +12424,6 @@
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -13426,22 +13420,13 @@
|
||||
"libsodium": "^0.7.13"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/light-my-request": {
|
||||
"version": "5.14.0",
|
||||
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz",
|
||||
"integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==",
|
||||
"version": "5.13.0",
|
||||
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.13.0.tgz",
|
||||
"integrity": "sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"cookie": "^0.7.0",
|
||||
"cookie": "^0.6.0",
|
||||
"process-warning": "^3.0.0",
|
||||
"set-cookie-parser": "^2.4.1"
|
||||
}
|
||||
@@ -13520,15 +13505,6 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -13656,6 +13632,11 @@
|
||||
"get-func-name": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lru_map": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz",
|
||||
"integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -15280,9 +15261,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -18884,9 +18865,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz",
|
||||
"integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==",
|
||||
"version": "5.4.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
|
||||
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@@ -44,7 +44,7 @@
|
||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
|
||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas",
|
||||
"auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest",
|
||||
"auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up",
|
||||
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
|
||||
@@ -156,12 +156,11 @@
|
||||
"connect-redis": "^7.1.1",
|
||||
"cron": "^3.1.7",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify": "^4.28.1",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"google-auth-library": "^9.9.0",
|
||||
"googleapis": "^137.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"hdb": "^0.19.10",
|
||||
"ioredis": "^5.3.2",
|
||||
"jmespath": "^0.16.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
4
backend/src/@types/hdb.d.ts
vendored
@@ -1,4 +0,0 @@
|
||||
declare module "hdb" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Untyped, the function returns `any`.
|
||||
function createClient(options): any;
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const BATCH_SIZE = 10_000;
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
|
||||
|
||||
if (!hasAuthMethodColumnAccessToken) {
|
||||
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
||||
t.string("authMethod").nullable();
|
||||
});
|
||||
|
||||
let nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
|
||||
let totalUpdated = 0;
|
||||
|
||||
do {
|
||||
const batchIds = nullableAccessTokens.map((token) => token.id);
|
||||
|
||||
// ! Update the auth method column in batches for the current batch
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.IdentityAccessToken)
|
||||
.whereIn("id", batchIds)
|
||||
.update({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore because generate schema happens after this
|
||||
authMethod: knex(TableName.Identity)
|
||||
.select("authMethod")
|
||||
.whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`)
|
||||
.whereNotNull("authMethod")
|
||||
.first()
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
|
||||
|
||||
totalUpdated += batchIds.length;
|
||||
console.log(`Updated ${batchIds.length} access tokens in batch <> Total updated: ${totalUpdated}`);
|
||||
} while (nullableAccessTokens.length > 0);
|
||||
|
||||
// ! We delete all access tokens where the identity has no auth method set!
|
||||
// ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted.
|
||||
await knex(TableName.IdentityAccessToken)
|
||||
.whereNotExists((queryBuilder) => {
|
||||
void queryBuilder
|
||||
.select("id")
|
||||
.from(TableName.Identity)
|
||||
.whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`)
|
||||
.whereNotNull("authMethod");
|
||||
})
|
||||
.delete();
|
||||
|
||||
// Finally we set the authMethod to notNullable after populating the column.
|
||||
// This will fail if the data is not populated correctly, so it's safe.
|
||||
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
||||
t.string("authMethod").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
// ! We aren't dropping the authMethod column from the Identity itself, because we wan't to be able to easily rollback for the time being.
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
|
||||
|
||||
if (hasAuthMethodColumnAccessToken) {
|
||||
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
|
||||
t.dropColumn("authMethod");
|
||||
});
|
||||
}
|
||||
}
|
@@ -20,7 +20,8 @@ export const IdentityAccessTokensSchema = z.object({
|
||||
identityId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
name: z.string().nullable().optional()
|
||||
name: z.string().nullable().optional(),
|
||||
authMethod: z.string()
|
||||
});
|
||||
|
||||
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;
|
||||
|
@@ -189,7 +189,7 @@ export enum ProjectUpgradeStatus {
|
||||
|
||||
export enum IdentityAuthMethod {
|
||||
TOKEN_AUTH = "token-auth",
|
||||
Univeral = "universal-auth",
|
||||
UNIVERSAL_AUTH = "universal-auth",
|
||||
KUBERNETES_AUTH = "kubernetes-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
|
@@ -16,7 +16,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
// @ts-ignore
|
||||
id: seedData1.machineIdentity.id,
|
||||
name: seedData1.machineIdentity.name,
|
||||
authMethod: IdentityAuthMethod.Univeral
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
|
||||
}
|
||||
]);
|
||||
const identityUa = await knex(TableName.IdentityUniversalAuth)
|
||||
|
@@ -1,20 +0,0 @@
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
|
||||
export const verifyHostInputValidity = (host: string) => {
|
||||
const appCfg = getConfig();
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
};
|
@@ -2,9 +2,10 @@ import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = () => {
|
||||
@@ -18,8 +19,23 @@ const generateUsername = () => {
|
||||
|
||||
export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
|
||||
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
if (
|
||||
isCloud &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||
) {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
@@ -9,7 +9,6 @@ import { MongoAtlasProvider } from "./mongo-atlas";
|
||||
import { MongoDBProvider } from "./mongo-db";
|
||||
import { RabbitMqProvider } from "./rabbit-mq";
|
||||
import { RedisDatabaseProvider } from "./redis";
|
||||
import { SapHanaProvider } from "./sap-hana";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
|
||||
export const buildDynamicSecretProviders = () => ({
|
||||
@@ -23,6 +22,5 @@ export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
||||
[DynamicSecretProviders.SapHana]: SapHanaProvider()
|
||||
[DynamicSecretProviders.Ldap]: LdapProvider()
|
||||
});
|
||||
|
@@ -166,17 +166,6 @@ export const DynamicSecretMongoDBSchema = z.object({
|
||||
)
|
||||
});
|
||||
|
||||
export const DynamicSecretSapHanaSchema = z.object({
|
||||
host: z.string().trim().toLowerCase(),
|
||||
port: z.number(),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const AzureEntraIDSchema = z.object({
|
||||
tenantId: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
@@ -207,8 +196,7 @@ export enum DynamicSecretProviders {
|
||||
MongoDB = "mongo-db",
|
||||
RabbitMq = "rabbit-mq",
|
||||
AzureEntraID = "azure-entra-id",
|
||||
Ldap = "ldap",
|
||||
SapHana = "sap-hana"
|
||||
Ldap = "ldap"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
@@ -216,7 +204,6 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }),
|
||||
|
@@ -2,9 +2,10 @@ import { MongoClient } from "mongodb";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
@@ -18,8 +19,22 @@ const generateUsername = () => {
|
||||
|
||||
export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
|
@@ -3,11 +3,12 @@ import https from "https";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = () => {
|
||||
@@ -78,8 +79,23 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa
|
||||
|
||||
export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
|
||||
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
if (
|
||||
isCloud &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||
) {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
@@ -3,10 +3,11 @@ import { Redis } from "ioredis";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = () => {
|
||||
@@ -50,8 +51,22 @@ const executeTransactions = async (connection: Redis, commands: string[]): Promi
|
||||
|
||||
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
if (
|
||||
isCloud &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
|
@@ -1,174 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import handlebars from "handlebars";
|
||||
import hdb from "hdb";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
|
||||
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
||||
const client = hdb.createClient({
|
||||
host: providerInputs.host,
|
||||
port: providerInputs.port,
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
...(providerInputs.ca
|
||||
? {
|
||||
ca: providerInputs.ca
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
client.connect((err: any) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (client.readyState) {
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
reject(new Error("SAP HANA client not ready"));
|
||||
});
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const testResult: boolean = await new Promise((resolve, reject) => {
|
||||
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
||||
if (err) {
|
||||
reject();
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
return testResult;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const client = await getClient(providerInputs);
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, username: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, username: string, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
try {
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@@ -3,9 +3,11 @@ import knex from "knex";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
@@ -27,8 +29,27 @@ const generateUsername = (provider: SqlProviders) => {
|
||||
|
||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
if (
|
||||
isCloud &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
if (
|
||||
providerInputs.host === "localhost" ||
|
||||
providerInputs.host === "127.0.0.1" ||
|
||||
// database infisical uses
|
||||
dbHost === providerInputs.host
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
|
@@ -669,12 +669,6 @@ export const RAW_SECRETS = {
|
||||
type: "The type of the secret to delete.",
|
||||
projectSlug: "The slug of the project to delete the secret in.",
|
||||
workspaceId: "The ID of the project where the secret is located."
|
||||
},
|
||||
GET_REFERENCE_TREE: {
|
||||
secretName: "The name of the secret to get the reference tree for.",
|
||||
workspaceId: "The ID of the project where the secret is located.",
|
||||
environment: "The slug of the environment where the the secret is located.",
|
||||
secretPath: "The folder path where the secret is located."
|
||||
}
|
||||
} as const;
|
||||
|
||||
|
@@ -1087,7 +1087,6 @@ export const registerRoutes = async (
|
||||
|
||||
const identityTokenAuthService = identityTokenAuthServiceFactory({
|
||||
identityTokenAuthDAL,
|
||||
identityDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
permissionService,
|
||||
@@ -1096,7 +1095,6 @@ export const registerRoutes = async (
|
||||
const identityUaService = identityUaServiceFactory({
|
||||
identityOrgMembershipDAL,
|
||||
permissionService,
|
||||
identityDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityUaClientSecretDAL,
|
||||
identityUaDAL,
|
||||
@@ -1106,7 +1104,6 @@ export const registerRoutes = async (
|
||||
identityKubernetesAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
orgBotDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
@@ -1115,7 +1112,6 @@ export const registerRoutes = async (
|
||||
identityGcpAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
@@ -1124,7 +1120,6 @@ export const registerRoutes = async (
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityDAL,
|
||||
licenseService,
|
||||
permissionService
|
||||
});
|
||||
@@ -1133,7 +1128,6 @@ export const registerRoutes = async (
|
||||
identityAzureAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
@@ -1142,7 +1136,6 @@ export const registerRoutes = async (
|
||||
identityOidcAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
orgBotDAL
|
||||
|
@@ -293,10 +293,10 @@ export const registerCmekRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "Decrypt data with KMS key",
|
||||
params: z.object({
|
||||
keyId: z.string().uuid().describe(KMS.DECRYPT.keyId)
|
||||
keyId: z.string().uuid().describe(KMS.ENCRYPT.keyId)
|
||||
}),
|
||||
body: z.object({
|
||||
ciphertext: base64Schema.describe(KMS.DECRYPT.ciphertext)
|
||||
ciphertext: base64Schema.describe(KMS.ENCRYPT.plaintext)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -37,7 +37,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identity: IdentitiesSchema
|
||||
identity: IdentitiesSchema.extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -216,7 +218,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -261,7 +265,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
}).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
@@ -319,7 +325,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
temporaryAccessEndTime: z.date().nullable().optional()
|
||||
})
|
||||
),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
}),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||
})
|
||||
)
|
||||
|
@@ -58,7 +58,9 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
})
|
||||
).array(),
|
||||
totalCount: z.number()
|
||||
|
@@ -264,7 +264,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
temporaryAccessEndTime: z.date().nullable().optional()
|
||||
})
|
||||
),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
}),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||
})
|
||||
.array(),
|
||||
@@ -285,6 +287,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
orderDirection: req.query.orderDirection,
|
||||
search: req.query.search
|
||||
});
|
||||
|
||||
return { identityMemberships, totalCount };
|
||||
}
|
||||
});
|
||||
@@ -328,7 +331,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
temporaryAccessEndTime: z.date().nullable().optional()
|
||||
})
|
||||
),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
}),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||
})
|
||||
})
|
||||
|
@@ -23,18 +23,6 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import { secretRawSchema } from "../sanitizedSchemas";
|
||||
|
||||
const SecretReferenceNode = z.object({
|
||||
key: z.string(),
|
||||
value: z.string().optional(),
|
||||
environment: z.string(),
|
||||
secretPath: z.string()
|
||||
});
|
||||
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
|
||||
|
||||
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
|
||||
children: z.lazy(() => SecretReferenceNodeTree.array())
|
||||
});
|
||||
|
||||
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@@ -2114,58 +2102,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/raw/:secretName/secret-reference-tree",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get secret reference tree",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName)
|
||||
}),
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.workspaceId),
|
||||
environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
tree: SecretReferenceNodeTree,
|
||||
value: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretName } = req.params;
|
||||
const { secretPath, environment, workspaceId } = req.query;
|
||||
const { tree, value } = await server.services.secret.getSecretReferenceTree({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: workspaceId,
|
||||
secretName,
|
||||
secretPath,
|
||||
environment
|
||||
});
|
||||
|
||||
return { tree, value };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/backfill-secret-references",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas";
|
||||
import { TableName, TIdentityAccessTokens } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -17,54 +17,27 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
const doc = await (tx || db.replicaNode())(TableName.IdentityAccessToken)
|
||||
.where(filter)
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
|
||||
.leftJoin(TableName.IdentityUaClientSecret, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn(
|
||||
`${TableName.IdentityAccessToken}.identityUAClientSecretId`,
|
||||
`${TableName.IdentityUaClientSecret}.id`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityUniversalAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn(
|
||||
`${TableName.IdentityUaClientSecret}.identityUAId`,
|
||||
`${TableName.IdentityUniversalAuth}.id`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityGcpAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.GCP_AUTH])).andOn(
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityAwsAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AWS_AUTH])).andOn(
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityAwsAuth}.identityId`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityAzureAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AZURE_AUTH])).andOn(
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityAzureAuth}.identityId`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityKubernetesAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.KUBERNETES_AUTH])).andOn(
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityOidcAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.OIDC_AUTH])).andOn(
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityOidcAuth}.identityId`
|
||||
);
|
||||
})
|
||||
.leftJoin(TableName.IdentityTokenAuth, (qb) => {
|
||||
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.TOKEN_AUTH])).andOn(
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
);
|
||||
})
|
||||
.leftJoin(
|
||||
TableName.IdentityUaClientSecret,
|
||||
`${TableName.IdentityAccessToken}.identityUAClientSecretId`,
|
||||
`${TableName.IdentityUaClientSecret}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityUniversalAuth,
|
||||
`${TableName.IdentityUaClientSecret}.identityUAId`,
|
||||
`${TableName.IdentityUniversalAuth}.id`
|
||||
)
|
||||
.leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
|
||||
|
||||
.select(selectAllTableCols(TableName.IdentityAccessToken))
|
||||
.select(
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
|
||||
@@ -82,14 +55,13 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
|
||||
return {
|
||||
...doc,
|
||||
accessTokenTrustedIps:
|
||||
doc.accessTokenTrustedIpsUa ||
|
||||
doc.accessTokenTrustedIpsGcp ||
|
||||
doc.accessTokenTrustedIpsAws ||
|
||||
doc.accessTokenTrustedIpsAzure ||
|
||||
doc.accessTokenTrustedIpsK8s ||
|
||||
doc.accessTokenTrustedIpsOidc ||
|
||||
doc.accessTokenTrustedIpsToken
|
||||
trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa,
|
||||
trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp,
|
||||
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
|
||||
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
|
||||
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
|
||||
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||
|
||||
import { TableName, TIdentityAccessTokens } from "@app/db/schemas";
|
||||
import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
|
||||
@@ -164,10 +164,22 @@ export const identityAccessTokenServiceFactory = ({
|
||||
message: "Failed to authorize revoked access token, access token is revoked"
|
||||
});
|
||||
|
||||
if (ipAddress && identityAccessToken) {
|
||||
const trustedIpsMap: Record<IdentityAuthMethod, unknown> = {
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
|
||||
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
|
||||
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
|
||||
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
|
||||
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth
|
||||
};
|
||||
|
||||
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
|
||||
|
||||
if (ipAddress) {
|
||||
checkIPAgainstBlocklist({
|
||||
ipAddress,
|
||||
trustedIps: identityAccessToken?.accessTokenTrustedIps as TIp[]
|
||||
trustedIps: trustedIps as TIp[]
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -13,7 +13,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
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";
|
||||
@@ -33,7 +32,6 @@ type TIdentityAwsAuthServiceFactoryDep = {
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
};
|
||||
@@ -44,7 +42,6 @@ export const identityAwsAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityDAL,
|
||||
licenseService,
|
||||
permissionService
|
||||
}: TIdentityAwsAuthServiceFactoryDep) => {
|
||||
@@ -113,7 +110,8 @@ export const identityAwsAuthServiceFactory = ({
|
||||
accessTokenTTL: identityAwsAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit
|
||||
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.AWS_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -155,10 +153,12 @@ export const identityAwsAuthServiceFactory = ({
|
||||
}: TAttachAwsAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity.authMethod)
|
||||
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add AWS Auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
@@ -206,13 +206,6 @@ export const identityAwsAuthServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.AWS_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityAwsAuth, orgId: identityMembershipOrg.orgId };
|
||||
@@ -234,10 +227,12 @@ export const identityAwsAuthServiceFactory = ({
|
||||
}: TUpdateAwsAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update AWS Auth"
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
|
||||
throw new NotFoundError({
|
||||
message: "The identity does not have AWS Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -293,10 +288,12 @@ export const identityAwsAuthServiceFactory = ({
|
||||
const getAwsAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAwsAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have AWS Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -320,10 +317,11 @@ export const identityAwsAuthServiceFactory = ({
|
||||
}: TRevokeAwsAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have aws auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -348,7 +346,6 @@ export const identityAwsAuthServiceFactory = ({
|
||||
|
||||
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
|
||||
const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx);
|
||||
await identityDAL.updateById(identityId, { authMethod: null }, tx);
|
||||
return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityAwsAuth;
|
||||
|
@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
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";
|
||||
@@ -32,7 +31,6 @@ type TIdentityAzureAuthServiceFactoryDep = {
|
||||
>;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@@ -43,7 +41,6 @@ export const identityAzureAuthServiceFactory = ({
|
||||
identityAzureAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TIdentityAzureAuthServiceFactoryDep) => {
|
||||
@@ -84,7 +81,8 @@ export const identityAzureAuthServiceFactory = ({
|
||||
accessTokenTTL: identityAzureAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
|
||||
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.AZURE_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -126,11 +124,12 @@ export const identityAzureAuthServiceFactory = ({
|
||||
}: TAttachAzureAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity.authMethod)
|
||||
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add Azure Auth to already configured identity"
|
||||
});
|
||||
|
||||
}
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
@@ -176,13 +175,7 @@ export const identityAzureAuthServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.AZURE_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return doc;
|
||||
});
|
||||
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
|
||||
@@ -204,10 +197,11 @@ export const identityAzureAuthServiceFactory = ({
|
||||
}: TUpdateAzureAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update Azure Auth"
|
||||
});
|
||||
}
|
||||
|
||||
const identityGcpAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -266,10 +260,11 @@ export const identityAzureAuthServiceFactory = ({
|
||||
const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Azure Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -294,10 +289,11 @@ export const identityAzureAuthServiceFactory = ({
|
||||
}: TRevokeAzureAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have azure auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -321,7 +317,6 @@ export const identityAzureAuthServiceFactory = ({
|
||||
|
||||
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||
const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx);
|
||||
await identityDAL.updateById(identityId, { authMethod: null }, tx);
|
||||
return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityAzureAuth;
|
||||
|
@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
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";
|
||||
@@ -30,7 +29,6 @@ type TIdentityGcpAuthServiceFactoryDep = {
|
||||
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@@ -41,7 +39,6 @@ export const identityGcpAuthServiceFactory = ({
|
||||
identityGcpAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TIdentityGcpAuthServiceFactoryDep) => {
|
||||
@@ -125,7 +122,8 @@ export const identityGcpAuthServiceFactory = ({
|
||||
accessTokenTTL: identityGcpAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
|
||||
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.GCP_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -168,10 +166,12 @@ export const identityGcpAuthServiceFactory = ({
|
||||
}: TAttachGcpAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity.authMethod)
|
||||
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add GCP Auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
@@ -219,13 +219,6 @@ export const identityGcpAuthServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.GCP_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
|
||||
@@ -248,10 +241,12 @@ export const identityGcpAuthServiceFactory = ({
|
||||
}: TUpdateGcpAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update GCP Auth"
|
||||
});
|
||||
}
|
||||
|
||||
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -311,10 +306,12 @@ export const identityGcpAuthServiceFactory = ({
|
||||
const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have GCP Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -339,10 +336,12 @@ export const identityGcpAuthServiceFactory = ({
|
||||
}: TRevokeGcpAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have gcp auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -366,7 +365,6 @@ export const identityGcpAuthServiceFactory = ({
|
||||
|
||||
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
|
||||
const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx);
|
||||
await identityDAL.updateById(identityId, { authMethod: null }, tx);
|
||||
return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityGcpAuth;
|
||||
|
@@ -22,7 +22,6 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
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";
|
||||
@@ -44,7 +43,6 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
|
||||
>;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@@ -56,7 +54,6 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
identityKubernetesAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
orgBotDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
@@ -215,7 +212,8 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit
|
||||
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -260,10 +258,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
}: TAttachKubernetesAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity.authMethod)
|
||||
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add Kubernetes Auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
@@ -372,13 +372,6 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
|
||||
@@ -404,10 +397,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
}: TUpdateKubernetesAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update Kubernetes Auth"
|
||||
});
|
||||
}
|
||||
|
||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -532,11 +527,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
}: TGetKubernetesAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Kubernetes Auth attached"
|
||||
});
|
||||
|
||||
}
|
||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
@@ -597,10 +593,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
}: TRevokeKubernetesAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have kubernetes auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -624,7 +622,6 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
|
||||
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
||||
const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx);
|
||||
await identityDAL.updateById(identityId, { authMethod: null }, tx);
|
||||
return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityKubernetesAuth;
|
||||
|
@@ -22,7 +22,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
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";
|
||||
@@ -41,7 +40,6 @@ type TIdentityOidcAuthServiceFactoryDep = {
|
||||
identityOidcAuthDAL: TIdentityOidcAuthDALFactory;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
|
||||
@@ -52,7 +50,6 @@ export type TIdentityOidcAuthServiceFactory = ReturnType<typeof identityOidcAuth
|
||||
export const identityOidcAuthServiceFactory = ({
|
||||
identityOidcAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
identityAccessTokenDAL,
|
||||
@@ -61,7 +58,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
|
||||
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
||||
if (!identityOidcAuth) {
|
||||
throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" });
|
||||
throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" });
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
|
||||
@@ -181,7 +178,8 @@ export const identityOidcAuthServiceFactory = ({
|
||||
accessTokenTTL: identityOidcAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit
|
||||
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.OIDC_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -228,10 +226,11 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (!identityMembershipOrg) {
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
}
|
||||
if (identityMembershipOrg.identity.authMethod)
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add OIDC Auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
@@ -334,13 +333,6 @@ export const identityOidcAuthServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.OIDC_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert };
|
||||
@@ -364,11 +356,9 @@ export const identityOidcAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TUpdateOidcAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
}
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) {
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update OIDC Auth"
|
||||
});
|
||||
@@ -467,11 +457,9 @@ export const identityOidcAuthServiceFactory = ({
|
||||
|
||||
const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
}
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) {
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have OIDC Auth attached"
|
||||
});
|
||||
@@ -519,7 +507,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
throw new NotFoundError({ message: "Failed to find identity" });
|
||||
}
|
||||
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) {
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have OIDC auth"
|
||||
});
|
||||
@@ -551,7 +539,6 @@ export const identityOidcAuthServiceFactory = ({
|
||||
|
||||
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
|
||||
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
|
||||
await identityDAL.updateById(identityId, { authMethod: null }, tx);
|
||||
return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
|
||||
|
@@ -1,12 +1,24 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TIdentities } from "@app/db/schemas";
|
||||
import {
|
||||
TableName,
|
||||
TIdentities,
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAzureAuths,
|
||||
TIdentityGcpAuths,
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityTokenAuths,
|
||||
TIdentityUniversalAuths
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types";
|
||||
|
||||
import { buildAuthMethods } from "../identity/identity-fns";
|
||||
|
||||
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
|
||||
|
||||
export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
@@ -33,11 +45,48 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembership}.id`,
|
||||
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.IdentityUniversalAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityUniversalAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityGcpAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityAwsAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityAwsAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityOidcAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityOidcAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityAzureAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityAzureAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityTokenAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.IdentityProjectMembership),
|
||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership),
|
||||
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity),
|
||||
|
||||
db.ref("id").as("identityId").withSchema(TableName.Identity),
|
||||
db.ref("name").as("identityName").withSchema(TableName.Identity),
|
||||
db.ref("id").withSchema(TableName.IdentityProjectMembership),
|
||||
@@ -52,12 +101,33 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
|
||||
db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
|
||||
);
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ identityName, identityAuthMethod, id, createdAt, updatedAt, projectId, projectName }) => ({
|
||||
parentMapper: ({
|
||||
identityName,
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
projectId,
|
||||
projectName
|
||||
}) => ({
|
||||
id,
|
||||
identityId,
|
||||
createdAt,
|
||||
@@ -65,7 +135,15 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
identity: {
|
||||
id: identityId,
|
||||
name: identityName,
|
||||
authMethod: identityAuthMethod
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
})
|
||||
},
|
||||
project: {
|
||||
id: projectId,
|
||||
@@ -150,7 +228,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
})
|
||||
.where((qb) => {
|
||||
if (filter.identityId) {
|
||||
void qb.where("identityId", filter.identityId);
|
||||
void qb.where(`${TableName.IdentityProjectMembership}.identityId`, filter.identityId);
|
||||
}
|
||||
})
|
||||
.join(
|
||||
@@ -168,6 +246,43 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembership}.id`,
|
||||
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
|
||||
)
|
||||
|
||||
.leftJoin<TIdentityUniversalAuths>(
|
||||
TableName.IdentityUniversalAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityUniversalAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityGcpAuths>(
|
||||
TableName.IdentityGcpAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAwsAuths>(
|
||||
TableName.IdentityAwsAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityAwsAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityKubernetesAuths>(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOidcAuths>(
|
||||
TableName.IdentityOidcAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityOidcAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAzureAuths>(
|
||||
TableName.IdentityAzureAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityAzureAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityTokenAuths>(
|
||||
TableName.IdentityTokenAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.IdentityProjectMembership),
|
||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
|
||||
@@ -186,7 +301,14 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole),
|
||||
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
|
||||
);
|
||||
|
||||
// TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point
|
||||
@@ -204,7 +326,21 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({
|
||||
parentMapper: ({
|
||||
identityId,
|
||||
identityName,
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
projectName
|
||||
}) => ({
|
||||
id,
|
||||
identityId,
|
||||
createdAt,
|
||||
@@ -212,7 +348,15 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
identity: {
|
||||
id: identityId,
|
||||
name: identityName,
|
||||
authMethod: identityAuthMethod
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
})
|
||||
},
|
||||
project: {
|
||||
id: projectId,
|
||||
|
@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
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";
|
||||
@@ -32,11 +31,10 @@ type TIdentityTokenAuthServiceFactoryDep = {
|
||||
TIdentityTokenAuthDALFactory,
|
||||
"transaction" | "create" | "findOne" | "updateById" | "delete"
|
||||
>;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<
|
||||
TIdentityAccessTokenDALFactory,
|
||||
"create" | "find" | "update" | "findById" | "findOne" | "updateById"
|
||||
"create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete"
|
||||
>;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@@ -46,7 +44,7 @@ export type TIdentityTokenAuthServiceFactory = ReturnType<typeof identityTokenAu
|
||||
|
||||
export const identityTokenAuthServiceFactory = ({
|
||||
identityTokenAuthDAL,
|
||||
identityDAL,
|
||||
// identityDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
permissionService,
|
||||
@@ -65,10 +63,12 @@ export const identityTokenAuthServiceFactory = ({
|
||||
}: TAttachTokenAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity.authMethod)
|
||||
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add Token Auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
@@ -112,13 +112,6 @@ export const identityTokenAuthServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
|
||||
@@ -137,10 +130,12 @@ export const identityTokenAuthServiceFactory = ({
|
||||
}: TUpdateTokenAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update Token Auth"
|
||||
message: "The identity does not have token auth"
|
||||
});
|
||||
}
|
||||
|
||||
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -197,10 +192,12 @@ export const identityTokenAuthServiceFactory = ({
|
||||
const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Token Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
|
||||
|
||||
@@ -225,10 +222,12 @@ export const identityTokenAuthServiceFactory = ({
|
||||
}: TRevokeTokenAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Token Auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -254,7 +253,11 @@ export const identityTokenAuthServiceFactory = ({
|
||||
|
||||
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
|
||||
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
|
||||
await identityDAL.updateById(identityId, { authMethod: null }, tx);
|
||||
await identityAccessTokenDAL.delete({
|
||||
identityId,
|
||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
||||
});
|
||||
|
||||
return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityTokenAuth;
|
||||
@@ -270,10 +273,12 @@ export const identityTokenAuthServiceFactory = ({
|
||||
}: TCreateTokenAuthTokenDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Token Auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -307,7 +312,8 @@ export const identityTokenAuthServiceFactory = ({
|
||||
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit,
|
||||
name
|
||||
name,
|
||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -344,10 +350,12 @@ export const identityTokenAuthServiceFactory = ({
|
||||
}: TGetTokenAuthTokensDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Token Auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -359,7 +367,8 @@ export const identityTokenAuthServiceFactory = ({
|
||||
|
||||
const tokens = await identityAccessTokenDAL.find(
|
||||
{
|
||||
identityId
|
||||
identityId,
|
||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
||||
},
|
||||
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||
);
|
||||
@@ -375,16 +384,21 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateTokenAuthTokenDTO) => {
|
||||
const foundToken = await identityAccessTokenDAL.findById(tokenId);
|
||||
const foundToken = await identityAccessTokenDAL.findOne({
|
||||
id: tokenId,
|
||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
||||
});
|
||||
if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` });
|
||||
}
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Token Auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -409,6 +423,7 @@ export const identityTokenAuthServiceFactory = ({
|
||||
|
||||
const [token] = await identityAccessTokenDAL.update(
|
||||
{
|
||||
authMethod: IdentityAuthMethod.TOKEN_AUTH,
|
||||
identityId: foundToken.identityId,
|
||||
id: tokenId
|
||||
},
|
||||
@@ -429,7 +444,8 @@ export const identityTokenAuthServiceFactory = ({
|
||||
}: TRevokeTokenAuthTokenDTO) => {
|
||||
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
||||
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
|
||||
isAccessTokenRevoked: false
|
||||
isAccessTokenRevoked: false,
|
||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
||||
});
|
||||
if (!identityAccessToken)
|
||||
throw new NotFoundError({
|
||||
@@ -453,9 +469,15 @@ export const identityTokenAuthServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
|
||||
isAccessTokenRevoked: true
|
||||
});
|
||||
const [revokedToken] = await identityAccessTokenDAL.update(
|
||||
{
|
||||
id: identityAccessToken.id,
|
||||
authMethod: IdentityAuthMethod.TOKEN_AUTH
|
||||
},
|
||||
{
|
||||
isAccessTokenRevoked: true
|
||||
}
|
||||
);
|
||||
|
||||
return { revokedToken };
|
||||
};
|
||||
|
@@ -14,7 +14,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
|
||||
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
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";
|
||||
@@ -36,7 +35,6 @@ type TIdentityUaServiceFactoryDep = {
|
||||
identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory;
|
||||
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
|
||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@@ -48,7 +46,6 @@ export const identityUaServiceFactory = ({
|
||||
identityUaClientSecretDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TIdentityUaServiceFactoryDep) => {
|
||||
@@ -115,7 +112,8 @@ export const identityUaServiceFactory = ({
|
||||
accessTokenTTL: identityUa.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityUa.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit
|
||||
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -156,10 +154,12 @@ export const identityUaServiceFactory = ({
|
||||
}: TAttachUaDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity.authMethod)
|
||||
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add universal auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
@@ -221,13 +221,6 @@ export const identityUaServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.Univeral
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityUa, orgId: identityMembershipOrg.orgId };
|
||||
@@ -247,10 +240,12 @@ export const identityUaServiceFactory = ({
|
||||
}: TUpdateUaDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to updated universal auth"
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
}
|
||||
|
||||
const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
|
||||
|
||||
@@ -321,10 +316,12 @@ export const identityUaServiceFactory = ({
|
||||
const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
}
|
||||
|
||||
const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
|
||||
|
||||
@@ -348,10 +345,12 @@ export const identityUaServiceFactory = ({
|
||||
}: TRevokeUaDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -375,7 +374,6 @@ export const identityUaServiceFactory = ({
|
||||
|
||||
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
|
||||
const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx);
|
||||
await identityDAL.updateById(identityId, { authMethod: null }, tx);
|
||||
return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityUniversalAuth;
|
||||
@@ -393,10 +391,13 @@ export const identityUaServiceFactory = ({
|
||||
}: TCreateUaClientSecretDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -422,12 +423,11 @@ export const identityUaServiceFactory = ({
|
||||
const appCfg = getConfig();
|
||||
const clientSecret = crypto.randomBytes(32).toString("hex");
|
||||
const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS);
|
||||
const identityUniversalAuth = await identityUaDAL.findOne({
|
||||
identityId
|
||||
});
|
||||
|
||||
const identityUaAuth = await identityUaDAL.findOne({ identityId: identityMembershipOrg.identityId });
|
||||
|
||||
const identityUaClientSecret = await identityUaClientSecretDAL.create({
|
||||
identityUAId: identityUniversalAuth.id,
|
||||
identityUAId: identityUaAuth.id,
|
||||
description,
|
||||
clientSecretPrefix: clientSecret.slice(0, 4),
|
||||
clientSecretHash,
|
||||
@@ -439,7 +439,6 @@ export const identityUaServiceFactory = ({
|
||||
return {
|
||||
clientSecret,
|
||||
clientSecretData: identityUaClientSecret,
|
||||
uaAuth: identityUniversalAuth,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
};
|
||||
};
|
||||
@@ -453,10 +452,12 @@ export const identityUaServiceFactory = ({
|
||||
}: TGetUaClientSecretsDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -500,10 +501,13 @@ export const identityUaServiceFactory = ({
|
||||
}: TGetUniversalAuthClientSecretByIdDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -539,10 +543,13 @@ export const identityUaServiceFactory = ({
|
||||
}: TRevokeUaClientSecretDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
|
29
backend/src/services/identity/identity-fns.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
|
||||
export const buildAuthMethods = ({
|
||||
uaId,
|
||||
gcpId,
|
||||
awsId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
}: {
|
||||
uaId?: string;
|
||||
gcpId?: string;
|
||||
awsId?: string;
|
||||
kubernetesId?: string;
|
||||
oidcId?: string;
|
||||
azureId?: string;
|
||||
tokenId?: string;
|
||||
}) => {
|
||||
return [
|
||||
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
|
||||
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
|
||||
...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
|
||||
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
|
||||
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
|
||||
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
|
||||
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null]
|
||||
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
|
||||
};
|
@@ -1,12 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TIdentityOrgMemberships, TOrgRoles } from "@app/db/schemas";
|
||||
import {
|
||||
TableName,
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAzureAuths,
|
||||
TIdentityGcpAuths,
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOrgMemberships,
|
||||
TIdentityTokenAuths,
|
||||
TIdentityUniversalAuths,
|
||||
TOrgRoles
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
||||
|
||||
import { buildAuthMethods } from "./identity-fns";
|
||||
|
||||
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
|
||||
|
||||
export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
@@ -15,14 +28,73 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
const findOne = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
|
||||
try {
|
||||
const [data] = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||
.where(filter)
|
||||
.where((queryBuilder) => {
|
||||
Object.entries(filter).forEach(([key, value]) => {
|
||||
void queryBuilder.where(`${TableName.IdentityOrgMembership}.${key}`, value);
|
||||
});
|
||||
})
|
||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||
.select(db.ref("name").withSchema(TableName.Identity))
|
||||
.select(db.ref("authMethod").withSchema(TableName.Identity));
|
||||
|
||||
.leftJoin<TIdentityUniversalAuths>(
|
||||
TableName.IdentityUniversalAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityUniversalAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityGcpAuths>(
|
||||
TableName.IdentityGcpAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAwsAuths>(
|
||||
TableName.IdentityAwsAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityAwsAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityKubernetesAuths>(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOidcAuths>(
|
||||
TableName.IdentityOidcAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityOidcAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAzureAuths>(
|
||||
TableName.IdentityAzureAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityAzureAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityTokenAuths>(
|
||||
TableName.IdentityTokenAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
selectAllTableCols(TableName.IdentityOrgMembership),
|
||||
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
);
|
||||
|
||||
if (data) {
|
||||
const { name, authMethod } = data;
|
||||
return { ...data, identity: { id: data.identityId, name, authMethod } };
|
||||
const { name } = data;
|
||||
return {
|
||||
...data,
|
||||
identity: {
|
||||
id: data.identityId,
|
||||
name,
|
||||
authMethods: buildAuthMethods(data)
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindOne" });
|
||||
@@ -51,8 +123,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
|
||||
.select(
|
||||
selectAllTableCols(TableName.IdentityOrgMembership),
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("authMethod").withSchema(TableName.Identity).as("identityAuthMethod")
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName")
|
||||
)
|
||||
.where(filter)
|
||||
.as("paginatedIdentity");
|
||||
@@ -70,11 +141,49 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
const query = (tx || db.replicaNode())
|
||||
.from<TSubquery[number], TSubquery>(paginatedIdentity)
|
||||
.leftJoin<TOrgRoles>(TableName.OrgRoles, `paginatedIdentity.roleId`, `${TableName.OrgRoles}.id`)
|
||||
|
||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`paginatedIdentity.identityId`, `${TableName.IdentityMetadata}.identityId`)
|
||||
.andOn(`paginatedIdentity.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||
})
|
||||
|
||||
.leftJoin<TIdentityUniversalAuths>(
|
||||
TableName.IdentityUniversalAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityUniversalAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityGcpAuths>(
|
||||
TableName.IdentityGcpAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityGcpAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAwsAuths>(
|
||||
TableName.IdentityAwsAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityAwsAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityKubernetesAuths>(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOidcAuths>(
|
||||
TableName.IdentityOidcAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityOidcAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityAzureAuths>(
|
||||
TableName.IdentityAzureAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityAzureAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityTokenAuths>(
|
||||
TableName.IdentityTokenAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityTokenAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("id").withSchema("paginatedIdentity"),
|
||||
db.ref("role").withSchema("paginatedIdentity"),
|
||||
@@ -82,9 +191,16 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("orgId").withSchema("paginatedIdentity"),
|
||||
db.ref("createdAt").withSchema("paginatedIdentity"),
|
||||
db.ref("updatedAt").withSchema("paginatedIdentity"),
|
||||
db.ref("identityId").withSchema("paginatedIdentity"),
|
||||
db.ref("identityId").withSchema("paginatedIdentity").as("identityId"),
|
||||
db.ref("identityName").withSchema("paginatedIdentity"),
|
||||
db.ref("identityAuthMethod").withSchema("paginatedIdentity")
|
||||
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
|
||||
)
|
||||
// cr stands for custom role
|
||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||
@@ -114,11 +230,17 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
crName,
|
||||
identityId,
|
||||
identityName,
|
||||
identityAuthMethod,
|
||||
role,
|
||||
roleId,
|
||||
id,
|
||||
orgId,
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}) => ({
|
||||
@@ -126,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
roleId,
|
||||
identityId,
|
||||
id,
|
||||
|
||||
orgId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
@@ -141,7 +264,15 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
identity: {
|
||||
id: identityId,
|
||||
name: identityName,
|
||||
authMethod: identityAuthMethod as string
|
||||
authMethods: buildAuthMethods({
|
||||
uaId,
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
})
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
|
@@ -93,7 +93,7 @@ export const identityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
}
|
||||
return newIdentity;
|
||||
return { ...newIdentity, authMethods: [] };
|
||||
});
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
|
||||
|
@@ -11,8 +11,6 @@ import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
|
||||
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
|
||||
// akhilmhdh: JS regex with global save state in .test
|
||||
const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([^}]+)}/;
|
||||
|
||||
export const shouldUseSecretV2Bridge = (version: number) => version === 3;
|
||||
|
||||
@@ -378,13 +376,6 @@ const formatMultiValueEnv = (val?: string) => {
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
};
|
||||
|
||||
type TSecretReferenceTraceNode = {
|
||||
key: string;
|
||||
value?: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
children: TSecretReferenceTraceNode[];
|
||||
};
|
||||
type TInterpolateSecretArg = {
|
||||
projectId: string;
|
||||
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
|
||||
@@ -426,21 +417,14 @@ export const expandSecretReferencesFactory = ({
|
||||
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
|
||||
};
|
||||
|
||||
const recursivelyExpandSecret = async (dto: {
|
||||
value?: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
shouldStackTrace?: boolean;
|
||||
}) => {
|
||||
const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode;
|
||||
const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => {
|
||||
if (!dto.value) return "";
|
||||
|
||||
if (!dto.value) return { expandedValue: "", stackTrace };
|
||||
const stack = [{ ...dto, depth: 0, trace: stackTrace }];
|
||||
const stack = [{ ...dto, depth: 0 }];
|
||||
let expandedValue = dto.value;
|
||||
|
||||
while (stack.length) {
|
||||
const { value, secretPath, environment, depth, trace } = stack.pop()!;
|
||||
|
||||
const { value, secretPath, environment, depth } = stack.pop()!;
|
||||
// eslint-disable-next-line no-continue
|
||||
if (depth > MAX_SECRET_REFERENCE_DEPTH) continue;
|
||||
const refs = value?.match(INTERPOLATION_SYNTAX_REG);
|
||||
@@ -453,11 +437,6 @@ export const expandSecretReferencesFactory = ({
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!entities.length) continue;
|
||||
|
||||
let referencedSecretPath = "";
|
||||
let referencedSecretKey = "";
|
||||
let referencedSecretEnvironmentSlug = "";
|
||||
let referencedSecretValue = "";
|
||||
|
||||
if (entities.length === 1) {
|
||||
const [secretKey] = entities;
|
||||
|
||||
@@ -470,11 +449,17 @@ export const expandSecretReferencesFactory = ({
|
||||
|
||||
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
||||
secretCache[cacheKey][secretKey] = referredValue;
|
||||
|
||||
referencedSecretValue = referredValue.value;
|
||||
referencedSecretKey = secretKey;
|
||||
referencedSecretPath = secretPath;
|
||||
referencedSecretEnvironmentSlug = environment;
|
||||
if (INTERPOLATION_SYNTAX_REG.test(referredValue.value)) {
|
||||
stack.push({
|
||||
value: referredValue.value,
|
||||
secretPath,
|
||||
environment,
|
||||
depth: depth + 1
|
||||
});
|
||||
}
|
||||
if (referredValue) {
|
||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referredValue.value);
|
||||
}
|
||||
} else {
|
||||
const secretReferenceEnvironment = entities[0];
|
||||
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
|
||||
@@ -489,42 +474,24 @@ export const expandSecretReferencesFactory = ({
|
||||
|
||||
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
|
||||
secretCache[cacheKey][secretReferenceKey] = referedValue;
|
||||
|
||||
referencedSecretValue = referedValue.value;
|
||||
referencedSecretKey = secretReferenceKey;
|
||||
referencedSecretPath = secretReferencePath;
|
||||
referencedSecretEnvironmentSlug = secretReferenceEnvironment;
|
||||
}
|
||||
|
||||
const node = {
|
||||
value: referencedSecretValue,
|
||||
secretPath: referencedSecretPath,
|
||||
environment: referencedSecretEnvironmentSlug,
|
||||
depth: depth + 1,
|
||||
trace
|
||||
};
|
||||
|
||||
const shouldExpandMore = INTERPOLATION_SYNTAX_REG_NON_GLOBAL.test(referencedSecretValue);
|
||||
if (dto.shouldStackTrace) {
|
||||
const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null };
|
||||
trace?.children.push(stackTraceNode);
|
||||
// if stack trace this would be child node
|
||||
if (shouldExpandMore) {
|
||||
stack.push({ ...node, trace: stackTraceNode });
|
||||
if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) {
|
||||
stack.push({
|
||||
value: referedValue.value,
|
||||
secretPath: secretReferencePath,
|
||||
environment: secretReferenceEnvironment,
|
||||
depth: depth + 1
|
||||
});
|
||||
}
|
||||
} else if (shouldExpandMore) {
|
||||
// if no stack trace is needed we just keep going with root node
|
||||
stack.push(node);
|
||||
}
|
||||
|
||||
if (referencedSecretValue) {
|
||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referencedSecretValue);
|
||||
if (referedValue) {
|
||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { expandedValue, stackTrace };
|
||||
return expandedValue;
|
||||
};
|
||||
|
||||
const expandSecret = async (inputSecret: {
|
||||
@@ -538,21 +505,10 @@ export const expandSecretReferencesFactory = ({
|
||||
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
|
||||
if (!shouldExpand) return inputSecret.value;
|
||||
|
||||
const { expandedValue } = await recursivelyExpandSecret(inputSecret);
|
||||
|
||||
return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedValue) : expandedValue;
|
||||
const expandedSecretValue = await recursivelyExpandSecret(inputSecret);
|
||||
return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedSecretValue) : expandedSecretValue;
|
||||
};
|
||||
|
||||
const getExpandedSecretStackTrace = async (inputSecret: {
|
||||
value?: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
}) => {
|
||||
const { stackTrace, expandedValue } = await recursivelyExpandSecret({ ...inputSecret, shouldStackTrace: true });
|
||||
return { stackTrace, expandedValue };
|
||||
};
|
||||
|
||||
return { expandSecretReferences: expandSecret, getExpandedSecretStackTrace };
|
||||
return expandSecret;
|
||||
};
|
||||
|
||||
export const reshapeBridgeSecret = (
|
||||
|
@@ -41,7 +41,6 @@ import {
|
||||
TDeleteManySecretDTO,
|
||||
TDeleteSecretDTO,
|
||||
TGetASecretDTO,
|
||||
TGetSecretReferencesTreeDTO,
|
||||
TGetSecretsDTO,
|
||||
TGetSecretVersionsDTO,
|
||||
TMoveSecretsDTO,
|
||||
@@ -816,7 +815,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
const { expandSecretReferences } = expandSecretReferencesFactory({
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
projectId,
|
||||
folderDAL,
|
||||
secretDAL,
|
||||
@@ -966,7 +965,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
const { expandSecretReferences } = expandSecretReferencesFactory({
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
projectId,
|
||||
folderDAL,
|
||||
secretDAL,
|
||||
@@ -1033,7 +1032,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
value: secretValue,
|
||||
skipMultilineEncoding: secret.skipMultilineEncoding
|
||||
});
|
||||
|
||||
secretValue = expandedSecretValue || "";
|
||||
}
|
||||
|
||||
@@ -1930,88 +1928,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretReferenceTree = async ({
|
||||
environment,
|
||||
secretPath,
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
secretName,
|
||||
actorAuthMethod
|
||||
}: TGetSecretReferencesTreeDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new NotFoundError({
|
||||
message: "Folder not found for the given environment slug & secret path",
|
||||
name: "Create secret"
|
||||
});
|
||||
const folderId = folder.id;
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const secret = await secretDAL.findOne({
|
||||
folderId,
|
||||
key: secretName,
|
||||
type: SecretType.Shared
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: (secret?.tags || []).map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
|
||||
const secretValue = secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: "";
|
||||
|
||||
const { getExpandedSecretStackTrace } = expandSecretReferencesFactory({
|
||||
projectId,
|
||||
folderDAL,
|
||||
secretDAL,
|
||||
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
|
||||
canExpandValue: (expandEnvironment, expandSecretPath, expandSecretName, expandSecretTags) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: expandEnvironment,
|
||||
secretPath: expandSecretPath,
|
||||
secretName: expandSecretName,
|
||||
secretTags: expandSecretTags
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({
|
||||
environment,
|
||||
secretPath,
|
||||
value: secretValue
|
||||
});
|
||||
|
||||
return { tree: stackTrace, value: expandedValue };
|
||||
};
|
||||
|
||||
return {
|
||||
createSecret,
|
||||
deleteSecret,
|
||||
@@ -2026,7 +1942,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
moveSecrets,
|
||||
getSecretsCount,
|
||||
getSecretsCountMultiEnv,
|
||||
getSecretsMultiEnv,
|
||||
getSecretReferenceTree
|
||||
getSecretsMultiEnv
|
||||
};
|
||||
};
|
||||
|
@@ -278,10 +278,3 @@ export type TAttachSecretTagsDTO = {
|
||||
secretPath: string;
|
||||
type: SecretType;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetSecretReferencesTreeDTO = {
|
||||
projectId: string;
|
||||
secretName: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@@ -299,7 +299,7 @@ export const secretQueueFactory = ({
|
||||
);
|
||||
return content;
|
||||
}
|
||||
const { expandSecretReferences } = expandSecretReferencesFactory({
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
decryptSecretValue: dto.decryptor,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
|
@@ -38,7 +38,6 @@ import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
|
||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||
import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
|
||||
import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types";
|
||||
import { TSecretDALFactory } from "./secret-dal";
|
||||
import {
|
||||
decryptSecretRaw,
|
||||
@@ -1100,18 +1099,6 @@ export const secretServiceFactory = ({
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const getSecretReferenceTree = async (dto: TGetSecretReferencesTreeDTO) => {
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(dto.projectId);
|
||||
|
||||
if (!shouldUseSecretV2Bridge)
|
||||
throw new BadRequestError({
|
||||
message: "Project version does not support secret reference tree",
|
||||
name: "SecretReferenceTreeNotSupported"
|
||||
});
|
||||
|
||||
return secretV2BridgeService.getSecretReferenceTree(dto);
|
||||
};
|
||||
|
||||
const getSecretsRaw = async ({
|
||||
projectId,
|
||||
path,
|
||||
@@ -2870,7 +2857,6 @@ export const secretServiceFactory = ({
|
||||
startSecretV2Migration,
|
||||
getSecretsCount,
|
||||
getSecretsCountMultiEnv,
|
||||
getSecretsRawMultiEnv,
|
||||
getSecretReferenceTree
|
||||
getSecretsRawMultiEnv
|
||||
};
|
||||
};
|
||||
|
@@ -1,65 +0,0 @@
|
||||
---
|
||||
title: "Attribute-based Access Controls"
|
||||
description: "Learn how to use ABAC to manage permissions based on identity attributes."
|
||||
---
|
||||
|
||||
Infisical's Attribute-based Access Controls (ABAC) allow for dynamic, attribute-driven permissions for both user and machine identities.
|
||||
ABAC policies use metadata attributes—stored as key-value pairs on identities—to enforce fine-grained permissions that are context aware.
|
||||
|
||||
In ABAC, access controls are defined using metadata attributes, such as location or department, which can be set directly on user or machine identities.
|
||||
During policy execution, these attributes are evaluated, and determine whether said actor can access the requested resource or perform the requested operation.
|
||||
|
||||
## Project-level Permissions
|
||||
|
||||
Attribute-based access controls are currently available for polices defined on projects. You can set ABAC permissions to control access to environments, folders, secrets, and secret tags.
|
||||
|
||||
### Setting Metadata on Identities
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Manually Configure Metadata">
|
||||
<Steps>
|
||||
<Step title="Navigate to the Access Control page on the organization sidebar and select an identity (user or machine).">
|
||||
<img src="/images/platform/access-controls/add-metadata-step1.png" />
|
||||
</Step>
|
||||
<Step title="On the Identity Page, click the pencil icon to edit the selected identity.">
|
||||
<img src="/images/platform/access-controls/add-metadata-step2.png" />
|
||||
</Step>
|
||||
<Step title="Add metadata via key-value pairs and update the identity.">
|
||||
<img src="/images/platform/access-controls/add-metadata-step3.png" />
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
<Tab title="Automatically Populate Metadata">
|
||||
For organizations using SAML for login, Infisical automatically maps metadata attributes from SAML assertions to user identities.
|
||||
This makes it easy to create policies that dynamically adapt based on the SAML user’s attributes.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Defining ABAC Policies
|
||||
|
||||
<img src="/images/platform/access-controls/example-abac-1.png" />
|
||||
|
||||
ABAC policies make use of identity metadata to define dynamic permissions. Each attribute must start and end with double curly-brackets `{{ <attribute-name> }}`.
|
||||
The following attributes are available within project permissions:
|
||||
|
||||
- **User ID**: `{{ identity.id }}`
|
||||
- **Username**: `{{ identity.username }}`
|
||||
- **Metadata Attributes**: `{{ identity.metadata.<metadata-key-name> }}`
|
||||
|
||||
During policy execution, these placeholders are replaced by their actual values prior to evaluation.
|
||||
|
||||
### Example Use Case
|
||||
|
||||
#### Location-based Access Control
|
||||
|
||||
Suppose you want to restrict access to secrets within a specific folder based on a user's geographic region.
|
||||
You could assign a `location` attribute to each user (e.g., `identity.metadata.location`).
|
||||
You could then structure your folders to align with this attribute and define permissions accordingly.
|
||||
|
||||
For example, a policy might restrict access to folders matching the user's location attribute in the following pattern:
|
||||
```
|
||||
/appA/{{ identity.metadata.location }}
|
||||
```
|
||||
Using this structure, users can only access folders that correspond to their configured `location` attribute.
|
||||
Consequently, if a users attribute changes due to relocation, no policies need to be changed to gain access to the folders associated with their new location.
|
@@ -15,15 +15,6 @@ To make sure that users and machine identities are only accessing the resources
|
||||
>
|
||||
Manage user and machine identitity permissions through predefined roles.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Attribute-based Access Control"
|
||||
href="./attribute-based-access-controls"
|
||||
icon="address-book"
|
||||
color="#000000"
|
||||
>
|
||||
Manage user and machine identitity permissions based on their attributes.
|
||||
</Card>
|
||||
<Card
|
||||
title="Additional Privileges"
|
||||
href="./additional-privileges"
|
||||
|
@@ -20,10 +20,10 @@ The logs are formatted in JSON, requiring your logging provider to support JSON-
|
||||
<Steps>
|
||||
<Step title="Navigate to Organization Settings in your sidebar." />
|
||||
<Step title="Select Audit Log Streams Tab.">
|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Click on Create">
|
||||

|
||||

|
||||
|
||||
Provide the following values
|
||||
<ParamField path="Endpoint URL" type="string" required>
|
||||
@@ -44,11 +44,11 @@ Your Audit Logs are now ready to be streamed.
|
||||
|
||||
<Steps>
|
||||
<Step title="Select Connect Source">
|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Provide a name and select platform"/>
|
||||
<Step title="Provide Audit Log Stream inputs">
|
||||

|
||||

|
||||
|
||||
1. Copy the **endpoint** from Better Stack to the **Endpoint URL** field.
|
||||
3. Create a new header with key **Authorization** and set the value as **Bearer \<source token from betterstack\>**.
|
||||
@@ -59,21 +59,21 @@ Your Audit Logs are now ready to be streamed.
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to API Keys section">
|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Select New Key and provide a key name">
|
||||

|
||||

|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Find your Datadog region specific logging endpoint.">
|
||||

|
||||

|
||||
|
||||
1. Navigate to the [Datadog Send Logs API documentation](https://docs.datadoghq.com/api/latest/logs/?code-lang=curl&site=us5#send-logs).
|
||||
2. Pick your Datadog account region.
|
||||
3. Obtain your Datadog logging endpoint URL.
|
||||
</Step>
|
||||
<Step title="Provide audit log stream inputs">
|
||||

|
||||

|
||||
|
||||
1. Copy the **logging endpoint** from Datadog to the **Endpoint URL** field.
|
||||
2. Copy the **API Key** from previous step
|
||||
|
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: "SAP HANA"
|
||||
description: "Learn how to dynamically generate SAP HANA database account credentials."
|
||||
---
|
||||
|
||||
The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database credentials on demand.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- Infisical requires a SAP HANA database user in your instance with the necessary permissions. This user will facilitate the creation of new accounts as needed.
|
||||
Ensure the user possesses privileges for creating, dropping, and granting permissions to roles for it to be able to create dynamic secrets.
|
||||
|
||||
- The SAP HANA instance should be reachable by Infisical.
|
||||
|
||||
## Set up Dynamic Secrets with SAP HANA
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Overview Dashboard">
|
||||
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select SAP HANA">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Host" type="string" required>
|
||||
SAP HANA Host
|
||||
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Port" type="number" required>
|
||||
SAP HANA Port
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="User" type="string" required>
|
||||
Username that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Password" type="string" required>
|
||||
Password that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="CA(SSL)" type="string">
|
||||
A CA may be required for SSL if you are self-hosting SAP HANA
|
||||
|
||||
</ParamField>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="(Optional) Modify SQL Statements">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs.
|
||||

|
||||
|
||||
<Warning>
|
||||
Due to SAP HANA limitations, the attached SQL statements are not executed as a transaction.
|
||||
</Warning>
|
||||
|
||||
</Step>
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
|
||||
<Note>
|
||||
If this step fails, you may have to add the CA certficate.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
<Step title="Generate dynamic secrets">
|
||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||
|
||||

|
||||

|
||||
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||
</Tip>
|
||||
|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Audit or Revoke Leases
|
||||
|
||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||
This will allow you see the lease details and delete the lease ahead of its expiration time.
|
||||
|
||||

|
||||
|
||||
## Renew Leases
|
||||
|
||||
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
|
||||

|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic
|
||||
secret.
|
||||
</Warning>
|
@@ -20,7 +20,7 @@ Key Features:
|
||||
|
||||
A typical workflow for using identities consists of four steps:
|
||||
|
||||
1. Creating the identity with a name and [role](/documentation/platform/access-controls/role-based-access-controls) in Organization Access Control > Machine Identities.
|
||||
1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities.
|
||||
This step also involves configuring an authentication method for it.
|
||||
2. Adding the identity to the project(s) you want it to have access to.
|
||||
3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
|
||||
|
Before Width: | Height: | Size: 800 KiB |
Before Width: | Height: | Size: 933 KiB |
Before Width: | Height: | Size: 499 KiB |
Before Width: | Height: | Size: 802 KiB |
Before Width: | Height: | Size: 503 KiB |
Before Width: | Height: | Size: 457 KiB |
Before Width: | Height: | Size: 607 KiB |
@@ -10,9 +10,7 @@ It uses an `InfisicalSecret` resource to specify authentication and storage meth
|
||||
The operator continuously updates secrets and can also reload dependent deployments automatically.
|
||||
|
||||
<Note>
|
||||
If you are already using the External Secrets operator, you can view the
|
||||
integration documentation for it
|
||||
[here](https://external-secrets.io/latest/provider/infisical/).
|
||||
If you are already using the External Secrets operator, you can view the integration documentation for it [here](https://external-secrets.io/latest/provider/infisical/).
|
||||
</Note>
|
||||
|
||||
## Install Operator
|
||||
@@ -33,7 +31,7 @@ The operator can be install via [Helm](https://helm.sh) or [kubectl](https://git
|
||||
To select a specific version, view the application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags) and chart versions [here](https://cloudsmith.io/~infisical/repos/helm-charts/packages/detail/helm/secrets-operator/#versions)
|
||||
|
||||
```bash
|
||||
helm install --generate-name infisical-helm-charts/secrets-operator
|
||||
helm install --generate-name infisical-helm-charts/secrets-operator
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -63,106 +61,109 @@ Once you apply the manifest, the operator will be installed in `infisical-operat
|
||||
Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD).
|
||||
|
||||
```yaml example-infisical-secret-crd.yaml
|
||||
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: InfisicalSecret
|
||||
metadata:
|
||||
name: infisicalsecret-sample
|
||||
labels:
|
||||
label-to-be-passed-to-managed-secret: sample-value
|
||||
annotations:
|
||||
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
|
||||
name: infisicalsecret-sample
|
||||
labels:
|
||||
label-to-be-passed-to-managed-secret: sample-value
|
||||
annotations:
|
||||
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
|
||||
spec:
|
||||
hostAPI: https://app.infisical.com/api
|
||||
resyncInterval: 10
|
||||
authentication:
|
||||
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||
# If you have multiple authentication methods defined, it may cause issues.
|
||||
hostAPI: https://app.infisical.com/api
|
||||
resyncInterval: 10
|
||||
authentication:
|
||||
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||
# If you have multiple authentication methods defined, it may cause issues.
|
||||
|
||||
# (Deprecated) Service Token Auth
|
||||
serviceToken:
|
||||
serviceTokenSecretReference:
|
||||
secretName: service-token
|
||||
# (Deprecated) Service Token Auth
|
||||
serviceToken:
|
||||
serviceTokenSecretReference:
|
||||
secretName: service-token
|
||||
secretNamespace: default
|
||||
secretsScope:
|
||||
envSlug: <env-slug>
|
||||
secretsPath: <secrets-path>
|
||||
recursive: true
|
||||
|
||||
# Universal Auth
|
||||
universalAuth:
|
||||
secretsScope:
|
||||
projectSlug: new-ob-em
|
||||
envSlug: dev # "dev", "staging", "prod", etc..
|
||||
secretsPath: "/" # Root is "/"
|
||||
recursive: true # Wether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false
|
||||
credentialsRef:
|
||||
secretName: universal-auth-credentials
|
||||
secretNamespace: default
|
||||
|
||||
# Native Kubernetes Auth
|
||||
kubernetesAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountRef:
|
||||
name: <service-account-name>
|
||||
namespace: <service-account-namespace>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# AWS IAM Auth
|
||||
awsIamAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# Azure Auth
|
||||
azureAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
resource: https://management.azure.com/&client_id=CLIENT_ID # (Optional) This is the Azure resource that you want to access. For example, "https://management.azure.com/". If no value is provided, it will default to "https://management.azure.com/"
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# GCP ID Token Auth
|
||||
gcpIdTokenAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# GCP IAM Auth
|
||||
gcpIamAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
secretsScope:
|
||||
envSlug: <env-slug>
|
||||
secretsPath: <secrets-path>
|
||||
recursive: true
|
||||
creationPolicy: "Orphan" ## Owner | Orphan
|
||||
# secretType: kubernetes.io/dockerconfigjson
|
||||
|
||||
# Universal Auth
|
||||
universalAuth:
|
||||
secretsScope:
|
||||
projectSlug: new-ob-em
|
||||
envSlug: dev # "dev", "staging", "prod", etc..
|
||||
secretsPath: "/" # Root is "/"
|
||||
recursive: true # Wether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false
|
||||
credentialsRef:
|
||||
secretName: universal-auth-credentials
|
||||
secretNamespace: default
|
||||
|
||||
# Native Kubernetes Auth
|
||||
kubernetesAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountRef:
|
||||
name: <service-account-name>
|
||||
namespace: <service-account-namespace>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# AWS IAM Auth
|
||||
awsIamAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# Azure Auth
|
||||
azureAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
resource: https://management.azure.com/&client_id=CLIENT_ID # (Optional) This is the Azure resource that you want to access. For example, "https://management.azure.com/". If no value is provided, it will default to "https://management.azure.com/"
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# GCP ID Token Auth
|
||||
gcpIdTokenAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
# GCP IAM Auth
|
||||
gcpIamAuth:
|
||||
identityId: <your-machine-identity-id>
|
||||
|
||||
# secretsScope is identical to the secrets scope in the universalAuth field in this sample.
|
||||
secretsScope:
|
||||
projectSlug: your-project-slug
|
||||
envSlug: prod
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan" ## Owner | Orphan
|
||||
# secretType: kubernetes.io/dockerconfigjson
|
||||
```
|
||||
|
||||
### InfisicalSecret CRD properties
|
||||
@@ -192,31 +193,6 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
available on paid plans. Default re-sync interval is every 1 minute.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="tls">
|
||||
This block defines the TLS settings to use for connecting to the Infisical
|
||||
instance.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="tls.caRef">
|
||||
This block defines the reference to the CA certificate to use for connecting
|
||||
to the Infisical instance with SSL/TLS.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="tls.caRef.secretName">
|
||||
The name of the Kubernetes secret containing the CA certificate to use for
|
||||
connecting to the Infisical instance with SSL/TLS.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="tls.caRef.secretNamespace">
|
||||
The namespace of the Kubernetes secret containing the CA certificate to use
|
||||
for connecting to the Infisical instance with SSL/TLS.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="tls.caRef.key">
|
||||
The name of the key in the Kubernetes secret which contains the value of the
|
||||
CA certificate to use for connecting to the Infisical instance with SSL/TLS.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="authentication">
|
||||
This block defines the method that will be used to authenticate with Infisical
|
||||
so that secrets can be fetched
|
||||
@@ -246,6 +222,8 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
|
||||
</Steps>
|
||||
|
||||
|
||||
|
||||
<Info>
|
||||
Make sure to also populate the `secretsScope` field with the project slug
|
||||
_`projectSlug`_, environment slug _`envSlug`_, and secrets path
|
||||
@@ -387,15 +365,15 @@ spec:
|
||||
|
||||
</Step>
|
||||
<Step title="Add your identity ID & service account to your InfisicalSecret resource">
|
||||
Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource.
|
||||
In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created.
|
||||
Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource.
|
||||
In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created.
|
||||
See the example below for more details.
|
||||
</Step>
|
||||
<Step title="Add your Kubernetes service account token to the InfisicalSecret resource">
|
||||
Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`.
|
||||
Here you will need to enter the name and namespace of the service account.
|
||||
Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`.
|
||||
Here you will need to enter the name and namespace of the service account.
|
||||
The example below shows a complete InfisicalSecret resource with all required fields defined.
|
||||
</Step>
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
@@ -561,6 +539,8 @@ spec:
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
|
||||
<Accordion title="authentication.gcpIamAuth">
|
||||
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
|
||||
|
||||
@@ -897,42 +877,6 @@ spec:
|
||||
|
||||
</Accordion>
|
||||
|
||||
### Connecting to instances with private/self-signed certificate
|
||||
|
||||
To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalSecret` CRD
|
||||
to point to a CA certificate stored in a Kubernetes secret resource.
|
||||
|
||||
```yaml
|
||||
---
|
||||
spec:
|
||||
hostAPI: https://app.infisical.com/api
|
||||
resyncInterval: 10
|
||||
tls:
|
||||
caRef:
|
||||
secretName: custom-ca-certificate
|
||||
secretNamespace: default
|
||||
key: ca.crt
|
||||
authentication:
|
||||
---
|
||||
```
|
||||
|
||||
The definition file of the Kubernetes secret for the CA certificate can be structured like the following:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: custom-ca-certificate
|
||||
type: Opaque
|
||||
stringData:
|
||||
ca.crt: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
...
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
## Auto redeployment
|
||||
|
||||
Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed.
|
||||
@@ -945,7 +889,6 @@ To enable auto redeployment you simply have to add the following annotation to t
|
||||
```yaml
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
```
|
||||
|
||||
<Accordion title="Deployment example with auto redeploy enabled">
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
|
@@ -135,11 +135,11 @@
|
||||
"pages": [
|
||||
"documentation/platform/access-controls/overview",
|
||||
"documentation/platform/access-controls/role-based-access-controls",
|
||||
"documentation/platform/access-controls/attribute-based-access-controls",
|
||||
"documentation/platform/access-controls/additional-privileges",
|
||||
"documentation/platform/access-controls/temporary-access",
|
||||
"documentation/platform/access-controls/access-requests",
|
||||
"documentation/platform/pr-workflows",
|
||||
"documentation/platform/audit-log-streams",
|
||||
"documentation/platform/groups"
|
||||
]
|
||||
},
|
||||
@@ -178,8 +178,7 @@
|
||||
"documentation/platform/dynamic-secrets/mongo-atlas",
|
||||
"documentation/platform/dynamic-secrets/mongo-db",
|
||||
"documentation/platform/dynamic-secrets/azure-entra-id",
|
||||
"documentation/platform/dynamic-secrets/ldap",
|
||||
"documentation/platform/dynamic-secrets/sap-hana"
|
||||
"documentation/platform/dynamic-secrets/ldap"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Docker Swarm"
|
||||
description: "How to self-host Infisical with Docker Swarm (HA)."
|
||||
description: "How to self Infisical with Docker Swarm (HA)."
|
||||
---
|
||||
|
||||
# Self-Hosting Infisical with Docker Swarm
|
||||
|
2
frontend/package-lock.json
generated
@@ -26,7 +26,6 @@
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
@@ -4932,7 +4931,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz",
|
||||
"integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
|
@@ -39,7 +39,6 @@
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
|
@@ -127,8 +127,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
cursor-pointer select-none items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-md py-2
|
||||
pl-10 pr-4 text-sm outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
|
||||
isSelected && "bg-primary",
|
||||
isDisabled &&
|
||||
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
||||
isDisabled && "cursor-not-allowed text-gray-600 opacity-80 hover:!bg-transparent",
|
||||
className
|
||||
)}
|
||||
ref={forwardedRef}
|
||||
|
@@ -26,8 +26,7 @@ export enum DynamicSecretProviders {
|
||||
MongoDB = "mongo-db",
|
||||
RabbitMq = "rabbit-mq",
|
||||
AzureEntraId = "azure-entra-id",
|
||||
Ldap = "ldap",
|
||||
SapHana = "sap-hana"
|
||||
Ldap = "ldap"
|
||||
}
|
||||
|
||||
export enum SqlProviders {
|
||||
@@ -190,7 +189,7 @@ export type TDynamicSecretProvider =
|
||||
applicationId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.Ldap;
|
||||
inputs: {
|
||||
@@ -202,20 +201,9 @@ export type TDynamicSecretProvider =
|
||||
revocationLdif: string;
|
||||
rollbackLdif?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.SapHana;
|
||||
inputs: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
};
|
||||
};
|
||||
;
|
||||
|
||||
export type TCreateDynamicSecretDTO = {
|
||||
projectSlug: string;
|
||||
provider: TDynamicSecretProvider;
|
||||
|
@@ -12,7 +12,7 @@ export type IdentityTrustedIp = {
|
||||
export type Identity = {
|
||||
id: string;
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
authMethods: IdentityAuthMethod[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
@@ -8,8 +8,4 @@ export {
|
||||
useUpdateSecretBatch,
|
||||
useUpdateSecretV3
|
||||
} from "./mutations";
|
||||
export {
|
||||
useGetProjectSecrets,
|
||||
useGetProjectSecretsAllEnv,
|
||||
useGetSecretReferenceTree,
|
||||
useGetSecretVersion} from "./queries";
|
||||
export { useGetProjectSecrets, useGetProjectSecretsAllEnv, useGetSecretVersion } from "./queries";
|
||||
|
@@ -17,17 +17,14 @@ import {
|
||||
SecretVersions,
|
||||
TGetProjectSecretsAllEnvDTO,
|
||||
TGetProjectSecretsDTO,
|
||||
TGetProjectSecretsKey,
|
||||
TGetSecretReferenceTreeDTO,
|
||||
TSecretReferenceTraceNode
|
||||
TGetProjectSecretsKey
|
||||
} from "./types";
|
||||
|
||||
export const secretKeys = {
|
||||
// this is also used in secretSnapshot part
|
||||
getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) =>
|
||||
[{ workspaceId, environment, secretPath }, "secrets"] as const,
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
|
||||
getSecretReferenceTree: (dto: TGetSecretReferenceTreeDTO) => ["secret-reference-tree", dto]
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const
|
||||
};
|
||||
|
||||
export const fetchProjectSecrets = async ({
|
||||
@@ -230,33 +227,3 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
||||
return data.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}, [])
|
||||
});
|
||||
|
||||
const fetchSecretReferenceTree = async ({
|
||||
secretPath,
|
||||
projectId,
|
||||
secretKey,
|
||||
environmentSlug
|
||||
}: TGetSecretReferenceTreeDTO) => {
|
||||
const { data } = await apiRequest.get<{ tree: TSecretReferenceTraceNode; value: string }>(
|
||||
`/api/v3/secrets/raw/${secretKey}/secret-reference-tree`,
|
||||
{
|
||||
params: {
|
||||
secretPath,
|
||||
workspaceId: projectId,
|
||||
environment: environmentSlug
|
||||
}
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetSecretReferenceTree = (dto: TGetSecretReferenceTreeDTO) =>
|
||||
useQuery({
|
||||
enabled:
|
||||
Boolean(dto.environmentSlug) &&
|
||||
Boolean(dto.secretPath) &&
|
||||
Boolean(dto.projectId) &&
|
||||
Boolean(dto.secretKey),
|
||||
queryKey: secretKeys.getSecretReferenceTree(dto),
|
||||
queryFn: () => fetchSecretReferenceTree(dto)
|
||||
});
|
||||
|
@@ -210,18 +210,3 @@ export type TMoveSecretsDTO = {
|
||||
secretIds: string[];
|
||||
shouldOverwrite: boolean;
|
||||
};
|
||||
|
||||
export type TGetSecretReferenceTreeDTO = {
|
||||
secretKey: string;
|
||||
secretPath: string;
|
||||
environmentSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TSecretReferenceTraceNode = {
|
||||
key: string;
|
||||
value?: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
children: TSecretReferenceTraceNode[];
|
||||
};
|
||||
|
@@ -5,7 +5,7 @@ import { useDebounce } from "@app/hooks/useDebounce";
|
||||
|
||||
export const usePagination = <T extends string>(initialOrderBy: T) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(100);
|
||||
const [perPage, setPerPage] = useState(20);
|
||||
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
|
||||
const [search, setSearch] = useState("");
|
||||
|
@@ -82,7 +82,6 @@ import { Workspace } from "@app/hooks/api/types";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner";
|
||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||
import { Mfa } from "@app/views/Login/Mfa";
|
||||
import { CreateOrgModal } from "@app/views/Org/components";
|
||||
@@ -362,7 +361,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
|
@@ -1,37 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton } from "@app/components/v2";
|
||||
|
||||
export const InsecureConnectionBanner = () => {
|
||||
const [isAcknowledged, setIsAcknowledged] = useState(
|
||||
localStorage.getItem("insecureConnectionAcknowledged") ?? false
|
||||
);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsAcknowledged(true);
|
||||
localStorage.setItem("insecureConnectionAcknowledged", "true");
|
||||
};
|
||||
|
||||
if (isAcknowledged) return null;
|
||||
|
||||
return (
|
||||
<div className="flex w-screen items-start border-b border-red-900 bg-red-700 py-1 px-2 font-inter text-sm text-mineshaft-200">
|
||||
<FontAwesomeIcon className="ml-3.5 mt-1" icon={faWarning} />
|
||||
<span className="mx-1 ml-2 mt-[0.04rem]">
|
||||
Your connection to this Infisical instance is not secured via HTTPS. Some features may not
|
||||
behave as expected.
|
||||
</span>
|
||||
<IconButton
|
||||
size="xs"
|
||||
className="ml-auto"
|
||||
colorSchema="danger"
|
||||
onClick={handleDismiss}
|
||||
ariaLabel="Dismiss banner"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1 +0,0 @@
|
||||
export * from "./InsecureConnectionBanner";
|
@@ -137,27 +137,6 @@ html {
|
||||
);
|
||||
}
|
||||
|
||||
.tree-line::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 1px;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
height: 50%;
|
||||
background-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.tree-line::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 50%;
|
||||
width: 12px;
|
||||
height: 1px;
|
||||
background-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.show-tags {
|
||||
transform: translateY(10px);
|
||||
transition: all 0.2s;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -19,12 +20,15 @@ import {
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import {
|
||||
IdentityAuthMethod,
|
||||
useDeleteIdentity,
|
||||
useGetIdentityById,
|
||||
useRevokeIdentityTokenAuthToken,
|
||||
useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api";
|
||||
useRevokeIdentityUniversalAuthClientSecret
|
||||
} from "@app/hooks/api";
|
||||
import { Identity } from "@app/hooks/api/identities/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { TabSections } from"@app/views/Org/Types";
|
||||
import { TabSections } from "@app/views/Org/Types";
|
||||
|
||||
import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
|
||||
import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
|
||||
@@ -49,6 +53,10 @@ export const IdentityPage = withPermission(
|
||||
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
|
||||
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
|
||||
|
||||
const [selectedAuthMethod, setSelectedAuthMethod] = useState<
|
||||
Identity["authMethods"][number] | null
|
||||
>(null);
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"identity",
|
||||
"deleteIdentity",
|
||||
@@ -124,7 +132,7 @@ export const IdentityPage = withPermission(
|
||||
|
||||
const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => {
|
||||
try {
|
||||
if (!data?.identity.id) return;
|
||||
if (!data?.identity.id || selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) return;
|
||||
|
||||
await revokeClientSecret({
|
||||
identityId: data?.identity.id,
|
||||
@@ -208,12 +216,12 @@ export const IdentityPage = withPermission(
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
authMethod: data.identity.authMethod
|
||||
allAuthMethods: data.identity.authMethods
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
{`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`}
|
||||
Add new auth method
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
@@ -247,6 +255,8 @@ export const IdentityPage = withPermission(
|
||||
<div className="mr-4 w-96">
|
||||
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||
<IdentityAuthenticationSection
|
||||
selectedAuthMethod={selectedAuthMethod}
|
||||
setSelectedAuthMethod={setSelectedAuthMethod}
|
||||
identityId={identityId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
|
@@ -1,15 +1,13 @@
|
||||
import { faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useEffect } from "react";
|
||||
import { faPencil, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
IconButton,
|
||||
// Button,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Button, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useGetIdentityById } from "@app/hooks/api";
|
||||
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
||||
import { Identity } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityClientSecrets } from "./IdentityClientSecrets";
|
||||
@@ -17,6 +15,8 @@ import { IdentityTokens } from "./IdentityTokens";
|
||||
|
||||
type Props = {
|
||||
identityId: string;
|
||||
setSelectedAuthMethod: (authMethod: Identity["authMethods"][number] | null) => void;
|
||||
selectedAuthMethod: Identity["authMethods"][number] | null;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
[
|
||||
@@ -33,16 +33,34 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
|
||||
export const IdentityAuthenticationSection = ({
|
||||
identityId,
|
||||
setSelectedAuthMethod,
|
||||
selectedAuthMethod,
|
||||
handlePopUpOpen
|
||||
}: Props) => {
|
||||
const { data } = useGetIdentityById(identityId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.identity) return;
|
||||
|
||||
if (data.identity.authMethods?.length) {
|
||||
setSelectedAuthMethod(data.identity.authMethods[0]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => setSelectedAuthMethod(null);
|
||||
}, [data?.identity]);
|
||||
|
||||
return data ? (
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3>
|
||||
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Tooltip content={`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`}>
|
||||
<Tooltip content="Add new auth method">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="copy icon"
|
||||
@@ -52,32 +70,85 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
authMethod: data.identity.authMethod
|
||||
allAuthMethods: data.identity.authMethods
|
||||
})
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Auth Method</p>
|
||||
{data.identity.authMethods.length > 0 ? (
|
||||
<>
|
||||
<div className="py-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="ml-px mb-0.5 text-sm font-semibold text-mineshaft-300">Auth Method</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-full">
|
||||
<Select
|
||||
className="w-full"
|
||||
value={selectedAuthMethod as string}
|
||||
onValueChange={(value) => setSelectedAuthMethod(value as IdentityAuthMethod)}
|
||||
>
|
||||
{(data.identity?.authMethods || []).map((authMethod) => (
|
||||
<SelectItem key={authMethod || authMethod} value={authMethod}>
|
||||
{identityAuthToNameMap[authMethod]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Tooltip content="Edit auth method">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
authMethod: selectedAuthMethod,
|
||||
allAuthMethods: data.identity.authMethods
|
||||
});
|
||||
}}
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
</Tooltip>{" "}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedAuthMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
|
||||
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||
)}
|
||||
{selectedAuthMethod === IdentityAuthMethod.TOKEN_AUTH && (
|
||||
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full space-y-2 pt-2">
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
No authentication methods configured. Get started by creating a new auth method.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
allAuthMethods: data.identity.authMethods
|
||||
});
|
||||
}}
|
||||
variant="outline_bg"
|
||||
className="w-full"
|
||||
size="xs"
|
||||
>
|
||||
Create Auth Method
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.identity.authMethod
|
||||
? identityAuthToNameMap[data.identity.authMethod]
|
||||
: "Not configured"}
|
||||
</p>
|
||||
</div>
|
||||
{data.identity.authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
|
||||
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||
)}
|
||||
{data.identity.authMethod === IdentityAuthMethod.TOKEN_AUTH && (
|
||||
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
@@ -1,38 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useDeleteIdentityAwsAuth,
|
||||
useDeleteIdentityAzureAuth,
|
||||
useDeleteIdentityGcpAuth,
|
||||
useDeleteIdentityKubernetesAuth,
|
||||
useDeleteIdentityOidcAuth,
|
||||
useDeleteIdentityTokenAuth,
|
||||
useDeleteIdentityUniversalAuth
|
||||
} from "@app/hooks/api";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
||||
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
|
||||
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
|
||||
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
|
||||
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
|
||||
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
|
||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||
import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
|
||||
@@ -43,229 +15,13 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
const identityAuthMethods = [
|
||||
{ label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH },
|
||||
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
|
||||
{ label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH },
|
||||
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
|
||||
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
|
||||
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
|
||||
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }
|
||||
];
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
authMethod: yup
|
||||
.mixed<IdentityAuthMethod>()
|
||||
.oneOf(Object.values(IdentityAuthMethod))
|
||||
.required("Auth method is required")
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
|
||||
export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
|
||||
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
|
||||
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
|
||||
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
|
||||
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
|
||||
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
|
||||
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
|
||||
const [selectedAuthMethod, setSelectedAuthMethod] = useState<IdentityAuthMethod | null>(null);
|
||||
|
||||
const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod;
|
||||
|
||||
const { control, watch, setValue, reset } = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
authMethod: initialAuthMethod
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// reset form on open
|
||||
if (popUp.identityAuthMethod.isOpen)
|
||||
reset({ authMethod: popUp?.identityAuthMethod?.data?.authMethod });
|
||||
}, [popUp.identityAuthMethod.isOpen]);
|
||||
|
||||
const identityAuthMethodData = {
|
||||
identityId: popUp?.identityAuthMethod.data?.identityId,
|
||||
name: popUp?.identityAuthMethod?.data?.name,
|
||||
authMethod: watch("authMethod")
|
||||
} as {
|
||||
identityId: string;
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (identityAuthMethodData?.authMethod) {
|
||||
setValue("authMethod", identityAuthMethodData.authMethod);
|
||||
return;
|
||||
}
|
||||
|
||||
setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH);
|
||||
}, [identityAuthMethodData?.authMethod]);
|
||||
|
||||
const onRevokeAuthMethodSubmit = async (authMethod: IdentityAuthMethod) => {
|
||||
if (!orgId || !authMethod) return;
|
||||
try {
|
||||
switch (authMethod) {
|
||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||
await revokeUniversalAuth({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IdentityAuthMethod.TOKEN_AUTH: {
|
||||
await revokeTokenAuth({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IdentityAuthMethod.KUBERNETES_AUTH: {
|
||||
await revokeKubernetesAuth({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IdentityAuthMethod.GCP_AUTH: {
|
||||
await revokeGcpAuth({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IdentityAuthMethod.AWS_AUTH: {
|
||||
await revokeAwsAuth({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IdentityAuthMethod.AZURE_AUTH: {
|
||||
await revokeAzureAuth({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IdentityAuthMethod.OIDC_AUTH: {
|
||||
await revokeOidcAuth({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: `Successfully removed ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpToggle("revokeAuthMethod", false);
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to remove ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
const renderIdentityAuthForm = () => {
|
||||
switch (identityAuthMethodData.authMethod) {
|
||||
case IdentityAuthMethod.AWS_AUTH: {
|
||||
return (
|
||||
<IdentityAwsAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.KUBERNETES_AUTH: {
|
||||
return (
|
||||
<IdentityKubernetesAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.GCP_AUTH: {
|
||||
return (
|
||||
<IdentityGcpAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.AZURE_AUTH: {
|
||||
return (
|
||||
<IdentityAzureAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||
return (
|
||||
<IdentityUniversalAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.OIDC_AUTH: {
|
||||
return (
|
||||
<IdentityOidcAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.TOKEN_AUTH: {
|
||||
return (
|
||||
<IdentityTokenAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
initialAuthMethod={initialAuthMethod!}
|
||||
revokeAuth={onRevokeAuthMethodSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
};
|
||||
const isSelectedAuthAlreadyConfigured =
|
||||
popUp?.identityAuthMethod?.data?.allAuthMethods?.includes(selectedAuthMethod);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -275,52 +31,23 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={`${
|
||||
identityAuthMethodData.authMethod === initialAuthMethod ? "Update" : "Configure"
|
||||
} Identity Auth Method for ${
|
||||
identityAuthToNameMap[identityAuthMethodData.authMethod!] ?? ""
|
||||
}`}
|
||||
title={
|
||||
isSelectedAuthAlreadyConfigured
|
||||
? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
||||
: `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="authMethod"
|
||||
defaultValue={IdentityAuthMethod.UNIVERSAL_AUTH}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Auth Method" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{identityAuthMethods.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{renderIdentityAuthForm()}
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp?.upgradePlan?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
||||
title={`Are you sure want to remove ${
|
||||
identityAuthMethodData?.authMethod
|
||||
? identityAuthToNameMap[identityAuthMethodData.authMethod]
|
||||
: "the auth method"
|
||||
} on ${identityAuthMethodData?.name ?? ""}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Remove"
|
||||
onDeleteApproved={() => onRevokeAuthMethodSubmit(identityAuthMethodData.authMethod!)}
|
||||
<IdentityAuthMethodModalContent
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identity={{
|
||||
name: popUp?.identityAuthMethod?.data?.name,
|
||||
authMethods: popUp?.identityAuthMethod?.data?.allAuthMethods,
|
||||
id: popUp?.identityAuthMethod.data?.identityId
|
||||
}}
|
||||
initialAuthMethod={initialAuthMethod}
|
||||
setSelectedAuthMethod={setSelectedAuthMethod}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
@@ -0,0 +1,317 @@
|
||||
import { useCallback } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useDeleteIdentityAwsAuth,
|
||||
useDeleteIdentityAzureAuth,
|
||||
useDeleteIdentityGcpAuth,
|
||||
useDeleteIdentityKubernetesAuth,
|
||||
useDeleteIdentityOidcAuth,
|
||||
useDeleteIdentityTokenAuth,
|
||||
useDeleteIdentityUniversalAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
||||
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
|
||||
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
|
||||
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
|
||||
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
|
||||
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
|
||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
|
||||
identity: {
|
||||
name: string;
|
||||
id: string;
|
||||
authMethods: IdentityAuthMethod[];
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
|
||||
};
|
||||
|
||||
type TRevokeOptions = {
|
||||
identityId: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
type TRevokeMethods = {
|
||||
revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
|
||||
render: () => JSX.Element;
|
||||
};
|
||||
|
||||
const identityAuthMethods = [
|
||||
{ label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH },
|
||||
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
|
||||
{ label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH },
|
||||
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
|
||||
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
|
||||
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
|
||||
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }
|
||||
];
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
authMethod: yup
|
||||
.mixed<IdentityAuthMethod>()
|
||||
.oneOf(Object.values(IdentityAuthMethod))
|
||||
.required("Auth method is required")
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
|
||||
export const IdentityAuthMethodModalContent = ({
|
||||
popUp,
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identity,
|
||||
initialAuthMethod,
|
||||
setSelectedAuthMethod
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
|
||||
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
|
||||
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
|
||||
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
|
||||
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
|
||||
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
|
||||
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
|
||||
|
||||
const { control, watch } = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: async () => {
|
||||
let authMethod = initialAuthMethod;
|
||||
|
||||
if (!authMethod) {
|
||||
const firstAuthMethodNotConfiguredAuthMethod = identityAuthMethods.find(
|
||||
({ value }) => !identity?.authMethods?.includes(value)
|
||||
);
|
||||
|
||||
if (firstAuthMethodNotConfiguredAuthMethod) {
|
||||
authMethod = firstAuthMethodNotConfiguredAuthMethod.value;
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedAuthMethod(authMethod);
|
||||
return {
|
||||
authMethod
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const watchedAuthMethod = watch("authMethod");
|
||||
|
||||
const identityAuthMethodData = {
|
||||
identityId: identity.id,
|
||||
name: identity.name,
|
||||
authMethod: watch("authMethod"),
|
||||
configuredAuthMethods: identity.authMethods
|
||||
} as {
|
||||
identityId: string;
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
configuredAuthMethods?: IdentityAuthMethod[];
|
||||
};
|
||||
|
||||
const isSelectedAuthAlreadyConfigured =
|
||||
identityAuthMethodData?.configuredAuthMethods?.includes(watchedAuthMethod);
|
||||
|
||||
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: {
|
||||
revokeMethod: revokeUniversalAuth,
|
||||
render: () => (
|
||||
<IdentityUniversalAuthForm
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.OIDC_AUTH]: {
|
||||
revokeMethod: revokeOidcAuth,
|
||||
render: () => (
|
||||
<IdentityOidcAuthForm
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: {
|
||||
revokeMethod: revokeTokenAuth,
|
||||
render: () => (
|
||||
<IdentityTokenAuthForm
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.AZURE_AUTH]: {
|
||||
revokeMethod: revokeAzureAuth,
|
||||
render: () => (
|
||||
<IdentityAzureAuthForm
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.GCP_AUTH]: {
|
||||
revokeMethod: revokeGcpAuth,
|
||||
render: () => (
|
||||
<IdentityGcpAuthForm
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: {
|
||||
revokeMethod: revokeKubernetesAuth,
|
||||
render: () => (
|
||||
<IdentityKubernetesAuthForm
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.AWS_AUTH]: {
|
||||
revokeMethod: revokeAwsAuth,
|
||||
render: () => (
|
||||
<IdentityAwsAuthForm
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
const isAlreadyConfigured = useCallback((method: IdentityAuthMethod) => {
|
||||
return identityAuthMethodData?.configuredAuthMethods?.includes(method);
|
||||
}, []);
|
||||
|
||||
const selectedMethodItem = methodMap[identityAuthMethodData.authMethod!];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="authMethod"
|
||||
defaultValue={IdentityAuthMethod.UNIVERSAL_AUTH}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Auth Method" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
isDisabled={isSelectedAuthAlreadyConfigured}
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (!isAlreadyConfigured(e as IdentityAuthMethod)) {
|
||||
setSelectedAuthMethod(e as IdentityAuthMethod);
|
||||
onChange(e);
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{identityAuthMethods.map(({ label, value }) => {
|
||||
const alreadyConfigured = isAlreadyConfigured(value);
|
||||
return (
|
||||
<Tooltip
|
||||
key={`auth-method-${value}`}
|
||||
content="Authentication method already configured"
|
||||
isDisabled={!alreadyConfigured}
|
||||
>
|
||||
<SelectItem
|
||||
isDisabled={alreadyConfigured}
|
||||
value={String(value || "")}
|
||||
key={label}
|
||||
>
|
||||
{label}{" "}
|
||||
{alreadyConfigured && !isSelectedAuthAlreadyConfigured && (
|
||||
<Badge>Configured</Badge>
|
||||
)}
|
||||
</SelectItem>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedMethodItem?.render ? selectedMethodItem.render() : <div />}
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp?.upgradePlan?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
||||
title={`Are you sure want to remove ${
|
||||
identityAuthMethodData?.authMethod
|
||||
? identityAuthToNameMap[identityAuthMethodData.authMethod]
|
||||
: "the auth method"
|
||||
} on ${identityAuthMethodData?.name ?? ""}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Remove"
|
||||
onDeleteApproved={async () => {
|
||||
if (!identityAuthMethodData.authMethod || !orgId || !selectedMethodItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await selectedMethodItem.revokeMethod({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully removed auth method",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpToggle("revokeAuthMethod", false);
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to remove auth method",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityAwsAuth,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
@@ -62,18 +62,15 @@ type Props = {
|
||||
identityAuthMethodData: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
configuredAuthMethods?: IdentityAuthMethod[];
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityAwsAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -82,13 +79,13 @@ export const IdentityAwsAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
|
||||
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||
identityAuthMethodData.authMethod! || ""
|
||||
);
|
||||
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
enabled: isUpdate
|
||||
});
|
||||
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -184,230 +181,204 @@ export const IdentityAwsAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedPrincipalArns"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Principal ARNs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedAccountIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Account IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="123456789012, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="https://sts.amazonaws.com/"
|
||||
name="stsEndpoint"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedPrincipalArns"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Principal ARNs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
||||
type="text"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedAccountIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Account IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="123456789012, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="https://sts.amazonaws.com/"
|
||||
name="stsEndpoint"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
Add IP Address
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{!isUpdate ? "Create" : "Edit"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{isUpdate && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@@ -6,7 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityAzureAuth,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -50,18 +50,15 @@ type Props = {
|
||||
identityAuthMethodData: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
configuredAuthMethods?: IdentityAuthMethod[];
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityAzureAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -70,18 +67,18 @@ export const IdentityAzureAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
|
||||
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||
identityAuthMethodData.authMethod! || ""
|
||||
);
|
||||
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
enabled: isUpdate
|
||||
});
|
||||
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
trigger,
|
||||
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -173,239 +170,204 @@ export const IdentityAzureAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="tenantId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tenant ID"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="resource"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Resource / Audience"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="https://management.azure.com/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedServicePrincipalIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Principal IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="tenantId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tenant ID"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="resource"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Resource / Audience"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="https://management.azure.com/" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedServicePrincipalIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Principal IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
Add IP Address
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{!isUpdate ? "Create" : "Edit"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
const result = await trigger();
|
||||
if (result) {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Please fill in all required fields",
|
||||
type: "error"
|
||||
});
|
||||
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{isUpdate && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@@ -6,15 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityGcpAuth,
|
||||
@@ -23,7 +15,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -59,18 +51,15 @@ type Props = {
|
||||
identityAuthMethodData: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
configuredAuthMethods?: IdentityAuthMethod[];
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityGcpAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData,
|
||||
revokeAuth,
|
||||
initialAuthMethod
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -79,11 +68,12 @@ export const IdentityGcpAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
|
||||
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||
identityAuthMethodData.authMethod! || ""
|
||||
);
|
||||
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
enabled: isUpdate
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -189,258 +179,228 @@ export const IdentityGcpAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="gce" key="gce">
|
||||
GCP ID Token Auth (Recommended)
|
||||
</SelectItem>
|
||||
<SelectItem value="iam" key="iam">
|
||||
GCP IAM Auth
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedServiceAccounts"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Account Emails"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedProjects"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Projects"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="my-gcp-project, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<SelectItem value="gce" key="gce">
|
||||
GCP ID Token Auth (Recommended)
|
||||
</SelectItem>
|
||||
<SelectItem value="iam" key="iam">
|
||||
GCP IAM Auth
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedZones"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Zones"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedServiceAccounts"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Account Emails"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
|
||||
type="text"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedProjects"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Projects"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
<Input {...field} placeholder="my-gcp-project, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedZones"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
Add IP Address
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{!isUpdate ? "Create" : "Edit"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
buttonText="Overwrite"
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{isUpdate && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@@ -6,14 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityKubernetesAuth,
|
||||
@@ -22,7 +15,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -60,18 +53,15 @@ type Props = {
|
||||
identityAuthMethodData: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
configuredAuthMethods?: IdentityAuthMethod[];
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityKubernetesAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -80,17 +70,18 @@ export const IdentityKubernetesAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
|
||||
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||
identityAuthMethodData.authMethod! || ""
|
||||
);
|
||||
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
enabled: isUpdate
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
trigger,
|
||||
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -200,291 +191,256 @@ export const IdentityKubernetesAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="kubernetesHost"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Kubernetes Host / Base Kubernetes API URL "
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'"
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tokenReviewerJwt"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Token Reviewer JWT"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Account Names"
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedNamespaces"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Namespaces"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
|
||||
>
|
||||
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedAudience"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Audience"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
|
||||
>
|
||||
<Input {...field} placeholder="" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="kubernetesHost"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Kubernetes Host / Base Kubernetes API URL "
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'"
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tokenReviewerJwt"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Token Reviewer JWT"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Account Names"
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedNamespaces"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Namespaces"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
|
||||
>
|
||||
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedAudience"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Audience"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
|
||||
>
|
||||
<Input {...field} placeholder="" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
Add IP Address
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Create"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
const result = await trigger();
|
||||
if (result) {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Please fill in all required fields",
|
||||
type: "error"
|
||||
});
|
||||
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{isUpdate && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@@ -154,12 +154,6 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
handlePopUpToggle("identity", false);
|
||||
router.push(`/org/${orgId}/identities/${createdId}`);
|
||||
|
||||
// handlePopUpOpen("identityAuthMethod", {
|
||||
// identityId: createdId,
|
||||
// name: createdName,
|
||||
// authMethod
|
||||
// });
|
||||
}
|
||||
|
||||
createNotification({
|
||||
|
@@ -7,21 +7,13 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z.object({
|
||||
accessTokenTrustedIps: z
|
||||
@@ -62,18 +54,15 @@ type Props = {
|
||||
identityAuthMethodData: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
configuredAuthMethods?: IdentityAuthMethod[];
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityOidcAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -82,17 +71,17 @@ export const IdentityOidcAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
|
||||
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||
identityAuthMethodData.authMethod! || ""
|
||||
);
|
||||
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
enabled: isUpdate
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
trigger,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -210,364 +199,329 @@ export const IdentityOidcAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="oidcDiscoveryUrl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="OIDC Discovery URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://token.actions.githubusercontent.com"
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundIssuer"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Issuer"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder="https://token.actions.githubusercontent.com"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundSubject"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input {...field} type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundAudiences"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Audiences"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input {...field} type="text" placeholder="service1, service2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{boundClaimsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`boundClaims.${index}.key`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Claims" : undefined}
|
||||
icon={
|
||||
index === 0 ? (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="property"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`boundClaims.${index}.value`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="value1, value2"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onClick={() => removeBoundClaimField(index)}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
appendBoundClaimField({
|
||||
key: "",
|
||||
value: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="oidcDiscoveryUrl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="OIDC Discovery URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
Add Claims
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="https://token.actions.githubusercontent.com"
|
||||
type="text"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundIssuer"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
label="Issuer"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder="https://token.actions.githubusercontent.com"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCert"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundSubject"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input {...field} type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="boundAudiences"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Audiences"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input {...field} type="text" placeholder="service1, service2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{boundClaimsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`boundClaims.${index}.key`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Claims" : undefined}
|
||||
icon={
|
||||
index === 0 ? (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={<span>This field supports glob patterns</span>}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="property"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`boundClaims.${index}.value`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="value1, value2"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
<IconButton
|
||||
onClick={() => removeBoundClaimField(index)}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
appendBoundClaimField({
|
||||
key: "",
|
||||
value: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Claims
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
Add IP Address
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Create"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
const result = await trigger();
|
||||
if (result) {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Please fill in all required fields",
|
||||
type: "error"
|
||||
});
|
||||
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{isUpdate && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityTokenAuth,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
useUpdateIdentityTokenAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -42,21 +42,18 @@ type Props = {
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
identityAuthMethodData: {
|
||||
identityAuthMethodData?: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
configuredAuthMethods?: IdentityAuthMethod[];
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityTokenAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -65,11 +62,13 @@ export const IdentityTokenAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
|
||||
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||
identityAuthMethodData.authMethod! || ""
|
||||
);
|
||||
|
||||
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
enabled: isUpdate
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -124,189 +123,163 @@ export const IdentityTokenAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: `Failed to ${isCurrentAuthMethod ? "update" : "configure"} identity`,
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
Add IP Address
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Create"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{isUpdate && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityUniversalAuth,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
@@ -65,21 +65,18 @@ type Props = {
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
identityAuthMethodData: {
|
||||
identityAuthMethodData?: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
configuredAuthMethods?: IdentityAuthMethod[];
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
initialAuthMethod: IdentityAuthMethod;
|
||||
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IdentityUniversalAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData,
|
||||
initialAuthMethod,
|
||||
revokeAuth
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -87,11 +84,13 @@ export const IdentityUniversalAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
|
||||
|
||||
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
|
||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||
identityAuthMethodData.authMethod! || ""
|
||||
);
|
||||
|
||||
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
|
||||
enabled: isCurrentAuthMethod
|
||||
enabled: isUpdate
|
||||
});
|
||||
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -190,7 +189,7 @@ export const IdentityUniversalAuthForm = ({
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
|
||||
text: `Successfully ${isUpdate ? "updated" : "created"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -199,8 +198,7 @@ export const IdentityUniversalAuthForm = ({
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text =
|
||||
error?.response?.data?.message ??
|
||||
`Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`;
|
||||
error?.response?.data?.message ?? `Failed to ${isUpdate ? "update" : "configure"} identity`;
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
@@ -210,243 +208,216 @@ export const IdentityUniversalAuthForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeClientSecretTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendClientSecretTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
|
||||
>
|
||||
Overwrite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isCurrentAuthMethod && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Remove Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<DeleteActionModal
|
||||
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
|
||||
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
|
||||
identityAuthMethodData?.name ?? ""
|
||||
}?`}
|
||||
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Overwrite"
|
||||
onDeleteApproved={async () => {
|
||||
await revokeAuth(initialAuthMethod);
|
||||
handleSubmit(onFormSubmit)();
|
||||
}}
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeClientSecretTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendClientSecretTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Edit" : "Create"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{isUpdate && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorSchema="danger"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||
>
|
||||
Delete Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
@@ -31,12 +30,7 @@ export const OrgInviteLink = ({ invite }: Props) => {
|
||||
Invite for <span className="font-medium">{invite.email}</span>
|
||||
</p>
|
||||
<div className="flex flex-col gap-1 rounded-md bg-white/[0.04] p-2 text-base text-gray-400">
|
||||
<p
|
||||
className={twMerge(
|
||||
"line-clamp-1 mr-4",
|
||||
window.isSecureContext ? "overflow-hidden text-ellipsis whitespace-nowrap" : "break-all"
|
||||
)}
|
||||
>
|
||||
<p className="line-clamp-1 mr-4 overflow-hidden text-ellipsis whitespace-nowrap ">
|
||||
{invite.link}
|
||||
</p>
|
||||
<Tooltip content={`Copy invitation link for ${invite.email}`}>
|
||||
|
@@ -1,14 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { DiRedis } from "react-icons/di";
|
||||
import {
|
||||
SiApachecassandra,
|
||||
SiElasticsearch,
|
||||
SiFiles,
|
||||
SiMicrosoftazure,
|
||||
SiMongodb,
|
||||
SiRabbitmq,
|
||||
SiSap
|
||||
} from "react-icons/si";
|
||||
import { SiApachecassandra, SiElasticsearch, SiFiles, SiMicrosoftazure, SiMongodb, SiRabbitmq } from "react-icons/si";
|
||||
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -27,7 +19,6 @@ import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
|
||||
import { MongoDBDatabaseInputForm } from "./MongoDBInputForm";
|
||||
import { RabbitMqInputForm } from "./RabbitMqInputForm";
|
||||
import { RedisInputForm } from "./RedisInputForm";
|
||||
import { SapHanaInputForm } from "./SapHanaInputForm";
|
||||
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
||||
|
||||
type Props = {
|
||||
@@ -92,17 +83,12 @@ const DYNAMIC_SECRET_LIST = [
|
||||
{
|
||||
icon: <SiMicrosoftazure size="1.5rem" />,
|
||||
provider: DynamicSecretProviders.AzureEntraId,
|
||||
title: "Azure Entra ID"
|
||||
title: "Azure Entra ID",
|
||||
},
|
||||
{
|
||||
icon: <SiFiles size="1.5rem" />,
|
||||
provider: DynamicSecretProviders.Ldap,
|
||||
title: "LDAP"
|
||||
},
|
||||
{
|
||||
icon: <SiSap size="1.5rem" />,
|
||||
provider: DynamicSecretProviders.SapHana,
|
||||
title: "SAP HANA"
|
||||
title: "LDAP",
|
||||
}
|
||||
];
|
||||
|
||||
@@ -343,7 +329,8 @@ export const CreateDynamicSecretForm = ({
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.Ldap && (
|
||||
<motion.div
|
||||
@@ -361,25 +348,9 @@ export const CreateDynamicSecretForm = ({
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.SapHana && (
|
||||
<motion.div
|
||||
key="dynamic-sap-hana-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<SapHanaInputForm
|
||||
onCompleted={handleFormReset}
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
@@ -1,329 +0,0 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onCompleted: () => void;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export const SapHanaInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: {
|
||||
creationStatement: `CREATE USER {{username}} PASSWORD {{password}} NO FORCE_FIRST_PASSWORD_CHANGE VALID UNTIL '{{expiration}}';
|
||||
GRANT "MONITORING" TO {{username}};`,
|
||||
revocationStatement: `REVOKE "MONITORING" FROM {{username}};
|
||||
DROP USER {{username}};`,
|
||||
renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.SapHana, inputs: provider },
|
||||
maxTTL,
|
||||
name,
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.port"
|
||||
defaultValue={443}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.username"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA(SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.revocationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.renewStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -221,24 +221,6 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === DynamicSecretProviders.SapHana) {
|
||||
const { DB_USERNAME, DB_PASSWORD } = data as {
|
||||
DB_USERNAME: string;
|
||||
DB_PASSWORD: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<OutputDisplay label="Username" value={DB_USERNAME} />
|
||||
<OutputDisplay
|
||||
label="Password"
|
||||
value={DB_PASSWORD}
|
||||
helperText="Important: Copy these credentials now. You will not be able to see them again after you close the modal."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
@@ -14,7 +14,6 @@ import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasFo
|
||||
import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm";
|
||||
import { EditDynamicSecretRabbitMqForm } from "./EditDynamicSecretRabbitMqForm";
|
||||
import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm";
|
||||
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||
|
||||
type Props = {
|
||||
@@ -231,33 +230,16 @@ export const EditDynamicSecretForm = ({
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretLdapForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{dynamicSecretDetails?.type === DynamicSecretProviders.SapHana && (
|
||||
<motion.div
|
||||
key="sap-hana-edit"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretSapHanaForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
>
|
||||
<EditDynamicSecretLdapForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
@@ -1,337 +0,0 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
})
|
||||
.partial(),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export const EditDynamicSecretSapHanaForm = ({
|
||||
onClose,
|
||||
dynamicSecret,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
data: {
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated dynamic secret"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="newName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.port"
|
||||
defaultValue={443}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.username"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA(SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.revocationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.renewStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -393,7 +393,7 @@ export const SecretDetailSidebar = ({
|
||||
) : (
|
||||
<div className="mt-2 ml-1 flex items-center space-x-2">
|
||||
<Button
|
||||
className="w-full px-2 py-1"
|
||||
className="px-2 py-1"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||
onClick={() => setCreateReminderFormOpen.on()}
|
||||
@@ -448,9 +448,9 @@ export const SecretDetailSidebar = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-1 flex items-center space-x-4">
|
||||
<div className="ml-1 flex items-center space-x-2">
|
||||
<Button
|
||||
className="w-full px-2 py-1"
|
||||
className="px-2 py-1"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
||||
onClick={() => {
|
@@ -12,9 +12,6 @@ import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
@@ -39,8 +36,8 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { memo, useEffect } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { hasSecretReference, SecretReferenceTree } from "../SecretReferenceDetails";
|
||||
|
||||
import { CreateReminderForm } from "./CreateReminderForm";
|
||||
import {
|
||||
FontAwesomeSpriteName,
|
||||
formSchema,
|
||||
@@ -103,6 +100,9 @@ export const SecretItem = memo(
|
||||
|
||||
const secretName = watch("key");
|
||||
|
||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||
const secretReminderNote = watch("reminderNote");
|
||||
|
||||
const overrideAction = watch("overrideAction");
|
||||
const hasComment = Boolean(watch("comment"));
|
||||
|
||||
@@ -139,6 +139,7 @@ export const SecretItem = memo(
|
||||
);
|
||||
|
||||
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isSecValueCopied) {
|
||||
@@ -201,356 +202,208 @@ export const SecretItem = memo(
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div
|
||||
className={twMerge(
|
||||
"border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700",
|
||||
isDirty && "border-primary-400/50"
|
||||
)}
|
||||
>
|
||||
<div className="group flex">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-11 w-11 items-center justify-center px-4 py-3",
|
||||
isDirty && "text-primary"
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`checkbox-${secret.id}`}
|
||||
isChecked={isSelected}
|
||||
onCheckedChange={() => onToggleSecretSelect(secret)}
|
||||
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
|
||||
/>
|
||||
<FontAwesomeSymbol
|
||||
<>
|
||||
<CreateReminderForm
|
||||
repeatDays={secretReminderRepeatDays}
|
||||
note={secretReminderNote}
|
||||
isOpen={createReminderFormOpen}
|
||||
onOpenChange={(_, data) => {
|
||||
setCreateReminderFormOpen.toggle();
|
||||
|
||||
if (data) {
|
||||
setValue("reminderRepeatDays", data.days, { shouldDirty: true });
|
||||
setValue("reminderNote", data.note, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div
|
||||
className={twMerge(
|
||||
"border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700",
|
||||
isDirty && "border-primary-400/50"
|
||||
)}
|
||||
>
|
||||
<div className="group flex">
|
||||
<div
|
||||
className={twMerge(
|
||||
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
|
||||
isSelected && "hidden"
|
||||
"flex h-11 w-11 items-center justify-center px-4 py-3",
|
||||
isDirty && "text-primary"
|
||||
)}
|
||||
symbolName={FontAwesomeSpriteName.SecretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
||||
<Controller
|
||||
name="key"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<Input
|
||||
autoComplete="off"
|
||||
isReadOnly={isReadOnly}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
variant="plain"
|
||||
isDisabled={isOverriden}
|
||||
placeholder={error?.message}
|
||||
isError={Boolean(error)}
|
||||
onKeyUp={() => trigger("key")}
|
||||
{...field}
|
||||
className="w-full px-0 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
|
||||
>
|
||||
<Checkbox
|
||||
id={`checkbox-${secret.id}`}
|
||||
isChecked={isSelected}
|
||||
onCheckedChange={() => onToggleSecretSelect(secret)}
|
||||
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
|
||||
/>
|
||||
<FontAwesomeSymbol
|
||||
className={twMerge(
|
||||
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
|
||||
isSelected && "hidden"
|
||||
)}
|
||||
symbolName={FontAwesomeSpriteName.SecretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
||||
<Controller
|
||||
name="key"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<Input
|
||||
autoComplete="off"
|
||||
isReadOnly={isReadOnly}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
variant="plain"
|
||||
isDisabled={isOverriden}
|
||||
placeholder={error?.message}
|
||||
isError={Boolean(error)}
|
||||
onKeyUp={() => trigger("key")}
|
||||
{...field}
|
||||
className="w-full px-0 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
{isOverriden ? (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
key="value-overriden"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SecretInput
|
||||
key="value-overriden"
|
||||
isVisible={isVisible}
|
||||
isReadOnly={isReadOnly}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
name="value"
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
key="secret-value"
|
||||
isVisible={isVisible}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
{isOverriden ? (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
key="value-overriden"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SecretInput
|
||||
key="value-overriden"
|
||||
isVisible={isVisible}
|
||||
isReadOnly={isReadOnly}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
name="value"
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
key="secret-value"
|
||||
isVisible={isVisible}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3"
|
||||
symbolName={
|
||||
isSecValueCopied
|
||||
? FontAwesomeSpriteName.Check
|
||||
: FontAwesomeSpriteName.ClipboardCopy
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<IconButton
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="reference-tree"
|
||||
isDisabled={!isAllowed || !hasSecretReference(secret?.value)}
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(secret?.value)
|
||||
? "Secret Reference Tree"
|
||||
: "Secret does not contain references"
|
||||
}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.SecretReferenceTree}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="Secret Reference Details"
|
||||
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
|
||||
>
|
||||
<SecretReferenceTree
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
secretKey={secret?.key}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
ariaLabel="tags"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-5",
|
||||
hasTagsApplied && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Tags">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isTagSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={`${secret.id}-${tagId}`}
|
||||
icon={
|
||||
isTagSelected && (
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.CheckedCircle}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuItem className="px-1.5" asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateTag}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Override"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
ariaLabel="override-value"
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={handleOverrideClick}
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5",
|
||||
isOverriden && "w-5 text-primary"
|
||||
)}
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Override}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<Popover>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
||||
hasComment && "w-5 text-primary"
|
||||
)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-comment"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Comment">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Comment}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="share-secret"
|
||||
onClick={handleSecretShare}
|
||||
>
|
||||
<Tooltip content="Share Secret">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.ShareSecret}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<PopoverContent
|
||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
|
||||
sticky="always"
|
||||
>
|
||||
<FormControl label="Comment" className="mb-0">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
rows={8}
|
||||
cols={30}
|
||||
{...register("comment")}
|
||||
/>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{!isDirty ? (
|
||||
<motion.div
|
||||
key="options"
|
||||
className="flex h-10 flex-shrink-0 items-center space-x-4 px-[0.64rem]"
|
||||
initial={{ x: 0, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content="More">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.More}
|
||||
className="h-5 w-4"
|
||||
className="h-3.5 w-3"
|
||||
symbolName={
|
||||
isSecValueCopied
|
||||
? FontAwesomeSpriteName.Check
|
||||
: FontAwesomeSpriteName.ClipboardCopy
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
ariaLabel="tags"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-5",
|
||||
hasTagsApplied && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Tags">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isTagSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={`${secret.id}-${tagId}`}
|
||||
icon={
|
||||
isTagSelected && (
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.CheckedCircle}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuItem className="px-1.5" asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateTag}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -558,91 +411,237 @@ export const SecretItem = memo(
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
allowedLabel="Override"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-value"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
ariaLabel="override-value"
|
||||
isDisabled={!isAllowed}
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={handleOverrideClick}
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5",
|
||||
isOverriden && "w-5 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Close}
|
||||
className="h-5 w-4"
|
||||
symbolName={FontAwesomeSpriteName.Override}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="options-save"
|
||||
className="flex h-10 flex-shrink-0 items-center space-x-4 px-3"
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
Object.keys(errors || {}).length
|
||||
? Object.entries(errors)
|
||||
.map(([key, { message }]) => `Field ${key}: ${message}`)
|
||||
.join("\n")
|
||||
: "Save"
|
||||
}
|
||||
>
|
||||
|
||||
{!isOverriden && (
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
type="submit"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"p-0 text-primary opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
||||
Boolean(secretReminderRepeatDays) && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={isSubmitting || Boolean(errors.key)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-reminder"
|
||||
onClick={() => setCreateReminderFormOpen.on()}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Spinner className="m-0 h-4 w-4 p-0" />
|
||||
) : (
|
||||
<Tooltip
|
||||
content={
|
||||
secretReminderRepeatDays && secretReminderRepeatDays > 0
|
||||
? `Every ${secretReminderRepeatDays} day${
|
||||
Number(secretReminderRepeatDays) > 1 ? "s" : ""
|
||||
}
|
||||
`
|
||||
: "Reminder"
|
||||
}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Check}
|
||||
className={twMerge(
|
||||
"h-4 w-4 text-primary",
|
||||
Boolean(Object.keys(errors || {}).length) && "text-red"
|
||||
)}
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Clock}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Cancel">
|
||||
)}
|
||||
|
||||
<Popover>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
||||
hasComment && "w-5 text-primary"
|
||||
)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-comment"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Comment">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Comment}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"p-0 opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
onClick={() => reset()}
|
||||
isDisabled={isSubmitting}
|
||||
ariaLabel="share-secret"
|
||||
onClick={handleSecretShare}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Close}
|
||||
className="h-4 w-4 text-primary"
|
||||
/>
|
||||
<Tooltip content="Share Secret">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.ShareSecret}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<PopoverContent
|
||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
|
||||
sticky="always"
|
||||
>
|
||||
<FormControl label="Comment" className="mb-0">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
rows={8}
|
||||
cols={30}
|
||||
{...register("comment")}
|
||||
/>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{!isDirty ? (
|
||||
<motion.div
|
||||
key="options"
|
||||
className="flex h-10 flex-shrink-0 items-center space-x-4 px-[0.64rem]"
|
||||
initial={{ x: 0, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content="More">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.More}
|
||||
className="h-5 w-4"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-value"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Close}
|
||||
className="h-5 w-4"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="options-save"
|
||||
className="flex h-10 flex-shrink-0 items-center space-x-4 px-3"
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
Object.keys(errors || {}).length
|
||||
? Object.entries(errors)
|
||||
.map(([key, { message }]) => `Field ${key}: ${message}`)
|
||||
.join("\n")
|
||||
: "Save"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
type="submit"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"p-0 text-primary opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
isDisabled={isSubmitting || Boolean(errors.key)}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Spinner className="m-0 h-4 w-4 p-0" />
|
||||
) : (
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Check}
|
||||
className={twMerge(
|
||||
"h-4 w-4 text-primary",
|
||||
Boolean(Object.keys(errors || {}).length) && "text-red"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Cancel">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"p-0 opacity-0 group-hover:opacity-100",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
onClick={() => reset()}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Close}
|
||||
className="h-4 w-4 text-primary"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@@ -16,7 +16,7 @@ import { WsTag } from "@app/hooks/api/types";
|
||||
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
|
||||
|
||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||
import { SecretDetailSidebar } from "./SecretDetailSidebar";
|
||||
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
||||
import { SecretItem } from "./SecretItem";
|
||||
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
||||
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
faEllipsis,
|
||||
faKey,
|
||||
faLock,
|
||||
faProjectDiagram,
|
||||
faShare,
|
||||
faTags
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -74,8 +73,7 @@ export enum FontAwesomeSpriteName {
|
||||
CheckedCircle = "check-circle",
|
||||
ReplicatedSecretKey = "secret-replicated",
|
||||
ShareSecret = "share-secret",
|
||||
KeyLock = "key-lock",
|
||||
SecretReferenceTree = "secret-reference-tree"
|
||||
KeyLock = "key-lock"
|
||||
}
|
||||
|
||||
// this is an optimization technique
|
||||
@@ -93,6 +91,5 @@ export const FontAwesomeSpriteSymbols = [
|
||||
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle },
|
||||
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
|
||||
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
|
||||
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock },
|
||||
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree }
|
||||
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock }
|
||||
];
|
||||
|
@@ -1,127 +0,0 @@
|
||||
/* credits: https://iamkate.com/code/tree-views/ */
|
||||
.tree {
|
||||
--spacing: 1.5rem;
|
||||
--radius: 4px;
|
||||
}
|
||||
|
||||
.tree li {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
|
||||
}
|
||||
|
||||
.tree ul {
|
||||
margin-left: calc(var(--radius) - var(--spacing));
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.tree ul li {
|
||||
border-left: 2px solid #888;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.tree ul li:last-child {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.tree ul li::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(var(--spacing) / -1);
|
||||
left: -2px;
|
||||
width: calc(var(--spacing) + 2px);
|
||||
height: calc(var(--spacing) + 13px);
|
||||
border: solid #888;
|
||||
border-radius: 0 0 0 8px;
|
||||
border-width: 0 0 2px 2px;
|
||||
transition: all 200ms linear;
|
||||
}
|
||||
|
||||
.details[open] summary ~ * {
|
||||
animation: sweep .5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes sweep {
|
||||
0% {opacity: 0; margin-left: -10px}
|
||||
100% {opacity: 1; margin-left: 0px}
|
||||
}
|
||||
|
||||
.tree summary {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.tree summary::marker,
|
||||
.tree summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree summary:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tree summary:focus-visible {
|
||||
outline: 1px dotted #000;
|
||||
}
|
||||
|
||||
.tree li::after,
|
||||
.tree summary::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(var(--spacing) / 2 - var(--radius));
|
||||
left: calc(var(--spacing) - var(--radius) - 1px);
|
||||
width: calc(2 * var(--radius));
|
||||
height: calc(2 * var(--radius));
|
||||
border-radius: 50%;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.tree summary::before {
|
||||
z-index: 1;
|
||||
background: #ddd 0 0;
|
||||
}
|
||||
|
||||
.tree details[open] > summary::before {
|
||||
background-position: calc(-2 * var(--radius)) 0;
|
||||
}
|
||||
|
||||
.collapsibleContent {
|
||||
/*overflow-y: hidden;*/
|
||||
}
|
||||
.collapsibleContent[data-state="open"] {
|
||||
animation: slideDown 300ms ease-out;
|
||||
}
|
||||
.collapsibleContent[data-state="closed"] {
|
||||
animation: slideUp 300ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
0% {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
height: var(--radix-collapsible-content-height);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
height: var(--radix-collapsible-content-height);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
@@ -1,128 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetSecretReferenceTree } from "@app/hooks/api";
|
||||
import { TSecretReferenceTraceNode } from "@app/hooks/api/types";
|
||||
|
||||
import style from "./SecretReferenceDetails.module.css";
|
||||
|
||||
type Props = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretKey: string;
|
||||
};
|
||||
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/;
|
||||
export const hasSecretReference = (value: string | undefined) =>
|
||||
value ? INTERPOLATION_SYNTAX_REG.test(value) : false;
|
||||
|
||||
export const SecretReferenceNode = ({
|
||||
node,
|
||||
isRoot,
|
||||
secretKey
|
||||
}: {
|
||||
node: TSecretReferenceTraceNode;
|
||||
isRoot?: boolean;
|
||||
secretKey?: string;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasChildren = node.children.length > 0;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Collapsible.Root open={isOpen} className="" onOpenChange={setIsOpen}>
|
||||
<Collapsible.Trigger
|
||||
className={twMerge(
|
||||
hasChildren && " decoration-bunker-4ø00 underline-offset-4 data-[state=open]:underline",
|
||||
"[&>svg]:data-[state=open]:rotate-[90deg] [&>svg]:data-[state=open]:text-yellow-500"
|
||||
)}
|
||||
disabled={!hasChildren}
|
||||
>
|
||||
{hasChildren && (
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className=" d mr-2 text-mineshaft-400 transition-transform duration-300 ease-linear"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
{isRoot
|
||||
? secretKey
|
||||
: `${node.environment}${
|
||||
node.secretPath === "/" ? "" : node.secretPath.split("/").join(".")
|
||||
}.${node.key}`}
|
||||
<Tooltip className="max-w-md break-words" content={node.value}>
|
||||
<span
|
||||
className={twMerge(
|
||||
"ml-1 px-1 text-xs text-mineshaft-400",
|
||||
!node.value && "text-red-400"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={node.value ? faEye : faEyeSlash} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className={twMerge("mt-4", style.collapsibleContent)}>
|
||||
{hasChildren && (
|
||||
<ul>
|
||||
{node.children.map((el, index) => (
|
||||
<SecretReferenceNode node={el} key={`${el.key}-${index + 1}`} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
|
||||
const { data, isLoading } = useGetSecretReferenceTree({
|
||||
secretPath,
|
||||
environmentSlug: environment,
|
||||
projectId,
|
||||
secretKey
|
||||
});
|
||||
|
||||
const tree = data?.tree;
|
||||
const secretValue = data?.value;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spinner size="xs" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormControl label="Expanded value">
|
||||
<SecretInput
|
||||
key="value-overriden"
|
||||
isReadOnly
|
||||
value={secretValue}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-700 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mb-2" label="Reference Tree" />
|
||||
<div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200">
|
||||
{tree && (
|
||||
<ul className={style.tree}>
|
||||
<SecretReferenceNode node={tree} isRoot secretKey={secretKey} />
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-mineshaft-400">
|
||||
Click a secret key to view its sub-references.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1 +0,0 @@
|
||||
export { hasSecretReference,SecretReferenceTree } from "./SecretReferenceDetails";
|
@@ -1,34 +1,17 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faCheck,
|
||||
faCopy,
|
||||
faProjectDiagram,
|
||||
faTrash,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
import {
|
||||
hasSecretReference,
|
||||
SecretReferenceTree
|
||||
} from "@app/views/SecretMainPage/components/SecretReferenceDetails";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string | null;
|
||||
@@ -160,7 +143,7 @@ export const SecretEditRow = ({
|
||||
</div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-24 justify-center space-x-3 pl-2 transition-all",
|
||||
"flex w-16 justify-center space-x-3 pl-2 transition-all",
|
||||
isImportedSecret && "pointer-events-none opacity-0"
|
||||
)}
|
||||
>
|
||||
@@ -220,48 +203,6 @@ export const SecretEditRow = ({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(defaultValue || "")
|
||||
? "Secret Reference Tree"
|
||||
: "Secret does not contain references"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="reference-tree"
|
||||
className="h-full"
|
||||
isDisabled={!hasSecretReference(defaultValue || "") || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faProjectDiagram} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="Secret Reference Details"
|
||||
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
|
||||
>
|
||||
<SecretReferenceTree
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
secretKey={secretName}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
|
@@ -85,22 +85,18 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
accessType
|
||||
});
|
||||
|
||||
const link = `${window.location.origin}/shared/secret/${id}`;
|
||||
|
||||
setSecretLink(link);
|
||||
setSecretLink(`${window.location.origin}/shared/secret/${id}`);
|
||||
reset();
|
||||
|
||||
navigator.clipboard.writeText(link);
|
||||
setCopyTextSecret("secret");
|
||||
|
||||
createNotification({
|
||||
text: "Shared secret link copied to clipboard.",
|
||||
text: "Successfully created a shared secret",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to create a shared secret.",
|
||||
text: "Failed to create a shared secret",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
@@ -230,7 +226,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create Secret Link
|
||||
Create secret link
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
@@ -257,7 +253,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
onClick={() => setSecretLink("")}
|
||||
rightIcon={<FontAwesomeIcon icon={faRedo} className="pl-2" />}
|
||||
>
|
||||
Share Another Secret
|
||||
Share another secret
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
@@ -1,80 +1,80 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowRight, faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
onPasswordSubmit: (val: any) => void;
|
||||
isSubmitting?: boolean;
|
||||
isInvalidCredential?: boolean;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
password: z.string()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const PasswordContainer = ({
|
||||
onPasswordSubmit,
|
||||
isSubmitting,
|
||||
isInvalidCredential
|
||||
}: Props) => {
|
||||
const { control, handleSubmit } = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ password }: FormData) => {
|
||||
onPasswordSubmit(password);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error) || isInvalidCredential}
|
||||
errorText={isInvalidCredential ? "Invalid credential" : error?.message}
|
||||
isRequired
|
||||
label="Password"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 rounded-md">
|
||||
<Input {...field} placeholder="Enter Password to view secret" type="password" />
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={isSubmitting ? "fa-spin" : ""}
|
||||
icon={isSubmitting ? faSpinner : faArrowRight}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
<Button
|
||||
className="w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")}
|
||||
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
|
||||
>
|
||||
Share Your Own Secret
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowRight, faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
onPasswordSubmit: (val: any) => void;
|
||||
isSubmitting?: boolean;
|
||||
isInvalidCredential?: boolean;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
password: z.string()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const PasswordContainer = ({
|
||||
onPasswordSubmit,
|
||||
isSubmitting,
|
||||
isInvalidCredential
|
||||
}: Props) => {
|
||||
const { control, handleSubmit } = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ password }: FormData) => {
|
||||
onPasswordSubmit(password);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error) || isInvalidCredential}
|
||||
errorText={isInvalidCredential ? "Invalid credential" : error?.message}
|
||||
isRequired
|
||||
label="Password"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 rounded-md">
|
||||
<Input {...field} placeholder="Enter Password to view secret" type="password" />
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={isSubmitting ? "fa-spin" : ""}
|
||||
icon={isSubmitting ? faSpinner : faArrowRight}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
<Button
|
||||
className="w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")}
|
||||
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
|
||||
>
|
||||
Share your own secret
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -79,7 +79,7 @@ export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
||||
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")}
|
||||
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
|
||||
>
|
||||
Share Your Own Secret
|
||||
Share your own secret
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|