From e99182c141422f624c5f98d6d5fcb5388da23f95 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 8 May 2024 15:51:09 -0700 Subject: [PATCH] Complete adding GCP GCE auth --- backend/package-lock.json | 212 +++++++- backend/package.json | 2 + backend/src/@types/fastify.d.ts | 4 +- backend/src/@types/knex.d.ts | 14 +- .../20240507213008_identity-gcp-iam-auth.ts | 15 +- .../db/schemas/access-approval-policies.ts | 2 +- ...gcp-iam-auths.ts => identity-gcp-auths.ts} | 15 +- backend/src/db/schemas/index.ts | 2 +- backend/src/db/schemas/models.ts | 4 +- backend/src/db/schemas/users.ts | 2 +- .../ee/services/audit-log/audit-log-types.ts | 38 +- backend/src/server/routes/index.ts | 13 +- ...-router.ts => identity-gcp-auth-router.ts} | 116 +++-- backend/src/server/routes/v1/index.ts | 4 +- .../identity-gcp-auth-dal.ts | 10 + .../identity-gcp-auth-fns.ts | 101 ++++ .../identity-gcp-auth-service.ts | 474 ++++++++++++++++++ .../identity-gcp-auth-types.ts} | 35 +- .../identity-gcp-auth-validators.ts | 24 + .../identity-gcp-iam-auth-dal.ts | 10 - .../identity-gcp-iam-auth-fns.ts | 17 - .../identity-gcp-iam-auth-service.ts | 304 ----------- .../identity-gcp-iam-auth-validators.ts | 14 - .../platform/identities/gcp-iam-auth.mdx | 17 +- .../src/hooks/api/identities/constants.tsx | 2 +- frontend/src/hooks/api/identities/enums.tsx | 2 +- frontend/src/hooks/api/identities/index.tsx | 6 +- .../src/hooks/api/identities/mutations.tsx | 98 ++-- frontend/src/hooks/api/identities/queries.tsx | 16 +- frontend/src/hooks/api/identities/types.ts | 15 +- .../IdentityAuthMethodModal.tsx | 8 +- ...amAuthForm.tsx => IdentityGcpAuthForm.tsx} | 127 ++++- 32 files changed, 1178 insertions(+), 545 deletions(-) rename backend/src/db/schemas/{identity-gcp-iam-auths.ts => identity-gcp-auths.ts} (54%) rename backend/src/server/routes/v1/{identity-gcp-iam-auth-router.ts => identity-gcp-auth-router.ts} (58%) create mode 100644 backend/src/services/identity-gcp-auth/identity-gcp-auth-dal.ts create mode 100644 backend/src/services/identity-gcp-auth/identity-gcp-auth-fns.ts create mode 100644 backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts rename backend/src/services/{identity-gcp-iam-auth/identity-gcp-iam-auth-types.ts => identity-gcp-auth/identity-gcp-auth-types.ts} (58%) create mode 100644 backend/src/services/identity-gcp-auth/identity-gcp-auth-validators.ts delete mode 100644 backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-dal.ts delete mode 100644 backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-fns.ts delete mode 100644 backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-service.ts delete mode 100644 backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-validators.ts rename frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/{IdentityGcpIamAuthForm.tsx => IdentityGcpAuthForm.tsx} (76%) diff --git a/backend/package-lock.json b/backend/package-lock.json index 5928f64a5..071ba6145 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -39,6 +39,8 @@ "dotenv": "^16.4.1", "fastify": "^4.26.0", "fastify-plugin": "^4.5.1", + "google-auth-library": "^9.9.0", + "googleapis": "^137.1.0", "handlebars": "^4.7.8", "ioredis": "^5.3.2", "jmespath": "^0.16.0", @@ -49,7 +51,7 @@ "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", "ms": "^2.1.3", - "mysql2": "^3.9.4", + "mysql2": "^3.9.7", "nanoid": "^5.0.4", "nodemailer": "^6.9.9", "ora": "^7.0.1", @@ -6183,6 +6185,14 @@ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -7759,6 +7769,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/extsprintf": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", @@ -8286,6 +8301,88 @@ "node": ">=8" } }, + "node_modules/gaxios": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.5.0.tgz", + "integrity": "sha512-R9QGdv8j4/dlNoQbX3hSaK/S0rkMijqjVvW3YM06CoBdbU/VdKd159j4hePpng0KuE6Lh6JJ7UdmVGJZFcAG1w==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gaxios/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/generate-function": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", @@ -8482,6 +8579,69 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, + "node_modules/google-auth-library": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.9.0.tgz", + "integrity": "sha512-9l+zO07h1tDJdIHN74SpnWIlNR+OuOemXlWJlLP9pXy6vFtizgpEzMuwJa4lqY9UAdiAv5DVd5ql0Am916I+aA==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/googleapis": { + "version": "137.1.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz", + "integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==", + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -8504,6 +8664,37 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -9277,6 +9468,14 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -10102,9 +10301,9 @@ } }, "node_modules/mysql2": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz", - "integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz", + "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -13703,6 +13902,11 @@ "querystring": "0.2.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", diff --git a/backend/package.json b/backend/package.json index 113fa4d5b..96a4a7c37 100644 --- a/backend/package.json +++ b/backend/package.json @@ -100,6 +100,8 @@ "dotenv": "^16.4.1", "fastify": "^4.26.0", "fastify-plugin": "^4.5.1", + "google-auth-library": "^9.9.0", + "googleapis": "^137.1.0", "handlebars": "^4.7.8", "ioredis": "^5.3.2", "jmespath": "^0.16.0", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 278aa7401..7f44b6ec3 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -33,7 +33,7 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityAwsIamAuthServiceFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-service"; -import { TIdentityGcpIamAuthServiceFactory } from "@app/services/identity-gcp-iam-auth/identity-gcp-iam-auth-service"; +import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; @@ -117,7 +117,7 @@ declare module "fastify" { identityAccessToken: TIdentityAccessTokenServiceFactory; identityProject: TIdentityProjectServiceFactory; identityUa: TIdentityUaServiceFactory; - identityGcpIamAuth: TIdentityGcpIamAuthServiceFactory; + identityGcpAuth: TIdentityGcpAuthServiceFactory; identityAwsIamAuth: TIdentityAwsIamAuthServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 7d1df5003..c8884bde4 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -62,9 +62,9 @@ import { TIdentityAwsIamAuths, TIdentityAwsIamAuthsInsert, TIdentityAwsIamAuthsUpdate, - TIdentityGcpIamAuths, - TIdentityGcpIamAuthsInsert, - TIdentityGcpIamAuthsUpdate, + TIdentityGcpAuths, + TIdentityGcpAuthsInsert, + TIdentityGcpAuthsUpdate, TIdentityOrgMemberships, TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsUpdate, @@ -332,10 +332,10 @@ declare module "knex/types/tables" { TIdentityUniversalAuthsInsert, TIdentityUniversalAuthsUpdate >; - [TableName.IdentityGcpIamAuth]: Knex.CompositeTableType< - TIdentityGcpIamAuths, - TIdentityGcpIamAuthsInsert, - TIdentityGcpIamAuthsUpdate + [TableName.IdentityGcpAuth]: Knex.CompositeTableType< + TIdentityGcpAuths, + TIdentityGcpAuthsInsert, + TIdentityGcpAuthsUpdate >; [TableName.IdentityAwsIamAuth]: Knex.CompositeTableType< TIdentityAwsIamAuths, diff --git a/backend/src/db/migrations/20240507213008_identity-gcp-iam-auth.ts b/backend/src/db/migrations/20240507213008_identity-gcp-iam-auth.ts index 3d1a3feb2..2de275045 100644 --- a/backend/src/db/migrations/20240507213008_identity-gcp-iam-auth.ts +++ b/backend/src/db/migrations/20240507213008_identity-gcp-iam-auth.ts @@ -4,8 +4,8 @@ import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { - if (!(await knex.schema.hasTable(TableName.IdentityGcpIamAuth))) { - await knex.schema.createTable(TableName.IdentityGcpIamAuth, (t) => { + if (!(await knex.schema.hasTable(TableName.IdentityGcpAuth))) { + await knex.schema.createTable(TableName.IdentityGcpAuth, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable(); t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable(); @@ -14,15 +14,20 @@ export async function up(knex: Knex): Promise { t.timestamps(true, true, true); t.uuid("identityId").notNullable().unique(); t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.text("encryptedCredentials").notNullable(); + t.string("credentialsIV").notNullable(); + t.string("credentialsTag").notNullable(); + t.string("type").notNullable(); t.string("allowedServiceAccounts").notNullable(); t.string("allowedProjects").notNullable(); + t.string("allowedZones").notNullable(); // GCE only (fully qualified zone names) }); } - await createOnUpdateTrigger(knex, TableName.IdentityGcpIamAuth); + await createOnUpdateTrigger(knex, TableName.IdentityGcpAuth); } export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists(TableName.IdentityGcpIamAuth); - await dropOnUpdateTrigger(knex, TableName.IdentityGcpIamAuth); + await knex.schema.dropTableIfExists(TableName.IdentityGcpAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityGcpAuth); } diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index bf7e74ff2..69068d23b 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -11,8 +11,8 @@ export const AccessApprovalPoliciesSchema = z.object({ id: z.string().uuid(), name: z.string(), approvals: z.number().default(1), - envId: z.string().uuid(), secretPath: z.string().nullable().optional(), + envId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/db/schemas/identity-gcp-iam-auths.ts b/backend/src/db/schemas/identity-gcp-auths.ts similarity index 54% rename from backend/src/db/schemas/identity-gcp-iam-auths.ts rename to backend/src/db/schemas/identity-gcp-auths.ts index e5b7d090e..5eda85463 100644 --- a/backend/src/db/schemas/identity-gcp-iam-auths.ts +++ b/backend/src/db/schemas/identity-gcp-auths.ts @@ -7,7 +7,7 @@ import { z } from "zod"; import { TImmutableDBKeys } from "./models"; -export const IdentityGcpIamAuthsSchema = z.object({ +export const IdentityGcpAuthsSchema = z.object({ id: z.string().uuid(), accessTokenTTL: z.coerce.number().default(7200), accessTokenMaxTTL: z.coerce.number().default(7200), @@ -16,10 +16,15 @@ export const IdentityGcpIamAuthsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), identityId: z.string().uuid(), + encryptedCredentials: z.string(), + credentialsIV: z.string(), + credentialsTag: z.string(), + type: z.string(), allowedServiceAccounts: z.string(), - allowedProjects: z.string() + allowedProjects: z.string(), + allowedZones: z.string() }); -export type TIdentityGcpIamAuths = z.infer; -export type TIdentityGcpIamAuthsInsert = Omit, TImmutableDBKeys>; -export type TIdentityGcpIamAuthsUpdate = Partial, TImmutableDBKeys>>; +export type TIdentityGcpAuths = z.infer; +export type TIdentityGcpAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityGcpAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 0b8f1c22e..9ad2c3e46 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -18,7 +18,7 @@ export * from "./groups"; export * from "./identities"; export * from "./identity-access-tokens"; export * from "./identity-aws-iam-auths"; -export * from "./identity-gcp-iam-auths"; +export * from "./identity-gcp-auths"; export * from "./identity-org-memberships"; export * from "./identity-project-additional-privilege"; export * from "./identity-project-membership-role"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 2a4c2d9e6..952d18539 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -44,7 +44,7 @@ export enum TableName { Identity = "identities", IdentityAccessToken = "identity_access_tokens", IdentityUniversalAuth = "identity_universal_auths", - IdentityGcpIamAuth = "identity_gcp_iam_auths", + IdentityGcpAuth = "identity_gcp_auths", IdentityUaClientSecret = "identity_ua_client_secrets", IdentityAwsIamAuth = "identity_aws_iam_auths", IdentityOrgMembership = "identity_org_memberships", @@ -145,6 +145,6 @@ export enum ProjectUpgradeStatus { export enum IdentityAuthMethod { Univeral = "universal-auth", - GCP_IAM_AUTH = "gcp-iam-auth", + GCP_AUTH = "gcp-auth", AWS_IAM_AUTH = "aws-iam-auth" } diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts index 3eee2683f..d5a4d5b49 100644 --- a/backend/src/db/schemas/users.ts +++ b/backend/src/db/schemas/users.ts @@ -22,7 +22,7 @@ export const UsersSchema = z.object({ updatedAt: z.date(), isGhost: z.boolean().default(false), username: z.string(), - isEmailVerified: z.boolean().nullable().optional() + isEmailVerified: z.boolean().default(false).nullable().optional() }); export type TUsers = z.infer; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index aba6e7329..86fe56c52 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -66,10 +66,10 @@ export enum EventType { CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", - LOGIN_IDENTITY_GCP_IAM_AUTH = "login-identity-gcp-iam-auth", - ADD_IDENTITY_GCP_IAM_AUTH = "add-identity-gcp-iam -auth", - UPDATE_IDENTITY_GCP_IAM_AUTH = "update-identity-gcp-iam-auth", - GET_IDENTITY_GCP_IAM_AUTH = "get-identity-gcp-iam-auth", + LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth", + ADD_IDENTITY_GCP_AUTH = "add-identity-gcp -auth", + UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth", + GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth", LOGIN_IDENTITY_AWS_IAM_AUTH = "login-identity-aws-iam-auth", ADD_IDENTITY_AWS_IAM_AUTH = "add-identity-aws-iam-auth", UPDATE_IDENTITY_AWS_IAM_AUTH = "update-identity-aws-iam-auth", @@ -414,21 +414,23 @@ interface RevokeIdentityUniversalAuthClientSecretEvent { }; } -interface LoginIdentityGcpIamAuthEvent { - type: EventType.LOGIN_IDENTITY_GCP_IAM_AUTH; +interface LoginIdentityGcpAuthEvent { + type: EventType.LOGIN_IDENTITY_GCP_AUTH; metadata: { identityId: string; - identityGcpIamAuthId: string; + identityGcpAuthId: string; identityAccessTokenId: string; }; } -interface AddIdentityGcpIamAuthEvent { - type: EventType.ADD_IDENTITY_GCP_IAM_AUTH; +interface AddIdentityGcpAuthEvent { + type: EventType.ADD_IDENTITY_GCP_AUTH; metadata: { identityId: string; + type: string; allowedServiceAccounts: string; allowedProjects: string; + allowedZones: string; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; @@ -436,12 +438,14 @@ interface AddIdentityGcpIamAuthEvent { }; } -interface UpdateIdentityGcpIamAuthEvent { - type: EventType.UPDATE_IDENTITY_GCP_IAM_AUTH; +interface UpdateIdentityGcpAuthEvent { + type: EventType.UPDATE_IDENTITY_GCP_AUTH; metadata: { identityId: string; + type?: string; allowedServiceAccounts?: string; allowedProjects?: string; + allowedZones?: string; accessTokenTTL?: number; accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; @@ -449,8 +453,8 @@ interface UpdateIdentityGcpIamAuthEvent { }; } -interface GetIdentityGcpIamAuthEvent { - type: EventType.GET_IDENTITY_GCP_IAM_AUTH; +interface GetIdentityGcpAuthEvent { + type: EventType.GET_IDENTITY_GCP_AUTH; metadata: { identityId: string; }; @@ -754,10 +758,10 @@ export type Event = | CreateIdentityUniversalAuthClientSecretEvent | GetIdentityUniversalAuthClientSecretsEvent | RevokeIdentityUniversalAuthClientSecretEvent - | LoginIdentityGcpIamAuthEvent - | AddIdentityGcpIamAuthEvent - | UpdateIdentityGcpIamAuthEvent - | GetIdentityGcpIamAuthEvent + | LoginIdentityGcpAuthEvent + | AddIdentityGcpAuthEvent + | UpdateIdentityGcpAuthEvent + | GetIdentityGcpAuthEvent | LoginIdentityAwsIamAuthEvent | AddIdentityAwsIamAuthEvent | UpdateIdentityAwsIamAuthEvent diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e2dc11606..e14329b9d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -80,8 +80,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { identityAwsIamAuthDALFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-dal"; import { identityAwsIamAuthServiceFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-service"; -import { identityGcpIamAuthDALFactory } from "@app/services/identity-gcp-iam-auth/identity-gcp-iam-auth-dal"; -import { identityGcpIamAuthServiceFactory } from "@app/services/identity-gcp-iam-auth/identity-gcp-iam-auth-service"; +import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal"; +import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal"; import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; @@ -207,7 +207,7 @@ export const registerRoutes = async ( const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityAwsIamAuthDAL = identityAwsIamAuthDALFactory(db); - const identityGcpIamAuthDAL = identityGcpIamAuthDALFactory(db); + const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const auditLogDAL = auditLogDALFactory(db); const auditLogStreamDAL = auditLogStreamDALFactory(db); @@ -706,11 +706,12 @@ export const registerRoutes = async ( identityUaDAL, licenseService }); - const identityGcpIamAuthService = identityGcpIamAuthServiceFactory({ - identityGcpIamAuthDAL, + const identityGcpAuthService = identityGcpAuthServiceFactory({ + identityGcpAuthDAL, identityOrgMembershipDAL, identityAccessTokenDAL, identityDAL, + orgBotDAL, permissionService, licenseService }); @@ -792,7 +793,7 @@ export const registerRoutes = async ( identityAccessToken: identityAccessTokenService, identityProject: identityProjectService, identityUa: identityUaService, - identityGcpIamAuth: identityGcpIamAuthService, + identityGcpAuth: identityGcpAuthService, identityAwsIamAuth: identityAWSIAMAuthService, secretApprovalPolicy: sapService, accessApprovalPolicy: accessApprovalPolicyService, diff --git a/backend/src/server/routes/v1/identity-gcp-iam-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts similarity index 58% rename from backend/src/server/routes/v1/identity-gcp-iam-auth-router.ts rename to backend/src/server/routes/v1/identity-gcp-auth-router.ts index 4a0032c02..2f7b0240c 100644 --- a/backend/src/server/routes/v1/identity-gcp-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -1,22 +1,24 @@ import { z } from "zod"; -import { IdentityGcpIamAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; -import { validateGcpIamAuthField } from "@app/services/identity-gcp-iam-auth/identity-gcp-iam-auth-validators"; +import { + ModifiedIdentityGcpAuthsSchema, + validateGcpAuthField +} from "@app/services/identity-gcp-auth/identity-gcp-auth-validators"; -export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvider) => { +export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/gcp-iam-auth/login", + url: "/gcp-auth/login", config: { rateLimit: writeLimit }, schema: { - description: "Login with GCP IAM Auth", + description: "Login with GCP Auth", body: z.object({ identityId: z.string(), jwt: z.string() @@ -31,18 +33,18 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide } }, handler: async (req) => { - const { identityGcpIamAuth, accessToken, identityAccessToken, identityMembershipOrg } = - await server.services.identityGcpIamAuth.login(req.body); + const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityGcpAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: identityMembershipOrg?.orgId, event: { - type: EventType.LOGIN_IDENTITY_GCP_IAM_AUTH, + type: EventType.LOGIN_IDENTITY_GCP_AUTH, metadata: { - identityId: identityGcpIamAuth.identityId, + identityId: identityGcpAuth.identityId, identityAccessTokenId: identityAccessToken.id, - identityGcpIamAuthId: identityGcpIamAuth.id + identityGcpAuthId: identityGcpAuth.id } } }); @@ -50,21 +52,21 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide return { accessToken, tokenType: "Bearer" as const, - expiresIn: identityGcpIamAuth.accessTokenTTL, - accessTokenMaxTTL: identityGcpIamAuth.accessTokenMaxTTL + expiresIn: identityGcpAuth.accessTokenTTL, + accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL }; } }); server.route({ method: "POST", - url: "/gcp-iam-auth/identities/:identityId", + url: "/gcp-auth/identities/:identityId", config: { rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Attach GCP IAM Auth configuration onto identity", + description: "Attach GCP Auth configuration onto identity", security: [ { bearerAuth: [] @@ -74,8 +76,11 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide identityId: z.string().trim() }), body: z.object({ - allowedServiceAccounts: validateGcpIamAuthField, - allowedProjects: validateGcpIamAuthField, + credentials: z.string().trim(), + type: z.enum(["iam", "gce"]), + allowedServiceAccounts: validateGcpAuthField, + allowedProjects: validateGcpAuthField, + allowedZones: validateGcpAuthField, accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() @@ -102,12 +107,12 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide }), response: { 200: z.object({ - identityGcpIamAuth: IdentityGcpIamAuthsSchema + identityGcpAuth: ModifiedIdentityGcpAuthsSchema }) } }, handler: async (req) => { - const identityGcpIamAuth = await server.services.identityGcpIamAuth.attachGcpIamAuth({ + const identityGcpAuth = await server.services.identityGcpAuth.attachGcpAuth({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -118,34 +123,36 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityGcpIamAuth.orgId, + orgId: identityGcpAuth.orgId, event: { - type: EventType.ADD_IDENTITY_GCP_IAM_AUTH, + type: EventType.ADD_IDENTITY_GCP_AUTH, metadata: { - identityId: identityGcpIamAuth.identityId, - allowedServiceAccounts: identityGcpIamAuth.allowedServiceAccounts, - allowedProjects: identityGcpIamAuth.allowedProjects, - accessTokenTTL: identityGcpIamAuth.accessTokenTTL, - accessTokenMaxTTL: identityGcpIamAuth.accessTokenMaxTTL, - accessTokenTrustedIps: identityGcpIamAuth.accessTokenTrustedIps as TIdentityTrustedIp[], - accessTokenNumUsesLimit: identityGcpIamAuth.accessTokenNumUsesLimit + identityId: identityGcpAuth.identityId, + type: identityGcpAuth.type, + allowedServiceAccounts: identityGcpAuth.allowedServiceAccounts, + allowedProjects: identityGcpAuth.allowedProjects, + allowedZones: identityGcpAuth.allowedZones, + accessTokenTTL: identityGcpAuth.accessTokenTTL, + accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityGcpAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit } } }); - return { identityGcpIamAuth }; + return { identityGcpAuth }; } }); server.route({ method: "PATCH", - url: "/gcp-iam-auth/identities/:identityId", + url: "/gcp-auth/identities/:identityId", config: { rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Update GCP IAM Auth configuration on identity", + description: "Update GCP Auth configuration on identity", security: [ { bearerAuth: [] @@ -155,8 +162,11 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide identityId: z.string().trim() }), body: z.object({ - allowedServiceAccounts: z.string().trim().optional(), - allowedProjects: z.string().trim().optional(), + credentials: z.string().trim().optional(), + type: z.enum(["iam", "gce"]).optional(), + allowedServiceAccounts: validateGcpAuthField, + allowedProjects: validateGcpAuthField, + allowedZones: validateGcpAuthField, accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() @@ -176,12 +186,12 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide }), response: { 200: z.object({ - identityGcpIamAuth: IdentityGcpIamAuthsSchema + identityGcpAuth: ModifiedIdentityGcpAuthsSchema }) } }, handler: async (req) => { - const identityGcpIamAuth = await server.services.identityGcpIamAuth.updateGcpIamAuth({ + const identityGcpAuth = await server.services.identityGcpAuth.updateGcpAuth({ actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, @@ -192,34 +202,36 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityGcpIamAuth.orgId, + orgId: identityGcpAuth.orgId, event: { - type: EventType.UPDATE_IDENTITY_GCP_IAM_AUTH, + type: EventType.UPDATE_IDENTITY_GCP_AUTH, metadata: { - identityId: identityGcpIamAuth.identityId, - allowedServiceAccounts: identityGcpIamAuth.allowedServiceAccounts, - allowedProjects: identityGcpIamAuth.allowedProjects, - accessTokenTTL: identityGcpIamAuth.accessTokenTTL, - accessTokenMaxTTL: identityGcpIamAuth.accessTokenMaxTTL, - accessTokenTrustedIps: identityGcpIamAuth.accessTokenTrustedIps as TIdentityTrustedIp[], - accessTokenNumUsesLimit: identityGcpIamAuth.accessTokenNumUsesLimit + identityId: identityGcpAuth.identityId, + type: identityGcpAuth.type, + allowedServiceAccounts: identityGcpAuth.allowedServiceAccounts, + allowedProjects: identityGcpAuth.allowedProjects, + allowedZones: identityGcpAuth.allowedZones, + accessTokenTTL: identityGcpAuth.accessTokenTTL, + accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityGcpAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit } } }); - return { identityGcpIamAuth }; + return { identityGcpAuth }; } }); server.route({ method: "GET", - url: "/gcp-iam-auth/identities/:identityId", + url: "/gcp-auth/identities/:identityId", config: { rateLimit: readLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Retrieve GCP IAM Auth configuration on identity", + description: "Retrieve GCP Auth configuration on identity", security: [ { bearerAuth: [] @@ -230,12 +242,12 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide }), response: { 200: z.object({ - identityGcpIamAuth: IdentityGcpIamAuthsSchema + identityGcpAuth: ModifiedIdentityGcpAuthsSchema }) } }, handler: async (req) => { - const identityGcpIamAuth = await server.services.identityGcpIamAuth.getGcpIamAuth({ + const identityGcpAuth = await server.services.identityGcpAuth.getGcpAuth({ identityId: req.params.identityId, actor: req.permission.type, actorId: req.permission.id, @@ -245,16 +257,16 @@ export const registerIdentityGcpIamAuthRouter = async (server: FastifyZodProvide await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityGcpIamAuth.orgId, + orgId: identityGcpAuth.orgId, event: { - type: EventType.GET_IDENTITY_GCP_IAM_AUTH, + type: EventType.GET_IDENTITY_GCP_AUTH, metadata: { - identityId: identityGcpIamAuth.identityId + identityId: identityGcpAuth.identityId } } }); - return { identityGcpIamAuth }; + return { identityGcpAuth }; } }); }; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 74bad1bdd..406f78be6 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -3,7 +3,7 @@ import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAwsIamAuthRouter } from "./identity-aws-iam-auth-router"; -import { registerIdentityGcpIamAuthRouter } from "./identity-gcp-iam-auth-router"; +import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityUaRouter } from "./identity-ua"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; @@ -29,7 +29,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { async (authRouter) => { await authRouter.register(registerAuthRoutes); await authRouter.register(registerIdentityUaRouter); - await authRouter.register(registerIdentityGcpIamAuthRouter); + await authRouter.register(registerIdentityGcpAuthRouter); await authRouter.register(registerIdentityAwsIamAuthRouter); await authRouter.register(registerIdentityAccessTokenRouter); }, diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-dal.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-dal.ts new file mode 100644 index 000000000..e10250445 --- /dev/null +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityGcpAuthDALFactory = ReturnType; + +export const identityGcpAuthDALFactory = (db: TDbClient) => { + const gcpAuthOrm = ormify(db, TableName.IdentityGcpAuth); + return gcpAuthOrm; +}; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-fns.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-fns.ts new file mode 100644 index 000000000..ad3095b71 --- /dev/null +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-fns.ts @@ -0,0 +1,101 @@ +import axios from "axios"; +import { JWTInput, OAuth2Client } from "google-auth-library"; +import { google } from "googleapis"; +import jwt from "jsonwebtoken"; + +import { UnauthorizedError } from "@app/lib/errors"; + +import { TDecodedGcpIamAuthJwt, TGcpGceIdTokenPayload } from "./identity-gcp-auth-types"; + +/** + * Return the full details of the service account corresponding to the service account email or unique ID [serviceAccount]. + * @param {string} serviceAccount - The email or unique ID of the service account. + * @param {string} credentials - The credentials in the GCP Auth configuration for Infisical. + * @returns + */ +const getGcpServiceAccountDetails = async (serviceAccount: string, credentials: string) => { + const auth = new google.auth.GoogleAuth({ + credentials: JSON.parse(credentials) as JWTInput, + scopes: ["https://www.googleapis.com/auth/iam"] + }); + + const iam = google.iam({ + version: "v1", + auth + }); + + const { data } = await iam.projects.serviceAccounts.get({ + name: `projects/-/serviceAccounts/${serviceAccount}` + }); + + return data; +}; + +/** + * Validates that the identity token [jwt] sent in from a client GCE instance as part of GCP GCE authentication + * is valid. + * @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience). + * @param {string} jwt - The identity token to validate. + * @param {string} credentials - The credentials in the GCP Auth configuration for Infisical. + */ +export const validateGceIdentity = async ({ + identityId, + jwt: identityToken, + credentials +}: { + identityId: string; + jwt: string; + credentials: string; +}) => { + const oAuth2Client = new OAuth2Client(); + const response = await oAuth2Client.getFederatedSignonCerts(); + const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync( + identityToken, + response.certs, + identityId, // audience + ["https://accounts.google.com"] + ); + const payload = ticket.getPayload() as TGcpGceIdTokenPayload; + if (!payload || !payload.email) throw new UnauthorizedError(); + const serviceAccountDetails = await getGcpServiceAccountDetails(payload.email, credentials); + + return { serviceAccountDetails, gceInstanceDetails: payload }; +}; + +/** + * Validates that the signed JWT token for a GCP service account is valid as part of GCP IAM authentication. + * @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience). + * @param {string} jwt - The signed JWT token to validate. + * @param {string} credentials - The credentials in the GCP Auth configuration for Infisical. + * @returns + */ +export const validateIamIdentity = async ({ + identityId, + jwt: serviceAccountJwt, + credentials +}: { + identityId: string; + jwt: string; + credentials: string; +}) => { + const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt; + const { sub, aud } = decodedJwt.payload; + + const { + data + }: { + data: { + [key: string]: string; + }; + } = await axios.get(`https://www.googleapis.com/service_accounts/v1/metadata/x509/${sub}`); + + const publicKey = data[decodedJwt.header.kid]; + + jwt.verify(serviceAccountJwt, publicKey, { + algorithms: ["RS256"] + }); + + if (aud !== identityId) throw new UnauthorizedError(); + const serviceAccountDetails = await getGcpServiceAccountDetails(sub, credentials); + return { serviceAccountDetails }; +}; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts new file mode 100644 index 000000000..e2fb37386 --- /dev/null +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -0,0 +1,474 @@ +import { ForbiddenError } from "@casl/ability"; +import { iam_v1 } from "googleapis"; +import jwt from "jsonwebtoken"; + +import { IdentityAuthMethod, SecretKeyEncoding, TIdentityGcpAuthsUpdate } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { getConfig } from "@app/lib/config/env"; +import { + decryptSymmetric, + encryptSymmetric, + generateAsymmetricKeyPair, + generateSymmetricKey, + infisicalSymmetricDecrypt, + infisicalSymmetricEncypt +} from "@app/lib/crypto/encryption"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; + +import { 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"; +import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal"; +import { validateGceIdentity, validateIamIdentity } from "./identity-gcp-auth-fns"; +import { + TAttachGcpAuthDTO, + TGcpGceIdTokenPayload, + TGetGcpAuthDTO, + TLoginGcpAuthDTO, + TUpdateGcpAuthDTO +} from "./identity-gcp-auth-types"; + +type TIdentityGcpAuthServiceFactoryDep = { + identityGcpAuthDAL: Pick; + identityOrgMembershipDAL: Pick; + identityAccessTokenDAL: Pick; + identityDAL: Pick; + orgBotDAL: Pick; + permissionService: Pick; + licenseService: Pick; +}; + +export type TIdentityGcpAuthServiceFactory = ReturnType; + +export const identityGcpAuthServiceFactory = ({ + identityGcpAuthDAL, + identityOrgMembershipDAL, + identityAccessTokenDAL, + identityDAL, + orgBotDAL, + permissionService, + licenseService +}: TIdentityGcpAuthServiceFactoryDep) => { + const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginGcpAuthDTO) => { + const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); + if (!identityGcpAuth) throw new UnauthorizedError(); + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpAuth.identityId }); + if (!identityMembershipOrg) throw new UnauthorizedError(); + + const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); + if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { encryptedCredentials, credentialsIV, credentialsTag } = identityGcpAuth; + let credentials = ""; + if (encryptedCredentials && credentialsIV && credentialsTag) { + credentials = decryptSymmetric({ + ciphertext: encryptedCredentials, + key, + tag: credentialsTag, + iv: credentialsIV + }); + } + + let serviceAccountDetails: iam_v1.Schema$ServiceAccount; + let gceInstanceDetails: TGcpGceIdTokenPayload | undefined; + switch (identityGcpAuth.type) { + case "gce": { + const gceIdentity = await validateGceIdentity({ + identityId, + jwt: serviceAccountJwt, + credentials + }); + serviceAccountDetails = gceIdentity.serviceAccountDetails; + gceInstanceDetails = gceIdentity.gceInstanceDetails; + break; + } + case "iam": { + const iamIdentity = await validateIamIdentity({ + identityId, + jwt: serviceAccountJwt, + credentials + }); + serviceAccountDetails = iamIdentity.serviceAccountDetails; + break; + } + default: { + throw new BadRequestError({ message: "Invalid GCP Auth type" }); + } + } + + if (identityGcpAuth.allowedServiceAccounts) { + // validate if the service account is in the list of allowed service accounts + + const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts + .split(",") + .map((serviceAccount) => serviceAccount.trim()) + .some( + (serviceAccount) => + serviceAccount === serviceAccountDetails.email || serviceAccount === serviceAccountDetails.uniqueId + ); + + if (!isServiceAccountAllowed) throw new UnauthorizedError(); + } + + if (identityGcpAuth.allowedProjects) { + // validate if the project that the service account belongs to is in the list of allowed projects + + const isProjectAllowed = identityGcpAuth.allowedProjects + .split(",") + .map((project) => project.trim()) + .some((project) => project === serviceAccountDetails.projectId); + + if (!isProjectAllowed) throw new UnauthorizedError(); + } + + if (identityGcpAuth.type === "gce" && gceInstanceDetails && identityGcpAuth.allowedZones) { + const isZoneAllowed = identityGcpAuth.allowedZones + .split(",") + .map((zone) => zone.trim()) + .some((zone) => zone === gceInstanceDetails!.google.compute_engine.zone); + + if (!isZoneAllowed) throw new UnauthorizedError(); + } + + const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityGcpAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityGcpAuth.accessTokenTTL, + accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityGcpAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + { + expiresIn: + Number(identityAccessToken.accessTokenMaxTTL) === 0 + ? undefined + : Number(identityAccessToken.accessTokenMaxTTL) + } + ); + + return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg }; + }; + + const attachGcpAuth = async ({ + identityId, + credentials, + type, + allowedServiceAccounts, + allowedProjects, + allowedZones, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TAttachGcpAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity.authMethod) + 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" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const orgBot = await orgBotDAL.transaction(async (tx) => { + const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx); + if (doc) return doc; + + const { privateKey, publicKey } = generateAsymmetricKeyPair(); + const key = generateSymmetricKey(); + const { + ciphertext: encryptedPrivateKey, + iv: privateKeyIV, + tag: privateKeyTag, + encoding: privateKeyKeyEncoding, + algorithm: privateKeyAlgorithm + } = infisicalSymmetricEncypt(privateKey); + const { + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + encoding: symmetricKeyKeyEncoding, + algorithm: symmetricKeyAlgorithm + } = infisicalSymmetricEncypt(key); + + return orgBotDAL.create( + { + name: "Infisical org bot", + publicKey, + privateKeyIV, + encryptedPrivateKey, + symmetricKeyIV, + symmetricKeyTag, + encryptedSymmetricKey, + symmetricKeyAlgorithm, + orgId: identityMembershipOrg.orgId, + privateKeyTag, + privateKeyAlgorithm, + privateKeyKeyEncoding, + symmetricKeyKeyEncoding + }, + tx + ); + }); + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { + ciphertext: encryptedCredentials, + iv: credentialsIV, + tag: credentialsTag + } = encryptSymmetric(credentials, key); + + const identityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { + const doc = await identityGcpAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + type, + allowedServiceAccounts, + allowedProjects, + allowedZones, + encryptedCredentials, + credentialsIV, + credentialsTag, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + await identityDAL.updateById( + identityMembershipOrg.identityId, + { + authMethod: IdentityAuthMethod.GCP_AUTH + }, + tx + ); + return doc; + }); + return { ...identityGcpAuth, credentials, orgId: identityMembershipOrg.orgId }; + }; + + const updateGcpAuth = async ({ + identityId, + type, + credentials, + allowedServiceAccounts, + allowedProjects, + allowedZones, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateGcpAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH) + throw new BadRequestError({ + message: "Failed to update GCP Auth" + }); + + const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityGcpAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL) + ) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const updateQuery: TIdentityGcpAuthsUpdate = { + type, + allowedServiceAccounts, + allowedProjects, + allowedZones, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }; + + const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); + if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + if (credentials !== undefined) { + const { + ciphertext: encryptedCredentials, + iv: credentialsIV, + tag: credentialsTag + } = encryptSymmetric(credentials, key); + updateQuery.encryptedCredentials = encryptedCredentials; + updateQuery.credentialsIV = credentialsIV; + updateQuery.credentialsTag = credentialsTag; + } + + const updatedGcpAuth = await identityGcpAuthDAL.updateById(identityGcpAuth.id, updateQuery); + + return { + ...updatedGcpAuth, + credentials: decryptSymmetric({ + ciphertext: updatedGcpAuth.encryptedCredentials, + iv: updatedGcpAuth.credentialsIV, + tag: updatedGcpAuth.credentialsTag, + key + }), + orgId: identityMembershipOrg.orgId + }; + }; + + const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH) + throw new BadRequestError({ + message: "The identity does not have GCP Auth attached" + }); + + const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + + const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); + if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { encryptedCredentials, credentialsIV, credentialsTag } = identityGcpAuth; + let credentials = ""; + if (encryptedCredentials && credentialsIV && credentialsTag) { + credentials = decryptSymmetric({ + ciphertext: encryptedCredentials, + key, + tag: credentialsTag, + iv: credentialsIV + }); + } + + return { ...identityGcpAuth, credentials, orgId: identityMembershipOrg.orgId }; + }; + + return { + login, + attachGcpAuth, + updateGcpAuth, + getGcpAuth + }; +}; diff --git a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-types.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts similarity index 58% rename from backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-types.ts rename to backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts index 168a5e237..c01ab2edb 100644 --- a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-types.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts @@ -1,34 +1,61 @@ import { TProjectPermission } from "@app/lib/types"; -export type TLoginGcpIamAuthDTO = { +export type TLoginGcpAuthDTO = { identityId: string; jwt: string; }; -export type TAttachGcpIamAuthDTO = { +export type TAttachGcpAuthDTO = { identityId: string; + credentials: string; + type: "iam" | "gce"; allowedServiceAccounts: string; allowedProjects: string; + allowedZones: string; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: { ipAddress: string }[]; } & Omit; -export type TUpdateGcpIamAuthDTO = { +export type TUpdateGcpAuthDTO = { identityId: string; + credentials?: string; + type?: "iam" | "gce"; allowedServiceAccounts?: string; allowedProjects?: string; + allowedZones?: string; accessTokenTTL?: number; accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; accessTokenTrustedIps?: { ipAddress: string }[]; } & Omit; -export type TGetGcpIamAuthDTO = { +export type TGetGcpAuthDTO = { identityId: string; } & Omit; +export type TGcpGceIdTokenPayload = { + aud: string; + azp: string; + email: string; + email_verified: boolean; + exp: number; + google: { + compute_engine: { + instance_creation_timestamp: number; + instance_id: string; + instance_name: string; + project_id: string; + project_number: number; + zone: string; + }; + }; + iat: number; + iss: string; + sub: string; +}; + export type TDecodedGcpIamAuthJwt = { header: { alg: string; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-validators.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-validators.ts new file mode 100644 index 000000000..7e3ac9439 --- /dev/null +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-validators.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { IdentityGcpAuthsSchema } from "@app/db/schemas"; + +export const ModifiedIdentityGcpAuthsSchema = IdentityGcpAuthsSchema.omit({ + encryptedCredentials: true, + credentialsIV: true, + credentialsTag: true +}).extend({ + credentials: z.string() +}); + +export const validateGcpAuthField = z + .string() + .trim() + .default("") + .transform((data) => { + if (data === "") return ""; + // Trim each ID and join with ', ' to ensure formatting + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }); diff --git a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-dal.ts b/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-dal.ts deleted file mode 100644 index 78db67968..000000000 --- a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityGcpIamAuthDALFactory = ReturnType; - -export const identityGcpIamAuthDALFactory = (db: TDbClient) => { - const gcpIamAuthOrm = ormify(db, TableName.IdentityGcpIamAuth); - return gcpIamAuthOrm; -}; diff --git a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-fns.ts b/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-fns.ts deleted file mode 100644 index f9d054b1b..000000000 --- a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-fns.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Extracts the GCP service account email into the name and project ID parts where - * the email is in the format: @.iam.gserviceaccount.com - */ -export const extractGcpServiceAccountEmail = (email: string) => { - const regex = /^(.+)@(.+)\.iam\.gserviceaccount\.com$/; - const match = email.match(regex); - - if (!match) { - throw new Error("Invalid GCP service account email format."); - } - - const name = match[1]; - const projectId = match[2]; - - return { name, projectId }; -}; diff --git a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-service.ts b/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-service.ts deleted file mode 100644 index 380fffd68..000000000 --- a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-service.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { ForbiddenError } from "@casl/ability"; -import axios from "axios"; -import jwt from "jsonwebtoken"; - -import { IdentityAuthMethod } from "@app/db/schemas"; -import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; -import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; - -import { 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"; -import { TIdentityGcpIamAuthDALFactory } from "./identity-gcp-iam-auth-dal"; -import { extractGcpServiceAccountEmail } from "./identity-gcp-iam-auth-fns"; -import { - TAttachGcpIamAuthDTO, - TDecodedGcpIamAuthJwt, - TGetGcpIamAuthDTO, - TLoginGcpIamAuthDTO, - TUpdateGcpIamAuthDTO -} from "./identity-gcp-iam-auth-types"; - -type TIdentityGcpIamAuthServiceFactoryDep = { - identityGcpIamAuthDAL: Pick; - identityOrgMembershipDAL: Pick; - identityAccessTokenDAL: Pick; - identityDAL: Pick; - permissionService: Pick; - licenseService: Pick; -}; - -export type TIdentityGcpIamAuthServiceFactory = ReturnType; - -export const identityGcpIamAuthServiceFactory = ({ - identityGcpIamAuthDAL, - identityOrgMembershipDAL, - identityAccessTokenDAL, - identityDAL, - permissionService, - licenseService -}: TIdentityGcpIamAuthServiceFactoryDep) => { - const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginGcpIamAuthDTO) => { - const identityGcpIamAuth = await identityGcpIamAuthDAL.findOne({ identityId }); - if (!identityGcpIamAuth) throw new UnauthorizedError(); - - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpIamAuth.identityId }); - - const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt; - const { sub, aud } = decodedJwt.payload; - - const { - data - }: { - data: { - [key: string]: string; - }; - } = await axios.get(`https://www.googleapis.com/service_accounts/v1/metadata/x509/${sub}`); - - const publicKey = data[decodedJwt.header.kid]; - - jwt.verify(serviceAccountJwt, publicKey, { - algorithms: ["RS256"] - }); - - if (aud !== identityId) throw new UnauthorizedError(); - - const { name, projectId } = extractGcpServiceAccountEmail(sub); - - if (identityGcpIamAuth.allowedServiceAccounts) { - // validate if the service account is in the list of allowed service accounts - - const isServiceAccountAllowed = identityGcpIamAuth.allowedServiceAccounts - .split(",") - .map((serviceAccount) => serviceAccount.trim()) - .some((serviceAccount) => serviceAccount === name); - - if (!isServiceAccountAllowed) throw new UnauthorizedError(); - } - - if (identityGcpIamAuth.allowedProjects) { - // validate if the project that the service account belongs to is in the list of allowed projects - - const isProjectAllowed = identityGcpIamAuth.allowedProjects - .split(",") - .map((project) => project.trim()) - .some((project) => project === projectId); - - if (!isProjectAllowed) throw new UnauthorizedError(); - } - - const identityAccessToken = await identityGcpIamAuthDAL.transaction(async (tx) => { - const newToken = await identityAccessTokenDAL.create( - { - identityId: identityGcpIamAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityGcpIamAuth.accessTokenTTL, - accessTokenMaxTTL: identityGcpIamAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityGcpIamAuth.accessTokenNumUsesLimit - }, - tx - ); - return newToken; - }); - - const appCfg = getConfig(); - const accessToken = jwt.sign( - { - identityId: identityGcpIamAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - { - expiresIn: - Number(identityAccessToken.accessTokenMaxTTL) === 0 - ? undefined - : Number(identityAccessToken.accessTokenMaxTTL) - } - ); - - return { accessToken, identityGcpIamAuth, identityAccessToken, identityMembershipOrg }; - }; - - const attachGcpIamAuth = async ({ - identityId, - allowedServiceAccounts, - allowedProjects, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps, - actorId, - actorAuthMethod, - actor, - actorOrgId - }: TAttachGcpIamAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); - if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); - if (identityMembershipOrg.identity.authMethod) - throw new BadRequestError({ - message: "Failed to add AWS IAM Auth to already configured identity" - }); - - if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { - throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); - } - - const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - identityMembershipOrg.orgId, - actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); - - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); - const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { - if ( - !plan.ipAllowlisting && - accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && - accessTokenTrustedIp.ipAddress !== "::/0" - ) - throw new BadRequestError({ - message: - "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." - }); - if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) - throw new BadRequestError({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - return extractIPDetails(accessTokenTrustedIp.ipAddress); - }); - - const identityAwsIamAuth = await identityGcpIamAuthDAL.transaction(async (tx) => { - const doc = await identityGcpIamAuthDAL.create( - { - identityId: identityMembershipOrg.identityId, - allowedServiceAccounts, - allowedProjects, - accessTokenMaxTTL, - accessTokenTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) - }, - tx - ); - await identityDAL.updateById( - identityMembershipOrg.identityId, - { - authMethod: IdentityAuthMethod.GCP_IAM_AUTH - }, - tx - ); - return doc; - }); - return { ...identityAwsIamAuth, orgId: identityMembershipOrg.orgId }; - }; - - const updateGcpIamAuth = async ({ - identityId, - allowedServiceAccounts, - allowedProjects, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps, - actorId, - actorAuthMethod, - actor, - actorOrgId - }: TUpdateGcpIamAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); - if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); - if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_IAM_AUTH) - throw new BadRequestError({ - message: "Failed to update GCP IAM Auth" - }); - - const identityGcpIamAuth = await identityGcpIamAuthDAL.findOne({ identityId }); - - if ( - (accessTokenMaxTTL || identityGcpIamAuth.accessTokenMaxTTL) > 0 && - (accessTokenTTL || identityGcpIamAuth.accessTokenMaxTTL) > - (accessTokenMaxTTL || identityGcpIamAuth.accessTokenMaxTTL) - ) { - throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); - } - - const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - identityMembershipOrg.orgId, - actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); - const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { - if ( - !plan.ipAllowlisting && - accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && - accessTokenTrustedIp.ipAddress !== "::/0" - ) - throw new BadRequestError({ - message: - "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." - }); - if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) - throw new BadRequestError({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - return extractIPDetails(accessTokenTrustedIp.ipAddress); - }); - - const updatedGcpIamAuth = await identityGcpIamAuthDAL.updateById(identityGcpIamAuth.id, { - allowedServiceAccounts, - allowedProjects, - accessTokenMaxTTL, - accessTokenTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps - ? JSON.stringify(reformattedAccessTokenTrustedIps) - : undefined - }); - - return { ...updatedGcpIamAuth, orgId: identityMembershipOrg.orgId }; - }; - - const getGcpIamAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpIamAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); - if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); - if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_IAM_AUTH) - throw new BadRequestError({ - message: "The identity does not have GCP IAM Auth attached" - }); - - const gcpIamIdentityAuth = await identityGcpIamAuthDAL.findOne({ identityId }); - - const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - identityMembershipOrg.orgId, - actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - return { ...gcpIamIdentityAuth, orgId: identityMembershipOrg.orgId }; - }; - - return { - login, - attachGcpIamAuth, - updateGcpIamAuth, - getGcpIamAuth - }; -}; diff --git a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-validators.ts b/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-validators.ts deleted file mode 100644 index a581ba9cc..000000000 --- a/backend/src/services/identity-gcp-iam-auth/identity-gcp-iam-auth-validators.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from "zod"; - -export const validateGcpIamAuthField = z - .string() - .trim() - .default("") - .transform((data) => { - if (data === "") return ""; - // Trim each ID and join with ', ' to ensure formatting - return data - .split(",") - .map((id) => id.trim()) - .join(", "); - }); diff --git a/docs/documentation/platform/identities/gcp-iam-auth.mdx b/docs/documentation/platform/identities/gcp-iam-auth.mdx index cf46e36b0..5bc2b5d78 100644 --- a/docs/documentation/platform/identities/gcp-iam-auth.mdx +++ b/docs/documentation/platform/identities/gcp-iam-auth.mdx @@ -1,6 +1,6 @@ --- -title: GCP IAM Auth -description: "Learn how to authenticate with Infisical Google Cloud Platform via IAM service accounts" +title: GCP Auth +description: "Learn how to authenticate with Infisical Google Cloud Platform" --- **GCP IAM Auth** is a GCP-native authentication method for IAM service accounts. @@ -70,6 +70,19 @@ access the Infisical API using the GCP IAM authentication method. To access the Infisical API as the identity, you need to generate a signed JWT token using the `projects.serviceAccounts.signJwt` [API method](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt) and make a request to the `/api/v1/auth/gcp-iam-auth/login` endpoint containing the signed JWT token in exchange for an access token. + + Enable the [IAM Service Account Credentials API](https://console.developers.google.com/apis/api/iamcredentials.googleapis.com/overview) in your project + Cloud Resource Manager API + Identity and Access Management (IAM) API has not been used in project 512461120021 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/iam.googleapis.com/overview?project=512461120021 then retry. + + Add the role `roles/iam.serviceAccountTokenCreator` to the service account. This role includes the required `iam.serviceAccounts.signJwt` permission. + + Must have: `resourcemanager.projects.get` (for the Infisical service account) + +o Infisical uses scope:https://www.googleapis.com/auth/cloud-platform.read-only + + + We provide a few code examples below of how you can authenticate with Infisical to access the [Infisical API](/api-reference/overview/introduction). diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index a97b83861..798a13b88 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -2,6 +2,6 @@ import { IdentityAuthMethod } from "./enums"; export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth", - [IdentityAuthMethod.GCP_IAM_AUTH]: "GCP IAM Auth", + [IdentityAuthMethod.GCP_AUTH]: "GCP Auth", [IdentityAuthMethod.AWS_IAM_AUTH]: "AWS IAM Auth" }; diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index ecc8f1215..be50c72ad 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -1,5 +1,5 @@ export enum IdentityAuthMethod { UNIVERSAL_AUTH = "universal-auth", - GCP_IAM_AUTH = "gcp-iam-auth", + GCP_AUTH = "gcp-auth", AWS_IAM_AUTH = "aws-iam-auth" } diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index 21166816f..bd7347f88 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -2,7 +2,7 @@ export { identityAuthToNameMap } from "./constants"; export { IdentityAuthMethod } from "./enums"; export { useAddIdentityAwsIamAuth, - useAddIdentityGcpIamAuth, + useAddIdentityGcpAuth, useAddIdentityUniversalAuth, useCreateIdentity, useCreateIdentityUniversalAuthClientSecret, @@ -10,12 +10,12 @@ export { useRevokeIdentityUniversalAuthClientSecret, useUpdateIdentity, useUpdateIdentityAwsIamAuth, - useUpdateIdentityGcpIamAuth, + useUpdateIdentityGcpAuth, useUpdateIdentityUniversalAuth } from "./mutations"; export { useGetIdentityAwsIamAuth, - useGetIdentityGcpIamAuth, + useGetIdentityGcpAuth, useGetIdentityUniversalAuth, useGetIdentityUniversalAuthClientSecrets } from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 914264b87..e2cdc8841 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -6,7 +6,7 @@ import { organizationKeys } from "../organization/queries"; import { identitiesKeys } from "./queries"; import { AddIdentityAwsIamAuthDTO, - AddIdentityGcpIamAuthDTO, + AddIdentityGcpAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, CreateIdentityDTO, @@ -16,11 +16,11 @@ import { DeleteIdentityUniversalAuthClientSecretDTO, Identity, IdentityAwsIamAuth, - IdentityGcpIamAuth, + IdentityGcpAuth, IdentityUniversalAuth, UpdateIdentityAwsIamAuthDTO, UpdateIdentityDTO, - UpdateIdentityGcpIamAuthDTO, + UpdateIdentityGcpAuthDTO, UpdateIdentityUniversalAuthDTO } from "./types"; @@ -176,25 +176,31 @@ export const useRevokeIdentityUniversalAuthClientSecret = () => { }); }; -export const useAddIdentityGcpIamAuth = () => { +export const useAddIdentityGcpAuth = () => { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: async ({ identityId, + credentials, + type, allowedServiceAccounts, allowedProjects, + allowedZones, accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, accessTokenTrustedIps }) => { const { - data: { identityGcpIamAuth } - } = await apiRequest.post<{ identityGcpIamAuth: IdentityGcpIamAuth }>( - `/api/v1/auth/gcp-iam-auth/identities/${identityId}`, + data: { identityGcpAuth } + } = await apiRequest.post<{ identityGcpAuth: IdentityGcpAuth }>( + `/api/v1/auth/gcp-auth/identities/${identityId}`, { + credentials, + type, allowedServiceAccounts, allowedProjects, + allowedZones, accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, @@ -202,7 +208,47 @@ export const useAddIdentityGcpIamAuth = () => { } ); - return identityGcpIamAuth; + return identityGcpAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useUpdateIdentityGcpAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + credentials, + type, + allowedServiceAccounts, + allowedProjects, + allowedZones, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityGcpAuth } + } = await apiRequest.patch<{ identityGcpAuth: IdentityGcpAuth }>( + `/api/v1/auth/gcp-auth/identities/${identityId}`, + { + credentials, + type, + allowedServiceAccounts, + allowedProjects, + allowedZones, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityGcpAuth; }, onSuccess: (_, { organizationId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); @@ -246,40 +292,6 @@ export const useAddIdentityAwsIamAuth = () => { }); }; -export const useUpdateIdentityGcpIamAuth = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ - identityId, - allowedServiceAccounts, - allowedProjects, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps - }) => { - const { - data: { identityGcpIamAuth } - } = await apiRequest.patch<{ identityGcpIamAuth: IdentityGcpIamAuth }>( - `/api/v1/auth/gcp-iam-auth/identities/${identityId}`, - { - allowedServiceAccounts, - allowedProjects, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps - } - ); - - return identityGcpIamAuth; - }, - onSuccess: (_, { organizationId }) => { - queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); - } - }); -}; - export const useUpdateIdentityAwsIamAuth = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 623f1b310..62f139f5f 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -5,7 +5,7 @@ import { apiRequest } from "@app/config/request"; import { ClientSecretData, IdentityAwsIamAuth, - IdentityGcpIamAuth, + IdentityGcpAuth, IdentityUniversalAuth } from "./types"; @@ -14,7 +14,7 @@ export const identitiesKeys = { [{ identityId }, "identity-universal-auth"] as const, getIdentityUniversalAuthClientSecrets: (identityId: string) => [{ identityId }, "identity-universal-auth-client-secrets"] as const, - getIdentityGcpIamAuth: (identityId: string) => [{ identityId }, "identity-gcp-iam-auth"] as const, + getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const, getIdentityAwsIamAuth: (identityId: string) => [{ identityId }, "identity-aws-iam-auth"] as const }; @@ -48,17 +48,17 @@ export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) => }); }; -export const useGetIdentityGcpIamAuth = (identityId: string) => { +export const useGetIdentityGcpAuth = (identityId: string) => { return useQuery({ enabled: Boolean(identityId), - queryKey: identitiesKeys.getIdentityGcpIamAuth(identityId), + queryKey: identitiesKeys.getIdentityGcpAuth(identityId), queryFn: async () => { const { - data: { identityGcpIamAuth } - } = await apiRequest.get<{ identityGcpIamAuth: IdentityGcpIamAuth }>( - `/api/v1/auth/gcp-iam-auth/identities/${identityId}` + data: { identityGcpAuth } + } = await apiRequest.get<{ identityGcpAuth: IdentityGcpAuth }>( + `/api/v1/auth/gcp-auth/identities/${identityId}` ); - return identityGcpIamAuth; + return identityGcpAuth; } }); }; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index bb904f462..6cdb05a8c 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -113,21 +113,27 @@ export type UpdateIdentityUniversalAuthDTO = { }[]; }; -export type IdentityGcpIamAuth = { +export type IdentityGcpAuth = { identityId: string; + credentials: string; + type: "iam" | "gce"; allowedServiceAccounts: string; allowedProjects: string; + allowedZones: string; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; accessTokenTrustedIps: IdentityTrustedIp[]; }; -export type AddIdentityGcpIamAuthDTO = { +export type AddIdentityGcpAuthDTO = { organizationId: string; identityId: string; + credentials: string; + type: "iam" | "gce"; allowedServiceAccounts: string; allowedProjects: string; + allowedZones: string; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; @@ -136,11 +142,14 @@ export type AddIdentityGcpIamAuthDTO = { }[]; }; -export type UpdateIdentityGcpIamAuthDTO = { +export type UpdateIdentityGcpAuthDTO = { organizationId: string; identityId: string; + credentials?: string; + type?: "iam" | "gce"; allowedServiceAccounts?: string; allowedProjects?: string; + allowedZones?: string; accessTokenTTL?: number; accessTokenMaxTTL?: number; accessTokenNumUsesLimit?: number; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx index ca62bd617..994672ee1 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx @@ -15,7 +15,7 @@ import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityAwsIamAuthForm } from "./IdentityAwsIamAuthForm"; -import { IdentityGcpIamAuthForm } from "./IdentityGcpIamAuthForm"; +import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; type Props = { @@ -30,7 +30,7 @@ type Props = { const identityAuthMethods = [ { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, { label: "AWS IAM Auth", value: IdentityAuthMethod.AWS_IAM_AUTH }, - { label: "GCP Auth", value: IdentityAuthMethod.GCP_IAM_AUTH } + { label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH } ]; const schema = yup @@ -77,9 +77,9 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog /> ); } - case IdentityAuthMethod.GCP_IAM_AUTH: { + case IdentityAuthMethod.GCP_AUTH: { return ( - ; +export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; @@ -51,7 +60,7 @@ type Props = { }; }; -export const IdentityGcpIamAuthForm = ({ +export const IdentityGcpAuthForm = ({ handlePopUpOpen, handlePopUpToggle, identityAuthMethodData @@ -60,21 +69,25 @@ export const IdentityGcpIamAuthForm = ({ const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); - const { mutateAsync: addMutateAsync } = useAddIdentityGcpIamAuth(); - const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpIamAuth(); + const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); - const { data } = useGetIdentityGcpIamAuth(identityAuthMethodData?.identityId ?? ""); + const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? ""); const { control, handleSubmit, reset, - formState: { isSubmitting } + formState: { isSubmitting }, + watch } = useForm({ - resolver: yupResolver(schema), + resolver: zodResolver(schema), defaultValues: { + credentials: "", + type: "iam", allowedServiceAccounts: "", allowedProjects: "", + allowedZones: "", accessTokenTTL: "2592000", accessTokenMaxTTL: "2592000", accessTokenNumUsesLimit: "0", @@ -82,6 +95,8 @@ export const IdentityGcpIamAuthForm = ({ } }); + const watchedType = watch("type"); + const { fields: accessTokenTrustedIpsFields, append: appendAccessTokenTrustedIp, @@ -91,8 +106,11 @@ export const IdentityGcpIamAuthForm = ({ useEffect(() => { if (data) { reset({ + credentials: data.credentials, + type: data.type, allowedServiceAccounts: data.allowedServiceAccounts, allowedProjects: data.allowedProjects, + allowedZones: data.allowedZones, accessTokenTTL: String(data.accessTokenTTL), accessTokenMaxTTL: String(data.accessTokenMaxTTL), accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), @@ -106,8 +124,10 @@ export const IdentityGcpIamAuthForm = ({ }); } else { reset({ + type: "iam", allowedServiceAccounts: "", allowedProjects: "", + allowedZones: "", accessTokenTTL: "2592000", accessTokenMaxTTL: "2592000", accessTokenNumUsesLimit: "0", @@ -117,8 +137,11 @@ export const IdentityGcpIamAuthForm = ({ }, [data]); const onFormSubmit = async ({ + credentials, + type, allowedServiceAccounts, allowedProjects, + allowedZones, accessTokenTTL, accessTokenMaxTTL, accessTokenNumUsesLimit, @@ -129,10 +152,13 @@ export const IdentityGcpIamAuthForm = ({ if (data) { await updateMutateAsync({ + identityId: identityAuthMethodData.identityId, organizationId: orgId, + credentials, + type, allowedServiceAccounts, allowedProjects, - identityId: identityAuthMethodData.identityId, + allowedZones, accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), @@ -140,10 +166,13 @@ export const IdentityGcpIamAuthForm = ({ }); } else { await addMutateAsync({ - organizationId: orgId, identityId: identityAuthMethodData.identityId, + organizationId: orgId, + credentials, + type, allowedServiceAccounts: allowedServiceAccounts || "", allowedProjects: allowedProjects || "", + allowedZones: allowedZones || "", accessTokenTTL: Number(accessTokenTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), @@ -171,6 +200,41 @@ export const IdentityGcpIamAuthForm = ({ return (
+ ( + +