diff --git a/backend/e2e-test/routes/v1/identity.spec.ts b/backend/e2e-test/routes/v1/identity.spec.ts index ccb530c79..f1cc6cf05 100644 --- a/backend/e2e-test/routes/v1/identity.spec.ts +++ b/backend/e2e-test/routes/v1/identity.spec.ts @@ -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", diff --git a/backend/package-lock.json b/backend/package-lock.json index c3e863dd3..6829ebbc8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -51,7 +51,7 @@ "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", diff --git a/backend/package.json b/backend/package.json index 782c82835..1336478a1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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,7 +156,7 @@ "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", diff --git a/backend/src/db/migrations/20241014084900_identity-multiple-auth-methods.ts b/backend/src/db/migrations/20241014084900_identity-multiple-auth-methods.ts new file mode 100644 index 000000000..35d1984de --- /dev/null +++ b/backend/src/db/migrations/20241014084900_identity-multiple-auth-methods.ts @@ -0,0 +1,73 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +const BATCH_SIZE = 10_000; + +export async function up(knex: Knex): Promise { + 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 { + const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod"); + + if (hasAuthMethodColumnAccessToken) { + await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => { + t.dropColumn("authMethod"); + }); + } +} diff --git a/backend/src/db/schemas/identity-access-tokens.ts b/backend/src/db/schemas/identity-access-tokens.ts index 45445c811..bbff1b88c 100644 --- a/backend/src/db/schemas/identity-access-tokens.ts +++ b/backend/src/db/schemas/identity-access-tokens.ts @@ -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; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 7b48bb6fc..8fb916358 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -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", diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index 544739100..3798d4bf3 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -16,7 +16,7 @@ export async function seed(knex: Knex): Promise { // @ts-ignore id: seedData1.machineIdentity.id, name: seedData1.machineIdentity.name, - authMethod: IdentityAuthMethod.Univeral + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH } ]); const identityUa = await knex(TableName.IdentityUniversalAuth) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 42b581c6e..27f12f07a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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 diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 163b560e2..15e6eabef 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -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 }) }) ) diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index edc270171..52940eb44 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -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() diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index adb070ad9..b2cc3a8e9 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -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 }) }) }) diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index b0bcfda8e..f12bd8c15 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -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" }); diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index c625c1407..a59d1e959 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -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.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[] }); } diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index e9c80419b..6295446bd 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -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; identityAwsAuthDAL: Pick; identityOrgMembershipDAL: Pick; - identityDAL: Pick; licenseService: Pick; permissionService: Pick; }; @@ -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; diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 9267725fb..cc61df65f 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -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; identityAccessTokenDAL: Pick; - identityDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -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; 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 index b78f06fa3..a2a395f63 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -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; identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick; - identityDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -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; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index b324d9046..a99ae7c18 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -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; identityOrgMembershipDAL: Pick; - identityDAL: Pick; orgBotDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -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; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index be09c4777..02440ebe7 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -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; identityAccessTokenDAL: Pick; - identityDAL: Pick; permissionService: Pick; licenseService: Pick; orgBotDAL: Pick; @@ -52,7 +50,6 @@ export type TIdentityOidcAuthServiceFactory = ReturnType { 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 }; }); diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index 3e7ca7946..fd8eaa15d 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -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; 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( + TableName.IdentityUniversalAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityUniversalAuth}.identityId` + ) + .leftJoin( + TableName.IdentityGcpAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityGcpAuth}.identityId` + ) + .leftJoin( + TableName.IdentityAwsAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityAwsAuth}.identityId` + ) + .leftJoin( + TableName.IdentityKubernetesAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .leftJoin( + TableName.IdentityOidcAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityOidcAuth}.identityId` + ) + .leftJoin( + TableName.IdentityAzureAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityAzureAuth}.identityId` + ) + .leftJoin( + 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, diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index aa02cd94a..39f2f6589 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -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; identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick< TIdentityAccessTokenDALFactory, - "create" | "find" | "update" | "findById" | "findOne" | "updateById" + "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete" >; permissionService: Pick; licenseService: Pick; @@ -46,7 +44,7 @@ export type TIdentityTokenAuthServiceFactory = ReturnType { 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 }; }; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index aed9425d2..b456c1647 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -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; permissionService: Pick; licenseService: Pick; }; @@ -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, diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts new file mode 100644 index 000000000..49cf4d119 --- /dev/null +++ b/backend/src/services/identity/identity-fns.ts @@ -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[]; +}; diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 857f24562..bbdf96a2b 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -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; export const identityOrgDALFactory = (db: TDbClient) => { @@ -15,14 +28,73 @@ export const identityOrgDALFactory = (db: TDbClient) => { const findOne = async (filter: Partial, 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( + TableName.IdentityUniversalAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityUniversalAuth}.identityId` + ) + .leftJoin( + TableName.IdentityGcpAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityGcpAuth}.identityId` + ) + .leftJoin( + TableName.IdentityAwsAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityAwsAuth}.identityId` + ) + .leftJoin( + TableName.IdentityKubernetesAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .leftJoin( + TableName.IdentityOidcAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityOidcAuth}.identityId` + ) + .leftJoin( + TableName.IdentityAzureAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityAzureAuth}.identityId` + ) + .leftJoin( + 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(paginatedIdentity) .leftJoin(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( + TableName.IdentityUniversalAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityUniversalAuth}.identityId` + ) + .leftJoin( + TableName.IdentityGcpAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityGcpAuth}.identityId` + ) + .leftJoin( + TableName.IdentityAwsAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityAwsAuth}.identityId` + ) + .leftJoin( + TableName.IdentityKubernetesAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .leftJoin( + TableName.IdentityOidcAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityOidcAuth}.identityId` + ) + .leftJoin( + TableName.IdentityAzureAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityAzureAuth}.identityId` + ) + .leftJoin( + 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: [ diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index cb244898f..fffcbacc2 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -93,7 +93,7 @@ export const identityServiceFactory = ({ tx ); } - return newIdentity; + return { ...newIdentity, authMethods: [] }; }); await licenseService.updateSubscriptionOrgMemberCount(orgId); diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index ce3f03d01..cfc70450b 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -127,8 +127,7 @@ export const SelectItem = forwardRef( 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} diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 140d49536..559a01974 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -12,7 +12,7 @@ export type IdentityTrustedIp = { export type Identity = { id: string; name: string; - authMethod?: IdentityAuthMethod; + authMethods: IdentityAuthMethod[]; createdAt: string; updatedAt: string; }; diff --git a/frontend/src/views/Org/IdentityPage/IdentityPage.tsx b/frontend/src/views/Org/IdentityPage/IdentityPage.tsx index 3d863f2d3..0e2a88a75 100644 --- a/frontend/src/views/Org/IdentityPage/IdentityPage.tsx +++ b/frontend/src/views/Org/IdentityPage/IdentityPage.tsx @@ -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 )} @@ -247,6 +255,8 @@ export const IdentityPage = withPermission(
diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx index be7c744ce..264294429 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx @@ -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 ? (

Authentication

+ {(isAllowed) => { return ( - + - + ); }}
-
-
-

Auth Method

+ {data.identity.authMethods.length > 0 ? ( + <> +
+
+

Auth Method

+
+
+
+ +
+
+ + { + handlePopUpOpen("identityAuthMethod", { + identityId, + name: data.identity.name, + authMethod: selectedAuthMethod, + allAuthMethods: data.identity.authMethods + }); + }} + ariaLabel="copy icon" + variant="plain" + className="group relative" + > + + + {" "} +
+
+
+ {selectedAuthMethod === IdentityAuthMethod.UNIVERSAL_AUTH && ( + + )} + {selectedAuthMethod === IdentityAuthMethod.TOKEN_AUTH && ( + + )} + + ) : ( +
+

+ No authentication methods configured. Get started by creating a new auth method. +

+
-

- {data.identity.authMethod - ? identityAuthToNameMap[data.identity.authMethod] - : "Not configured"} -

-
- {data.identity.authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && ( - - )} - {data.identity.authMethod === IdentityAuthMethod.TOKEN_AUTH && ( - )}
) : ( 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 c924b4b66..70128fa1a 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 @@ -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() - .oneOf(Object.values(IdentityAuthMethod)) - .required("Auth method is required") - }) - .required(); - -export type FormData = yup.InferType; - 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(null); const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod; - const { control, watch, setValue, reset } = useForm({ - 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 ( - - ); - } - case IdentityAuthMethod.KUBERNETES_AUTH: { - return ( - - ); - } - case IdentityAuthMethod.GCP_AUTH: { - return ( - - ); - } - case IdentityAuthMethod.AZURE_AUTH: { - return ( - - ); - } - case IdentityAuthMethod.UNIVERSAL_AUTH: { - return ( - - ); - } - case IdentityAuthMethod.OIDC_AUTH: { - return ( - - ); - } - case IdentityAuthMethod.TOKEN_AUTH: { - return ( - - ); - } - default: { - return
; - } - } - }; + const isSelectedAuthAlreadyConfigured = + popUp?.identityAuthMethod?.data?.allAuthMethods?.includes(selectedAuthMethod); return ( - ( - - - - )} - /> - {renderIdentityAuthForm()} - handlePopUpToggle("upgradePlan", isOpen)} - text="You can use IP allowlisting if you switch to Infisical's Pro plan." - /> - handlePopUpToggle("revokeAuthMethod", isOpen)} - deleteKey="confirm" - buttonText="Remove" - onDeleteApproved={() => onRevokeAuthMethodSubmit(identityAuthMethodData.authMethod!)} + diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx new file mode 100644 index 000000000..8852af872 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx @@ -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; + 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() + .oneOf(Object.values(IdentityAuthMethod)) + .required("Auth method is required") + }) + .required(); + +export type FormData = yup.InferType; + +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({ + 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.UNIVERSAL_AUTH]: { + revokeMethod: revokeUniversalAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.OIDC_AUTH]: { + revokeMethod: revokeOidcAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.TOKEN_AUTH]: { + revokeMethod: revokeTokenAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.AZURE_AUTH]: { + revokeMethod: revokeAzureAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.GCP_AUTH]: { + revokeMethod: revokeGcpAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.KUBERNETES_AUTH]: { + revokeMethod: revokeKubernetesAuth, + render: () => ( + + ) + }, + + [IdentityAuthMethod.AWS_AUTH]: { + revokeMethod: revokeAwsAuth, + render: () => ( + + ) + } + }; + + const isAlreadyConfigured = useCallback((method: IdentityAuthMethod) => { + return identityAuthMethodData?.configuredAuthMethods?.includes(method); + }, []); + + const selectedMethodItem = methodMap[identityAuthMethodData.authMethod!]; + + return ( + <> + ( + + + + )} + /> + {selectedMethodItem?.render ? selectedMethodItem.render() :
} + handlePopUpToggle("upgradePlan", isOpen)} + text="You can use IP allowlisting if you switch to Infisical's Pro plan." + /> + 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" + }); + } + }} + /> + + ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx index 781a78125..a254397f4 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx @@ -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; }; 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 ( - <> -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - {accessTokenTrustedIpsFields.map(({ id }, index) => ( -
- { - return ( - - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> - - ); - }} + + ( + + - { - if (subscription?.ipAllowlisting) { - removeAccessTokenTrustedIp(index); - return; - } + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
-
+ ))} +
+ +
+
+
+ + +
-
-
- {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( - - ) : ( - - )} - -
- {isCurrentAuthMethod && ( - - )} -
- - internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)} - deleteKey="confirm" - buttonText="Overwrite" - onDeleteApproved={async () => { - await revokeAuth(initialAuthMethod); - handleSubmit(onFormSubmit)(); - }} - /> - + {isUpdate && ( + + )} +
+ ); }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx index a2a6387fb..0acc7ec5a 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx @@ -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; }; 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({ 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 ( - <> -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - {accessTokenTrustedIpsFields.map(({ id }, index) => ( -
- { - return ( - - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> - - ); - }} - /> - { - if (subscription?.ipAllowlisting) { - removeAccessTokenTrustedIp(index); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
-
+ ))} +
+ +
+
+
+ + +
-
-
- {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( - - ) : ( - - )} - -
- {isCurrentAuthMethod && ( - - )} -
- - 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 && ( + + )} +
+ ); }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx index 81dd51d22..fe0c60f27 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx @@ -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; }; 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 ( - <> -
- ( - - - - )} - /> - ( - + ( + + - - )} - /> - {watchedType === "gce" && ( - ( - - - - )} - /> + + GCP ID Token Auth (Recommended) + + + GCP IAM Auth + + + )} - {watchedType === "gce" && ( - ( - - - - )} - /> - )} - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - {accessTokenTrustedIpsFields.map(({ id }, index) => ( -
- { - return ( - - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> - - ); - }} + /> + ( + + - { - if (subscription?.ipAllowlisting) { - removeAccessTokenTrustedIp(index); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" + + )} + /> + {watchedType === "gce" && ( + ( + - - -
- ))} -
- +
+
+
+ + +
-
-
- {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( - - ) : ( - - )} - -
- {isCurrentAuthMethod && ( - - )} -
- - internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)} - deleteKey="confirm" - onDeleteApproved={async () => { - await revokeAuth(initialAuthMethod); - handleSubmit(onFormSubmit)(); - }} - /> - + {isUpdate && ( + + )} +
+ ); }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 3378fbe3d..c5474a611 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -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; }; 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({ 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 ( - <> -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - -