From 93be4095c0e5b354afda4acf6b6e9ce8141ecabb Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 2 Jul 2024 15:05:57 -0700 Subject: [PATCH 1/5] Finish preliminary token auth method --- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 8 + .../20240702175124_identity-token-auth.ts | 24 ++ .../src/db/schemas/identity-token-auths.ts | 23 ++ backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/models.ts | 2 + .../ee/services/audit-log/audit-log-types.ts | 54 +++ backend/src/server/routes/index.ts | 12 + .../routes/v1/identity-token-auth-router.ts | 312 +++++++++++++++++ backend/src/server/routes/v1/index.ts | 2 + .../identity-token-auth-dal.ts | 10 + .../identity-token-auth-service.ts | 331 ++++++++++++++++++ .../identity-token-auth-types.ts | 29 ++ .../src/hooks/api/identities/constants.tsx | 1 + frontend/src/hooks/api/identities/enums.tsx | 1 + frontend/src/hooks/api/identities/index.tsx | 10 +- .../src/hooks/api/identities/mutations.tsx | 84 ++++- frontend/src/hooks/api/identities/queries.tsx | 19 +- frontend/src/hooks/api/identities/types.ts | 42 +++ .../IdentityAuthMethodModal.tsx | 12 + .../IdentityKubernetesAuthForm.tsx | 12 +- .../IdentitySection/IdentitySection.tsx | 9 +- .../IdentitySection/IdentityTable.tsx | 38 +- .../IdentitySection/IdentityTokenAuthForm.tsx | 262 ++++++++++++++ .../IdentityTokenAuthTokenModal.tsx | 53 +++ 25 files changed, 1336 insertions(+), 17 deletions(-) create mode 100644 backend/src/db/migrations/20240702175124_identity-token-auth.ts create mode 100644 backend/src/db/schemas/identity-token-auths.ts create mode 100644 backend/src/server/routes/v1/identity-token-auth-router.ts create mode 100644 backend/src/services/identity-token-auth/identity-token-auth-dal.ts create mode 100644 backend/src/services/identity-token-auth/identity-token-auth-service.ts create mode 100644 backend/src/services/identity-token-auth/identity-token-auth-types.ts create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthTokenModal.tsx diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index c7e58fb099..33ee885cc4 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -42,6 +42,7 @@ import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-a import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; +import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; @@ -127,6 +128,7 @@ declare module "fastify" { identity: TIdentityServiceFactory; identityAccessToken: TIdentityAccessTokenServiceFactory; identityProject: TIdentityProjectServiceFactory; + identityTokenAuth: TIdentityTokenAuthServiceFactory; identityUa: TIdentityUaServiceFactory; identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory; identityGcpAuth: TIdentityGcpAuthServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 82ca897421..9d54335bbc 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -104,6 +104,9 @@ import { TIdentityProjectMemberships, TIdentityProjectMembershipsInsert, TIdentityProjectMembershipsUpdate, + TIdentityTokenAuths, + TIdentityTokenAuthsInsert, + TIdentityTokenAuthsUpdate, TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, TIdentityUaClientSecretsUpdate, @@ -450,6 +453,11 @@ declare module "knex/types/tables" { TIntegrationAuthsUpdate >; [TableName.Identity]: KnexOriginal.CompositeTableType; + [TableName.IdentityTokenAuth]: KnexOriginal.CompositeTableType< + TIdentityTokenAuths, + TIdentityTokenAuthsInsert, + TIdentityTokenAuthsUpdate + >; [TableName.IdentityUniversalAuth]: KnexOriginal.CompositeTableType< TIdentityUniversalAuths, TIdentityUniversalAuthsInsert, diff --git a/backend/src/db/migrations/20240702175124_identity-token-auth.ts b/backend/src/db/migrations/20240702175124_identity-token-auth.ts new file mode 100644 index 0000000000..66ca55b491 --- /dev/null +++ b/backend/src/db/migrations/20240702175124_identity-token-auth.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + await knex.schema.createTable(TableName.IdentityTokenAuth, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable(); + t.jsonb("accessTokenTrustedIps").notNullable(); + t.timestamps(true, true, true); + t.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + }); + + await createOnUpdateTrigger(knex, TableName.IdentityTokenAuth); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityTokenAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityTokenAuth); +} diff --git a/backend/src/db/schemas/identity-token-auths.ts b/backend/src/db/schemas/identity-token-auths.ts new file mode 100644 index 0000000000..0f3c8c9ff6 --- /dev/null +++ b/backend/src/db/schemas/identity-token-auths.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const IdentityTokenAuthsSchema = z.object({ + id: z.string().uuid(), + accessTokenTTL: z.coerce.number().default(7200), + accessTokenMaxTTL: z.coerce.number().default(7200), + accessTokenNumUsesLimit: z.coerce.number().default(0), + accessTokenTrustedIps: z.unknown(), + createdAt: z.date(), + updatedAt: z.date(), + identityId: z.string().uuid() +}); + +export type TIdentityTokenAuths = z.infer; +export type TIdentityTokenAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityTokenAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index af8c2070a1..bce99dfea7 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -32,6 +32,7 @@ export * from "./identity-org-memberships"; export * from "./identity-project-additional-privilege"; export * from "./identity-project-membership-role"; export * from "./identity-project-memberships"; +export * from "./identity-token-auths"; export * from "./identity-ua-client-secrets"; export * from "./identity-universal-auths"; export * from "./incident-contacts"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index bdc574bcb8..1dba712094 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -53,6 +53,7 @@ export enum TableName { Webhook = "webhooks", Identity = "identities", IdentityAccessToken = "identity_access_tokens", + IdentityTokenAuth = "identity_token_auths", IdentityUniversalAuth = "identity_universal_auths", IdentityKubernetesAuth = "identity_kubernetes_auths", IdentityGcpAuth = "identity_gcp_auths", @@ -161,6 +162,7 @@ export enum ProjectUpgradeStatus { } export enum IdentityAuthMethod { + TOKEN_AUTH = "token-auth", Univeral = "universal-auth", KUBERNETES_AUTH = "kubernetes-auth", GCP_AUTH = "gcp-auth", diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 0c6ff51c8e..802fc628df 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -66,6 +66,11 @@ export enum EventType { UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth", GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth", REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth", + CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth", + ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", + UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", + GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth", + REVOKE_IDENTITY_TOKEN_AUTH = "revoke-identity-token-auth", LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth", ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth", UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth", @@ -447,6 +452,50 @@ interface DeleteIdentityUniversalAuthEvent { }; } +interface CreateTokenIdentityTokenAuthEvent { + type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityTokenAuthEvent { + type: EventType.ADD_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface UpdateIdentityTokenAuthEvent { + type: EventType.UPDATE_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityTokenAuthEvent { + type: EventType.GET_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + }; +} + +interface DeleteIdentityTokenAuthEvent { + type: EventType.REVOKE_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + }; +} + interface LoginIdentityKubernetesAuthEvent { type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH; metadata: { @@ -1054,6 +1103,11 @@ export type Event = | UpdateIdentityUniversalAuthEvent | DeleteIdentityUniversalAuthEvent | GetIdentityUniversalAuthEvent + | CreateTokenIdentityTokenAuthEvent + | AddIdentityTokenAuthEvent + | UpdateIdentityTokenAuthEvent + | GetIdentityTokenAuthEvent + | DeleteIdentityTokenAuthEvent | LoginIdentityKubernetesAuthEvent | DeleteIdentityKubernetesAuthEvent | AddIdentityKubernetesAuthEvent diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 54c5e51d66..540ff84ce9 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -105,6 +105,8 @@ import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kub import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal"; import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; +import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal"; +import { identityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal"; import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal"; import { identityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; @@ -233,6 +235,7 @@ export const registerRoutes = async ( const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db); + const identityTokenAuthDAL = identityTokenAuthDALFactory(db); const identityUaDAL = identityUaDALFactory(db); const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); @@ -829,6 +832,14 @@ export const registerRoutes = async ( permissionService, identityProjectDAL }); + const identityTokenAuthService = identityTokenAuthServiceFactory({ + identityTokenAuthDAL, + identityDAL, + identityOrgMembershipDAL, + identityAccessTokenDAL, + permissionService, + licenseService + }); const identityUaService = identityUaServiceFactory({ identityOrgMembershipDAL, permissionService, @@ -967,6 +978,7 @@ export const registerRoutes = async ( identity: identityService, identityAccessToken: identityAccessTokenService, identityProject: identityProjectService, + identityTokenAuth: identityTokenAuthService, identityUa: identityUaService, identityKubernetesAuth: identityKubernetesAuthService, identityGcpAuth: identityGcpAuthService, diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts new file mode 100644 index 0000000000..e373198033 --- /dev/null +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -0,0 +1,312 @@ +import { z } from "zod"; + +import { IdentityTokenAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; + +export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/token-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Attach Token Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim() + }), + body: z.object({ + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + accessTokenTTL: z + .number() + .int() + .min(1) + .refine((value) => value !== 0, { + message: "accessTokenTTL must have a non zero number" + }) + .default(2592000), + accessTokenMaxTTL: z + .number() + .int() + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .default(2592000), + accessTokenNumUsesLimit: z.number().int().min(0).default(0) + }), + response: { + 200: z.object({ + identityTokenAuth: IdentityTokenAuthsSchema + }) + } + }, + handler: async (req) => { + const identityTokenAuth = await server.services.identityTokenAuth.attachTokenAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityTokenAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: identityTokenAuth.identityId, + accessTokenTTL: identityTokenAuth.accessTokenTTL, + accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityTokenAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit + } + } + }); + + return { + identityTokenAuth + }; + } + }); + + server.route({ + method: "PATCH", + url: "/token-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Update Token Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim() + }), + body: z.object({ + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional(), + accessTokenTTL: z.number().int().min(0).optional(), + accessTokenNumUsesLimit: z.number().int().min(0).optional(), + accessTokenMaxTTL: z + .number() + .int() + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .optional() + }), + response: { + 200: z.object({ + identityTokenAuth: IdentityTokenAuthsSchema + }) + } + }, + handler: async (req) => { + const identityTokenAuth = await server.services.identityTokenAuth.updateTokenAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityTokenAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: identityTokenAuth.identityId, + accessTokenTTL: identityTokenAuth.accessTokenTTL, + accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityTokenAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit + } + } + }); + + return { + identityTokenAuth + }; + } + }); + + server.route({ + method: "GET", + url: "/token-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Retrieve Token Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string() + }), + response: { + 200: z.object({ + identityTokenAuth: IdentityTokenAuthsSchema + }) + } + }, + handler: async (req) => { + const identityTokenAuth = await server.services.identityTokenAuth.getTokenAuth({ + identityId: req.params.identityId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityTokenAuth.orgId, + event: { + type: EventType.GET_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: identityTokenAuth.identityId + } + } + }); + + return { identityTokenAuth }; + } + }); + + server.route({ + method: "DELETE", + url: "/token-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete Token Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string() + }), + response: { + 200: z.object({ + identityTokenAuth: IdentityTokenAuthsSchema + }) + } + }, + handler: async (req) => { + const identityTokenAuth = await server.services.identityTokenAuth.revokeIdentityTokenAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityTokenAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: identityTokenAuth.identityId + } + } + }); + + return { identityTokenAuth }; + } + }); + + server.route({ + method: "POST", + url: "/token-auth/identities/:identityId/token", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Create token for identity with Token Auth configured", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string() + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + handler: async (req) => { + const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityTokenAuth.createTokenTokenAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg.orgId, + event: { + type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: identityTokenAuth.identityId, + identityAccessTokenId: identityAccessToken.id + } + } + }); + + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityTokenAuth.accessTokenTTL, + accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL + }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index c2969b382e..6c6ff3b513 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -9,6 +9,7 @@ import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityRouter } from "./identity-router"; +import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; import { registerIdentityUaRouter } from "./identity-universal-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationRouter } from "./integration-router"; @@ -33,6 +34,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register( async (authRouter) => { await authRouter.register(registerAuthRoutes); + await authRouter.register(registerIdentityTokenAuthRouter); await authRouter.register(registerIdentityUaRouter); await authRouter.register(registerIdentityKubernetesRouter); await authRouter.register(registerIdentityGcpAuthRouter); diff --git a/backend/src/services/identity-token-auth/identity-token-auth-dal.ts b/backend/src/services/identity-token-auth/identity-token-auth-dal.ts new file mode 100644 index 0000000000..64f5c9e7a2 --- /dev/null +++ b/backend/src/services/identity-token-auth/identity-token-auth-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityTokenAuthDALFactory = ReturnType; + +export const identityTokenAuthDALFactory = (db: TDbClient) => { + const tokenAuthOrm = ormify(db, TableName.IdentityTokenAuth); + return tokenAuthOrm; +}; 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 new file mode 100644 index 0000000000..4b94447193 --- /dev/null +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -0,0 +1,331 @@ +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; + +import { IdentityAuthMethod } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +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"; +import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal"; +import { + TAttachTokenAuthDTO, + TCreateTokenTokenAuthDTO, + TGetTokenAuthDTO, + TRevokeTokenAuthDTO, + TUpdateTokenAuthDTO +} from "./identity-token-auth-types"; + +type TIdentityTokenAuthServiceFactoryDep = { + identityTokenAuthDAL: TIdentityTokenAuthDALFactory; + identityDAL: Pick; + identityOrgMembershipDAL: Pick; + identityAccessTokenDAL: Pick; + permissionService: Pick; + licenseService: Pick; +}; + +export type TIdentityTokenAuthServiceFactory = ReturnType; + +export const identityTokenAuthServiceFactory = ({ + identityTokenAuthDAL, + identityDAL, + identityOrgMembershipDAL, + identityAccessTokenDAL, + permissionService, + licenseService +}: TIdentityTokenAuthServiceFactoryDep) => { + const attachTokenAuth = async ({ + identityId, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TAttachTokenAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity.authMethod) + throw new BadRequestError({ + message: "Failed to add Token Auth to already configured identity" + }); + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const identityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { + const doc = await identityTokenAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + await identityDAL.updateById( + identityMembershipOrg.identityId, + { + authMethod: IdentityAuthMethod.TOKEN_AUTH + }, + tx + ); + return doc; + }); + return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; + }; + + const updateTokenAuth = async ({ + identityId, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateTokenAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) + throw new BadRequestError({ + message: "Failed to update Token Auth" + }); + + const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityTokenAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityTokenAuth.accessTokenMaxTTL) > + (accessTokenMaxTTL || identityTokenAuth.accessTokenMaxTTL) + ) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const updatedTokenAuth = await identityTokenAuthDAL.updateById(identityTokenAuth.id, { + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }); + + return { + ...updatedTokenAuth, + orgId: identityMembershipOrg.orgId + }; + }; + + const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) + throw new BadRequestError({ + message: "The identity does not have Token Auth attached" + }); + + const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + + return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; + }; + + const revokeIdentityTokenAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeTokenAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) + throw new BadRequestError({ + message: "The identity does not have Token Auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to revoke Token Auth of identity with more privileged role" + }); + + const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { + const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx); + await identityDAL.updateById(identityId, { authMethod: null }, tx); + return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityTokenAuth; + }; + + const createTokenTokenAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TCreateTokenTokenAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) + throw new BadRequestError({ + message: "The identity does not have Token Auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to revoke Token Auth of identity with more privileged role" + }); + + const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); + + const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityTokenAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityTokenAuth.accessTokenTTL, + accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityTokenAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + { + expiresIn: + Number(identityAccessToken.accessTokenMaxTTL) === 0 + ? undefined + : Number(identityAccessToken.accessTokenMaxTTL) + } + ); + + return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg }; + }; + + return { + attachTokenAuth, + updateTokenAuth, + getTokenAuth, + revokeIdentityTokenAuth, + createTokenTokenAuth + }; +}; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts new file mode 100644 index 0000000000..3115b0237a --- /dev/null +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -0,0 +1,29 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TAttachTokenAuthDTO = { + identityId: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; +} & Omit; + +export type TUpdateTokenAuthDTO = { + identityId: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetTokenAuthDTO = { + identityId: string; +} & Omit; + +export type TRevokeTokenAuthDTO = { + identityId: string; +} & Omit; + +export type TCreateTokenTokenAuthDTO = { + identityId: string; +} & Omit; diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index 51495d4f24..bfa5147451 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -1,6 +1,7 @@ import { IdentityAuthMethod } from "./enums"; export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { + [IdentityAuthMethod.TOKEN_AUTH]: "Token Auth", [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth", [IdentityAuthMethod.KUBERNETES_AUTH]: "Kubernetes Auth", [IdentityAuthMethod.GCP_AUTH]: "GCP Auth", diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index 66af910939..4ad5337f16 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -1,4 +1,5 @@ export enum IdentityAuthMethod { + TOKEN_AUTH = "token-auth", UNIVERSAL_AUTH = "universal-auth", KUBERNETES_AUTH = "kubernetes-auth", GCP_AUTH = "gcp-auth", diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index 41b03669b9..e615ae561b 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -5,9 +5,11 @@ export { useAddIdentityAzureAuth, useAddIdentityGcpAuth, useAddIdentityKubernetesAuth, + useAddIdentityTokenAuth, useAddIdentityUniversalAuth, useCreateIdentity, useCreateIdentityUniversalAuthClientSecret, + useCreateTokenIdentityTokenAuth, useDeleteIdentity, useRevokeIdentityUniversalAuthClientSecret, useUpdateIdentity, @@ -15,13 +17,13 @@ export { useUpdateIdentityAzureAuth, useUpdateIdentityGcpAuth, useUpdateIdentityKubernetesAuth, - useUpdateIdentityUniversalAuth -} from "./mutations"; + useUpdateIdentityTokenAuth, + useUpdateIdentityUniversalAuth} from "./mutations"; export { useGetIdentityAwsAuth, useGetIdentityAzureAuth, useGetIdentityGcpAuth, useGetIdentityKubernetesAuth, + useGetIdentityTokenAuth, useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets -} from "./queries"; + useGetIdentityUniversalAuthClientSecrets} from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index cb1fe4c170..0d59ec9991 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -9,11 +9,14 @@ import { AddIdentityAzureAuthDTO, AddIdentityGcpAuthDTO, AddIdentityKubernetesAuthDTO, + AddIdentityTokenAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, CreateIdentityDTO, CreateIdentityUniversalAuthClientSecretDTO, CreateIdentityUniversalAuthClientSecretRes, + CreateTokenIdentityTokenAuthDTO, + CreateTokenIdentityTokenAuthRes, DeleteIdentityDTO, DeleteIdentityUniversalAuthClientSecretDTO, Identity, @@ -21,14 +24,15 @@ import { IdentityAzureAuth, IdentityGcpAuth, IdentityKubernetesAuth, + IdentityTokenAuth, IdentityUniversalAuth, UpdateIdentityAwsAuthDTO, UpdateIdentityAzureAuthDTO, UpdateIdentityDTO, UpdateIdentityGcpAuthDTO, UpdateIdentityKubernetesAuthDTO, - UpdateIdentityUniversalAuthDTO -} from "./types"; + UpdateIdentityTokenAuthDTO, + UpdateIdentityUniversalAuthDTO} from "./types"; export const useCreateIdentity = () => { const queryClient = useQueryClient(); @@ -485,3 +489,79 @@ export const useUpdateIdentityKubernetesAuth = () => { } }); }; + +export const useAddIdentityTokenAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityTokenAuth } + } = await apiRequest.post<{ identityTokenAuth: IdentityTokenAuth }>( + `/api/v1/auth/token-auth/identities/${identityId}`, + { + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityTokenAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useUpdateIdentityTokenAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityTokenAuth } + } = await apiRequest.patch<{ identityTokenAuth: IdentityTokenAuth }>( + `/api/v1/auth/token-auth/identities/${identityId}`, + { + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityTokenAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useCreateTokenIdentityTokenAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { data } = await apiRequest.post( + `/api/v1/auth/token-auth/identities/${identityId}/token` + ); + + return data; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index eb04227eb6..4b74f086ce 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -8,6 +8,7 @@ import { IdentityAzureAuth, IdentityGcpAuth, IdentityKubernetesAuth, + IdentityTokenAuth, IdentityUniversalAuth} from "./types"; export const identitiesKeys = { @@ -19,7 +20,8 @@ export const identitiesKeys = { [{ identityId }, "identity-kubernetes-auth"] as const, getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const, getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const, - getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const + getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const, + getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const }; export const useGetIdentityUniversalAuth = (identityId: string) => { @@ -111,3 +113,18 @@ export const useGetIdentityKubernetesAuth = (identityId: string) => { } }); }; + +export const useGetIdentityTokenAuth = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityTokenAuth(identityId), + queryFn: async () => { + const { + data: { identityTokenAuth } + } = await apiRequest.get<{ identityTokenAuth: IdentityTokenAuth }>( + `/api/v1/auth/token-auth/identities/${identityId}` + ); + return identityTokenAuth; + } + }); +}; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 80d066c720..117fcbb88a 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -311,3 +311,45 @@ export type DeleteIdentityUniversalAuthClientSecretDTO = { identityId: string; clientSecretId: string; }; + +export type IdentityTokenAuth = { + identityId: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityTokenAuthDTO = { + organizationId: string; + identityId: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityTokenAuthDTO = { + organizationId: string; + identityId: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + +export type CreateTokenIdentityTokenAuthDTO = { + identityId: string; + organizationId: string; +}; + +export type CreateTokenIdentityTokenAuthRes = { + accessToken: string; + tokenType: string; + expiresIn: number; + accessTokenMaxTTL: number; +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx index a1dc5d678a..bee7b65a6b 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 @@ -18,6 +18,7 @@ import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm"; import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; +import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; type Props = { @@ -30,6 +31,7 @@ type Props = { }; 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 }, @@ -117,6 +119,16 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog /> ); } + case IdentityAuthMethod.TOKEN_AUTH: { + return ( + + ); + } + default: { return
; } 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 142b25dc30..abb541cfb5 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 @@ -17,8 +17,6 @@ import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; -// TODO: Add CA cert and token reviewer JWT fields - const schema = z .object({ kubernetesHost: z.string(), @@ -77,11 +75,11 @@ export const IdentityKubernetesAuthForm = ({ } = useForm({ resolver: zodResolver(schema), defaultValues: { - kubernetesHost: "", // TODO + kubernetesHost: "", tokenReviewerJwt: "", - allowedNames: "", // TODO - allowedNamespaces: "", // TODO - allowedAudience: "", // TODO + allowedNames: "", + allowedNamespaces: "", + allowedAudience: "", caCert: "", accessTokenTTL: "2592000", accessTokenMaxTTL: "2592000", @@ -118,7 +116,7 @@ export const IdentityKubernetesAuthForm = ({ }); } else { reset({ - kubernetesHost: "", // TODO + kubernetesHost: "", tokenReviewerJwt: "", allowedNames: "", allowedNamespaces: "", diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index b8b1f1708c..c2acc8b4b3 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -18,6 +18,7 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal"; import { IdentityModal } from "./IdentityModal"; import { IdentityTable } from "./IdentityTable"; +import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal"; export const IdentitySection = withPermission( @@ -33,7 +34,8 @@ export const IdentitySection = withPermission( "deleteIdentity", "universalAuthClientSecret", "deleteUniversalAuthClientSecret", - "upgradePlan" + "upgradePlan", + "tokenAuthToken" ] as const); const isMoreIdentitiesAllowed = subscription?.identityLimit @@ -122,6 +124,11 @@ export const IdentitySection = withPermission( handlePopUpOpen={handlePopUpOpen} handlePopUpToggle={handlePopUpToggle} /> + , data?: { identityId?: string; @@ -50,6 +60,7 @@ type Props = { name: string; slug: string; }; + accessToken?: string; } ) => void; }; @@ -59,6 +70,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { const orgId = currentOrg?.id || ""; const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); + const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); const { data, isLoading } = useGetIdentityMembershipOrgs(orgId); const { data: roles } = useGetOrgRoles(orgId); @@ -139,6 +151,28 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { {authMethod ? identityAuthToNameMap[authMethod] : "Not configured"}
+ {authMethod === IdentityAuthMethod.TOKEN_AUTH && ( + + { + const newTokenData = await createToken({ + identityId: id, + organizationId: orgId + }); + + handlePopUpOpen("tokenAuthToken", { + accessToken: newTokenData.accessToken + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + > + + + + )} {authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && ( ; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + state?: boolean + ) => void; + identityAuthMethodData: { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + }; +}; + +export const IdentityTokenAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth(); + + const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? ""); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + const onFormSubmit = async ({ + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityAuthMethodData) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${ + identityAuthMethodData?.authMethod ? "updated" : "configured" + } auth method`, + type: "success" + }); + + reset(); + } catch (err) { + createNotification({ + text: `Failed to ${identityAuthMethodData?.authMethod ? "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; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+ + +
+ + ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthTokenModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthTokenModal.tsx new file mode 100644 index 0000000000..ee421c6a95 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthTokenModal.tsx @@ -0,0 +1,53 @@ +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { IconButton, Modal, ModalContent, Tooltip } from "@app/components/v2"; +import { useTimedReset } from "@app/hooks"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + popUp: UsePopUpState<["tokenAuthToken"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["tokenAuthToken"]>, state?: boolean) => void; +}; + +export const IdentityTokenAuthTokenModal = ({ popUp, handlePopUpToggle }: Props) => { + const [copyTextAccessToken, isCopyingAccessToken, setCopyTextAccessToken] = useTimedReset( + { + initialState: "Copy to clipboard" + } + ); + + const popUpData = popUp?.tokenAuthToken?.data as { + accessToken: string; + }; + + return ( + { + handlePopUpToggle("tokenAuthToken", isOpen); + }} + > + + {popUpData?.accessToken && ( +
+

{popUpData.accessToken}

+ + { + navigator.clipboard.writeText(popUpData.accessToken); + setCopyTextAccessToken("Copied"); + }} + > + + + +
+ )} +
+
+ ); +}; From 4cfe564f3d2f2b447955a128d2b5610e2cd11cc6 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 2 Jul 2024 15:15:45 -0700 Subject: [PATCH 2/5] Fix lint issues --- .../identity-token-auth/identity-token-auth-service.ts | 5 ++++- .../components/IdentitySection/IdentitySection.tsx | 6 +----- 2 files changed, 5 insertions(+), 6 deletions(-) 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 4b94447193..a03371bac7 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 @@ -25,7 +25,10 @@ import { } from "./identity-token-auth-types"; type TIdentityTokenAuthServiceFactoryDep = { - identityTokenAuthDAL: TIdentityTokenAuthDALFactory; + identityTokenAuthDAL: Pick< + TIdentityTokenAuthDALFactory, + "transaction" | "create" | "findOne" | "updateById" | "delete" + >; identityDAL: Pick; identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index c2acc8b4b3..3068eaf927 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -124,11 +124,7 @@ export const IdentitySection = withPermission( handlePopUpOpen={handlePopUpOpen} handlePopUpToggle={handlePopUpToggle} /> - + Date: Wed, 3 Jul 2024 13:29:31 -0700 Subject: [PATCH 3/5] Identity redesign modal opt --- .../IdentitySection/IdentityModalV2.tsx | 193 ++++++++++++++++++ .../IdentitySection/IdentitySection.tsx | 8 + .../IdentitySection/IdentityTable.tsx | 23 ++- 3 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModalV2.tsx diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModalV2.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModalV2.tsx new file mode 100644 index 0000000000..ea272fab5f --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModalV2.tsx @@ -0,0 +1,193 @@ +import { Controller, useForm } from "react-hook-form"; +import { faKey } from "@fortawesome/free-solid-svg-icons"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + EmptyState, + FormControl, + Input, + Modal, + ModalContent, + Tab, + Table, + TableContainer, + TabList, + TabPanel, + Tabs, + TBody, + Td, + Th, + THead, + Tr} from "@app/components/v2"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + popUp: UsePopUpState<["identityModalV2", "upgradePlan"]>; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityModalV2", "upgradePlan"]>, + state?: boolean + ) => void; +}; + +enum TabSections { + AccessTokens = "access-tokens", + AuthMethod = "auth-method" +} + +const schema = z.object({ + name: z.string(), + role: z.string() +}); + +type FormData = z.infer; + +export const IdentityModalV2 = ({ popUp, handlePopUpToggle }: Props) => { + const { + control, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + role: "" + } + }); + + const identityData = popUp?.identityModalV2?.data as { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + }; + + return ( + { + handlePopUpToggle("identityModalV2", isOpen); + }} + > + +
+
+
+
+
+

Some ID

+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+
+
+ + + Access Tokens + Auth Method + + +
+
+

Access Token

+ +
+

+ Create an access token to authenticate with the API +

+
+ + + + + + + + + + + + + +
TokenExpires +
+ +
+
+
+ Amx +
+
+
+ {/* ( + + + + )} + /> */} + {/* {renderIdentityAuthForm()} */} + {/* handlePopUpToggle("upgradePlan", isOpen)} + text="You can use IP allowlisting if you switch to Infisical's Pro plan." + /> */} +
+
+ ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 3068eaf927..dce8adcb13 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -17,6 +17,8 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal"; import { IdentityModal } from "./IdentityModal"; +// new +import { IdentityModalV2 } from "./IdentityModalV2"; import { IdentityTable } from "./IdentityTable"; import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal"; @@ -30,6 +32,7 @@ export const IdentitySection = withPermission( const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "identity", + "identityModalV2", // TODO: update "identityAuthMethod", "deleteIdentity", "universalAuthClientSecret", @@ -114,6 +117,11 @@ export const IdentitySection = withPermission( handlePopUpOpen={handlePopUpOpen} handlePopUpToggle={handlePopUpToggle} /> + { data.map(({ identity: { id, name, authMethod }, role, customRole }) => { return ( - {name} + + + Date: Mon, 8 Jul 2024 17:12:49 +0700 Subject: [PATCH 4/5] Finish new identity page --- ...240704161322_identity-access-token-name.ts | 24 ++ .../src/db/schemas/identity-access-tokens.ts | 3 +- .../ee/services/audit-log/audit-log-types.ts | 20 ++ backend/src/server/routes/index.ts | 4 +- .../routes/v1/identity-access-token-router.ts | 35 ++ .../src/server/routes/v1/identity-router.ts | 67 +++- .../routes/v1/identity-token-auth-router.ts | 117 +++++- .../identity-access-token-service.ts | 61 +++- .../identity-access-token-types.ts | 6 + .../identity-project/identity-project-dal.ts | 103 +++++- .../identity-token-auth-service.ts | 115 +++++- .../identity-token-auth-types.ts | 13 + .../src/services/identity/identity-service.ts | 37 +- .../src/services/identity/identity-types.ts | 4 + frontend/src/hooks/api/identities/index.tsx | 13 +- .../src/hooks/api/identities/mutations.tsx | 213 ++++++++++- frontend/src/hooks/api/identities/queries.tsx | 74 +++- frontend/src/hooks/api/identities/types.ts | 63 +++- .../[id]/identities/[identityId]/index.tsx | 20 ++ .../views/Org/IdentityPage/IdentityPage.tsx | 332 ++++++++++++++++++ .../IdentityAuthenticationSection.tsx | 98 ++++++ .../IdentityClientSecrets.tsx | 120 +++++++ .../IdentityTokens.tsx | 119 +++++++ .../IdentityAuthenticationSection/index.tsx | 1 + .../components/IdentityClientSecretModal.tsx | 191 ++++++++++ .../components/IdentityDetailsSection.tsx | 67 ++++ .../components/IdentityProjectsSection.tsx | 57 +++ .../components/IdentityTokenListModal.tsx | 269 ++++++++++++++ .../components/IdentityTokenModal.tsx | 183 ++++++++++ .../Org/IdentityPage/components/index.tsx | 6 + frontend/src/views/Org/IdentityPage/index.tsx | 1 + .../IdentitySection/IdentityAwsAuthForm.tsx | 59 +++- .../IdentitySection/IdentityAzureAuthForm.tsx | 59 +++- .../IdentitySection/IdentityGcpAuthForm.tsx | 59 +++- .../IdentityKubernetesAuthForm.tsx | 59 +++- .../IdentitySection/IdentityModal.tsx | 62 ++-- .../IdentitySection/IdentityModalV2.tsx | 193 ---------- .../IdentitySection/IdentitySection.tsx | 22 +- .../IdentitySection/IdentityTable.tsx | 221 +----------- .../IdentitySection/IdentityTokenAuthForm.tsx | 59 +++- ...IdentityUniversalAuthClientSecretModal.tsx | 74 +--- .../IdentityUniversalAuthForm.tsx | 59 +++- 42 files changed, 2691 insertions(+), 671 deletions(-) create mode 100644 backend/src/db/migrations/20240704161322_identity-access-token-name.ts create mode 100644 frontend/src/pages/org/[id]/identities/[identityId]/index.tsx create mode 100644 frontend/src/views/Org/IdentityPage/IdentityPage.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/index.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityTokenListModal.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx create mode 100644 frontend/src/views/Org/IdentityPage/components/index.tsx create mode 100644 frontend/src/views/Org/IdentityPage/index.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModalV2.tsx diff --git a/backend/src/db/migrations/20240704161322_identity-access-token-name.ts b/backend/src/db/migrations/20240704161322_identity-access-token-name.ts new file mode 100644 index 0000000000..8e84dfc4bd --- /dev/null +++ b/backend/src/db/migrations/20240704161322_identity-access-token-name.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.IdentityAccessToken)) { + const hasNameColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "name"); + if (!hasNameColumn) { + await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => { + t.string("name").nullable(); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.IdentityAccessToken)) { + if (await knex.schema.hasColumn(TableName.IdentityAccessToken, "name")) { + await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => { + t.dropColumn("name"); + }); + } + } +} diff --git a/backend/src/db/schemas/identity-access-tokens.ts b/backend/src/db/schemas/identity-access-tokens.ts index 18dbb81930..45445c8114 100644 --- a/backend/src/db/schemas/identity-access-tokens.ts +++ b/backend/src/db/schemas/identity-access-tokens.ts @@ -19,7 +19,8 @@ export const IdentityAccessTokensSchema = z.object({ identityUAClientSecretId: z.string().nullable().optional(), identityId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + name: z.string().nullable().optional() }); export type TIdentityAccessTokens = z.infer; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 802fc628df..6d8274a825 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -67,6 +67,8 @@ export enum EventType { GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth", REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth", CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth", + UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", + GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth", @@ -460,6 +462,22 @@ interface CreateTokenIdentityTokenAuthEvent { }; } +interface UpdateTokenIdentityTokenAuthEvent { + type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + tokenId: string; + name?: string; + }; +} + +interface GetTokensIdentityTokenAuthEvent { + type: EventType.GET_TOKENS_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + }; +} + interface AddIdentityTokenAuthEvent { type: EventType.ADD_IDENTITY_TOKEN_AUTH; metadata: { @@ -1104,6 +1122,8 @@ export type Event = | DeleteIdentityUniversalAuthEvent | GetIdentityUniversalAuthEvent | CreateTokenIdentityTokenAuthEvent + | UpdateTokenIdentityTokenAuthEvent + | GetTokensIdentityTokenAuthEvent | AddIdentityTokenAuthEvent | UpdateIdentityTokenAuthEvent | GetIdentityTokenAuthEvent diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 540ff84ce9..315b1cf6f8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -812,11 +812,13 @@ export const registerRoutes = async ( permissionService, identityDAL, identityOrgMembershipDAL, + identityProjectDAL, licenseService }); const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL, - identityOrgMembershipDAL + identityOrgMembershipDAL, + permissionService }); const identityProjectService = identityProjectServiceFactory({ permissionService, diff --git a/backend/src/server/routes/v1/identity-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts index 7ed62e6798..8f65a99849 100644 --- a/backend/src/server/routes/v1/identity-access-token-router.ts +++ b/backend/src/server/routes/v1/identity-access-token-router.ts @@ -2,6 +2,8 @@ import { z } from "zod"; import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvider) => { server.route({ @@ -61,4 +63,37 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid }; } }); + + server.route({ + url: "/token/revoke-by-id", + method: "POST", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Revoke access token by the id of the token", + body: z.object({ + tokenId: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.identityAccessToken.revokeAccessTokenById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + return { + message: "Successfully revoked access token" + }; + } + }); }; diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index a425f963e1..9fc9baa49d 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -1,6 +1,12 @@ import { z } from "zod"; -import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; +import { + IdentitiesSchema, + IdentityOrgMembershipsSchema, + OrgMembershipRole, + OrgRolesSchema, + ProjectsSchema +} from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { IDENTITIES } from "@app/lib/api-docs"; import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -260,4 +266,63 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { return { identities }; } }); + + server.route({ + method: "GET", + url: "/:identityId/identity-memberships", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "List project memberships that identity with id is part of", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(IDENTITIES.GET_BY_ID.identityId) + }), + response: { + 200: z.object({ + identityMemberships: z.array( + z.object({ + id: z.string(), + identityId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), + project: ProjectsSchema.pick({ name: true, id: true }) + }) + ) + }) + } + }, + handler: async (req) => { + const identityMemberships = await server.services.identity.listProjectIdentitiesByIdentityId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + return { identityMemberships }; + } + }); }; diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index e373198033..6e06c5b6ab 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { IdentityTokenAuthsSchema } from "@app/db/schemas"; +import { IdentityAccessTokensSchema, IdentityTokenAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -255,7 +255,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider server.route({ method: "POST", - url: "/token-auth/identities/:identityId/token", + url: "/token-auth/identities/:identityId/tokens", config: { rateLimit: writeLimit }, @@ -270,6 +270,9 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider params: z.object({ identityId: z.string() }), + body: z.object({ + name: z.string().optional() + }), response: { 200: z.object({ accessToken: z.string(), @@ -286,7 +289,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - identityId: req.params.identityId + identityId: req.params.identityId, + ...req.body }); await server.services.auditLog.createAuditLog({ @@ -309,4 +313,111 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }; } }); + + server.route({ + method: "GET", + url: "/token-auth/identities/:identityId/tokens", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get tokens for identity with Token Auth configured", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string() + }), + querystring: z.object({ + offset: z.coerce.number().min(0).max(100).default(0), + limit: z.coerce.number().min(1).max(100).default(20) + }), + response: { + 200: z.object({ + tokens: IdentityAccessTokensSchema.array() + }) + } + }, + handler: async (req) => { + const { tokens, identityMembershipOrg } = await server.services.identityTokenAuth.getTokensTokenAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + ...req.query + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg.orgId, + event: { + type: EventType.GET_TOKENS_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: req.params.identityId + } + } + }); + + return { tokens }; + } + }); + + server.route({ + method: "PATCH", + url: "/token-auth/identities/:identityId/tokens/:tokenId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Update token for identity with Token Auth configured", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string(), + tokenId: z.string() + }), + body: z.object({ + name: z.string().optional() + }), + response: { + 200: z.object({ + token: IdentityAccessTokensSchema + }) + } + }, + handler: async (req) => { + const { token, identityMembershipOrg } = await server.services.identityTokenAuth.updateTokenTokenAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + tokenId: req.params.tokenId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg.orgId, + event: { + type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: req.params.identityId, + tokenId: token.id, + name: req.body.name + } + } + }); + + return { token }; + } + }); }; 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 3e7fe31a6f..5998c172cf 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,9 @@ +import { ForbiddenError } from "@casl/ability"; import jwt, { JwtPayload } from "jsonwebtoken"; import { TableName, TIdentityAccessTokens } from "@app/db/schemas"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; @@ -8,18 +11,24 @@ import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; import { AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; -import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; +import { + TIdentityAccessTokenJwtPayload, + TRenewAccessTokenDTO, + TRevokeAccessTokenByIdDTO +} from "./identity-access-token-types"; type TIdentityAccessTokenServiceFactoryDep = { identityAccessTokenDAL: TIdentityAccessTokenDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory; + permissionService: Pick; }; export type TIdentityAccessTokenServiceFactory = ReturnType; export const identityAccessTokenServiceFactory = ({ identityAccessTokenDAL, - identityOrgMembershipDAL + identityOrgMembershipDAL, + permissionService }: TIdentityAccessTokenServiceFactoryDep) => { const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => { const { @@ -131,7 +140,47 @@ export const identityAccessTokenServiceFactory = ({ }); if (!identityAccessToken) throw new UnauthorizedError(); - const revokedToken = await identityAccessTokenDAL.deleteById(identityAccessToken.id); + const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, { + isAccessTokenRevoked: true + }); + + return { revokedToken }; + }; + + const revokeAccessTokenById = async ({ + tokenId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeAccessTokenByIdDTO) => { + const identityAccessToken = await identityAccessTokenDAL.findOne({ + [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, + isAccessTokenRevoked: false + }); + if (!identityAccessToken) throw new UnauthorizedError(); + + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ + identityId: identityAccessToken.identityId + }); + + if (!identityOrgMembership) { + throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityOrgMembership.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, { + isAccessTokenRevoked: true + }); + return { revokedToken }; }; @@ -141,6 +190,10 @@ export const identityAccessTokenServiceFactory = ({ isAccessTokenRevoked: false }); if (!identityAccessToken) throw new UnauthorizedError(); + if (identityAccessToken.isAccessTokenRevoked) + throw new UnauthorizedError({ + message: "Failed to authorize revoked access token" + }); if (ipAddress && identityAccessToken) { checkIPAgainstBlocklist({ @@ -168,5 +221,5 @@ export const identityAccessTokenServiceFactory = ({ return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; }; - return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; + return { renewAccessToken, revokeAccessToken, revokeAccessTokenById, fnValidateIdentityAccessToken }; }; diff --git a/backend/src/services/identity-access-token/identity-access-token-types.ts b/backend/src/services/identity-access-token/identity-access-token-types.ts index 86967df76d..5c67454ad1 100644 --- a/backend/src/services/identity-access-token/identity-access-token-types.ts +++ b/backend/src/services/identity-access-token/identity-access-token-types.ts @@ -1,3 +1,5 @@ +import { TProjectPermission } from "@app/lib/types"; + export type TRenewAccessTokenDTO = { accessToken: string; }; @@ -8,3 +10,7 @@ export type TIdentityAccessTokenJwtPayload = { identityAccessTokenId: string; authTokenType: string; }; + +export type TRevokeAccessTokenByIdDTO = { + tokenId: string; +} & Omit; diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index 497d05c3ca..adfc9cf77e 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -10,6 +10,103 @@ export type TIdentityProjectDALFactory = ReturnType { const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership); + const findByIdentityId = async (identityId: string, tx?: Knex) => { + try { + const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) + .where(`${TableName.IdentityProjectMembership}.identityId`, identityId) + .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) + .join( + TableName.IdentityProjectMembershipRole, + `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) + .leftJoin( + TableName.ProjectRoles, + `${TableName.IdentityProjectMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) + .leftJoin( + TableName.IdentityProjectAdditionalPrivilege, + `${TableName.IdentityProjectMembership}.id`, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` + ) + .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), + db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), + 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) + ); + + const members = sqlNestRelationships({ + data: docs, + parentMapper: ({ identityName, identityAuthMethod, id, createdAt, updatedAt, projectId, projectName }) => ({ + id, + identityId, + createdAt, + updatedAt, + identity: { + id: identityId, + name: identityName, + authMethod: identityAuthMethod + }, + project: { + id: projectId, + name: projectName + } + }), + key: "id", + childrenMapper: [ + { + label: "roles" as const, + key: "membershipRoleId", + mapper: ({ + role, + customRoleId, + customRoleName, + customRoleSlug, + membershipRoleId, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) => ({ + id: membershipRoleId, + role, + customRoleId, + customRoleName, + customRoleSlug, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) + } + ] + }); + return members; + } catch (error) { + throw new DatabaseError({ error, name: "FindByIdentityId" }); + } + }; + const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) @@ -105,5 +202,9 @@ export const identityProjectDALFactory = (db: TDbClient) => { } }; - return { ...identityProjectOrm, findByProjectId }; + return { + ...identityProjectOrm, + findByIdentityId, + findByProjectId + }; }; 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 a03371bac7..c331472385 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 @@ -20,8 +20,10 @@ import { TAttachTokenAuthDTO, TCreateTokenTokenAuthDTO, TGetTokenAuthDTO, + TGetTokensTokenAuthDTO, TRevokeTokenAuthDTO, - TUpdateTokenAuthDTO + TUpdateTokenAuthDTO, + TUpdateTokenTokenAuthDTO } from "./identity-token-auth-types"; type TIdentityTokenAuthServiceFactoryDep = { @@ -31,7 +33,7 @@ type TIdentityTokenAuthServiceFactoryDep = { >; identityDAL: Pick; identityOrgMembershipDAL: Pick; - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -258,7 +260,8 @@ export const identityTokenAuthServiceFactory = ({ actorId, actor, actorAuthMethod, - actorOrgId + actorOrgId, + name }: TCreateTokenTokenAuthDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); @@ -285,7 +288,7 @@ export const identityTokenAuthServiceFactory = ({ const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); if (!hasPriviledge) throw new ForbiddenRequestError({ - message: "Failed to revoke Token Auth of identity with more privileged role" + message: "Failed to create token for identity with more privileged role" }); const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); @@ -298,7 +301,8 @@ export const identityTokenAuthServiceFactory = ({ accessTokenTTL: identityTokenAuth.accessTokenTTL, accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL, accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit + accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit, + name }, tx ); @@ -324,11 +328,110 @@ export const identityTokenAuthServiceFactory = ({ return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg }; }; + const getTokensTokenAuth = async ({ + identityId, + offset = 0, + limit = 20, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TGetTokensTokenAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) + throw new BadRequestError({ + message: "The identity does not have Token Auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to get tokens for identity with more privileged role" + }); + + const tokens = await identityAccessTokenDAL.find( + { + identityId + }, + { offset, limit, sort: [["updatedAt", "desc"]] } + ); + + return { tokens, identityMembershipOrg }; + }; + + const updateTokenTokenAuth = async ({ + identityId, + tokenId, + name, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TUpdateTokenTokenAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) + throw new BadRequestError({ + message: "The identity does not have Token Auth" + }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) + throw new ForbiddenRequestError({ + message: "Failed to update token for identity with more privileged role" + }); + + const [token] = await identityAccessTokenDAL.update( + { + identityId, + id: tokenId + }, + { + name + } + ); + + return { token, identityMembershipOrg }; + }; + return { attachTokenAuth, updateTokenAuth, getTokenAuth, revokeIdentityTokenAuth, - createTokenTokenAuth + createTokenTokenAuth, + getTokensTokenAuth, + updateTokenTokenAuth }; }; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts index 3115b0237a..4419abc36c 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-types.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -26,4 +26,17 @@ export type TRevokeTokenAuthDTO = { export type TCreateTokenTokenAuthDTO = { identityId: string; + name?: string; +} & Omit; + +export type TGetTokensTokenAuthDTO = { + identityId: string; + offset: number; + limit: number; +} & Omit; + +export type TUpdateTokenTokenAuthDTO = { + identityId: string; + tokenId: string; + name?: string; } & Omit; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 62c812a3ed..d3fc9c788b 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -7,15 +7,23 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { TOrgPermission } from "@app/lib/types"; +import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { ActorType } from "../auth/auth-type"; import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityOrgDALFactory } from "./identity-org-dal"; -import { TCreateIdentityDTO, TDeleteIdentityDTO, TGetIdentityByIdDTO, TUpdateIdentityDTO } from "./identity-types"; +import { + TCreateIdentityDTO, + TDeleteIdentityDTO, + TGetIdentityByIdDTO, + TListProjectIdentitiesByIdentityIdDTO, + TUpdateIdentityDTO +} from "./identity-types"; type TIdentityServiceFactoryDep = { identityDAL: TIdentityDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory; + identityProjectDAL: Pick; permissionService: Pick; licenseService: Pick; }; @@ -25,6 +33,7 @@ export type TIdentityServiceFactory = ReturnType; export const identityServiceFactory = ({ identityDAL, identityOrgMembershipDAL, + identityProjectDAL, permissionService, licenseService }: TIdentityServiceFactoryDep) => { @@ -196,11 +205,35 @@ export const identityServiceFactory = ({ return identityMemberships; }; + const listProjectIdentitiesByIdentityId = async ({ + identityId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TListProjectIdentitiesByIdentityIdDTO) => { + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityOrgMembership.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + + const identityMemberships = await identityProjectDAL.findByIdentityId(identityId); + return identityMemberships; + }; + return { createIdentity, updateIdentity, deleteIdentity, listOrgIdentities, - getIdentityById + getIdentityById, + listProjectIdentitiesByIdentityId }; }; diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index 5125413e85..52df905cea 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -25,3 +25,7 @@ export interface TIdentityTrustedIp { type: IPType; prefix: number; } + +export type TListProjectIdentitiesByIdentityIdDTO = { + identityId: string; +} & Omit; diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index e615ae561b..b4d4f9e6d2 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -11,19 +11,30 @@ export { useCreateIdentityUniversalAuthClientSecret, useCreateTokenIdentityTokenAuth, useDeleteIdentity, + useDeleteIdentityAwsAuth, + useDeleteIdentityAzureAuth, + useDeleteIdentityGcpAuth, + useDeleteIdentityKubernetesAuth, + useDeleteIdentityTokenAuth, + useDeleteIdentityUniversalAuth, useRevokeIdentityUniversalAuthClientSecret, + useRevokeToken, useUpdateIdentity, useUpdateIdentityAwsAuth, useUpdateIdentityAzureAuth, useUpdateIdentityGcpAuth, useUpdateIdentityKubernetesAuth, useUpdateIdentityTokenAuth, - useUpdateIdentityUniversalAuth} from "./mutations"; + useUpdateIdentityUniversalAuth, + useUpdateTokenIdentityTokenAuth} from "./mutations"; export { useGetIdentityAwsAuth, useGetIdentityAzureAuth, + useGetIdentityById, useGetIdentityGcpAuth, useGetIdentityKubernetesAuth, + useGetIdentityProjectMemberships, useGetIdentityTokenAuth, + useGetIdentityTokensTokenAuth, useGetIdentityUniversalAuth, useGetIdentityUniversalAuthClientSecrets} from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 0d59ec9991..323b45fd53 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -17,22 +17,32 @@ import { CreateIdentityUniversalAuthClientSecretRes, CreateTokenIdentityTokenAuthDTO, CreateTokenIdentityTokenAuthRes, + DeleteIdentityAwsAuthDTO, + DeleteIdentityAzureAuthDTO, DeleteIdentityDTO, + DeleteIdentityGcpAuthDTO, + DeleteIdentityKubernetesAuthDTO, + DeleteIdentityTokenAuthDTO, DeleteIdentityUniversalAuthClientSecretDTO, + DeleteIdentityUniversalAuthDTO, Identity, + IdentityAccessToken, IdentityAwsAuth, IdentityAzureAuth, IdentityGcpAuth, IdentityKubernetesAuth, IdentityTokenAuth, IdentityUniversalAuth, + RevokeTokenDTO, + RevokeTokenRes, UpdateIdentityAwsAuthDTO, UpdateIdentityAzureAuthDTO, UpdateIdentityDTO, UpdateIdentityGcpAuthDTO, UpdateIdentityKubernetesAuthDTO, UpdateIdentityTokenAuthDTO, - UpdateIdentityUniversalAuthDTO} from "./types"; + UpdateIdentityUniversalAuthDTO, + UpdateTokenIdentityTokenAuthDTO} from "./types"; export const useCreateIdentity = () => { const queryClient = useQueryClient(); @@ -62,8 +72,9 @@ export const useUpdateIdentity = () => { return identity; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { organizationId, identityId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); } }); }; @@ -107,8 +118,10 @@ export const useAddIdentityUniversalAuth = () => { }); return identityUniversalAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId)); } }); }; @@ -135,8 +148,27 @@ export const useUpdateIdentityUniversalAuth = () => { }); return identityUniversalAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId)); + } + }); +}; + +export const useDeleteIdentityUniversalAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityUniversalAuth } + } = await apiRequest.delete(`/api/v1/auth/universal-auth/identities/${identityId}`); + return identityUniversalAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId)); } }); }; @@ -218,8 +250,10 @@ export const useAddIdentityGcpAuth = () => { return identityGcpAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId)); } }); }; @@ -256,8 +290,27 @@ export const useUpdateIdentityGcpAuth = () => { return identityGcpAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId)); + } + }); +}; + +export const useDeleteIdentityGcpAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityGcpAuth } + } = await apiRequest.delete(`/api/v1/auth/gcp-auth/identities/${identityId}`); + return identityGcpAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId)); } }); }; @@ -292,8 +345,10 @@ export const useAddIdentityAwsAuth = () => { return identityAwsAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId)); } }); }; @@ -328,8 +383,27 @@ export const useUpdateIdentityAwsAuth = () => { return identityAwsAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId)); + } + }); +}; + +export const useDeleteIdentityAwsAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityAwsAuth } + } = await apiRequest.delete(`/api/v1/auth/aws-auth/identities/${identityId}`); + return identityAwsAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId)); } }); }; @@ -364,8 +438,10 @@ export const useAddIdentityAzureAuth = () => { return identityAzureAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId)); } }); }; @@ -406,8 +482,10 @@ export const useAddIdentityKubernetesAuth = () => { return identityKubernetesAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId)); } }); }; @@ -442,8 +520,27 @@ export const useUpdateIdentityAzureAuth = () => { return identityAzureAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId)); + } + }); +}; + +export const useDeleteIdentityAzureAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityAzureAuth } + } = await apiRequest.delete(`/api/v1/auth/azure-auth/identities/${identityId}`); + return identityAzureAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId)); } }); }; @@ -484,8 +581,27 @@ export const useUpdateIdentityKubernetesAuth = () => { return identityKubernetesAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId)); + } + }); +}; + +export const useDeleteIdentityKubernetesAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityKubernetesAuth } + } = await apiRequest.delete(`/api/v1/auth/kubernetes-auth/identities/${identityId}`); + return identityKubernetesAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId)); } }); }; @@ -514,8 +630,10 @@ export const useAddIdentityTokenAuth = () => { return identityTokenAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId)); } }); }; @@ -544,8 +662,27 @@ export const useUpdateIdentityTokenAuth = () => { return identityTokenAuth; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { identityId, organizationId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId)); + } + }); +}; + +export const useDeleteIdentityTokenAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityTokenAuth } + } = await apiRequest.delete(`/api/v1/auth/token-auth/identities/${identityId}`); + return identityTokenAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityTokenAuth(identityId)); } }); }; @@ -553,15 +690,55 @@ export const useUpdateIdentityTokenAuth = () => { export const useCreateTokenIdentityTokenAuth = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ identityId }) => { + mutationFn: async ({ identityId, name }) => { const { data } = await apiRequest.post( - `/api/v1/auth/token-auth/identities/${identityId}/token` + `/api/v1/auth/token-auth/identities/${identityId}/tokens`, + { + name + } ); return data; }, - onSuccess: (_, { organizationId }) => { - queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + onSuccess: (_, { identityId }) => { + queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId)); + } + }); +}; + +export const useUpdateTokenIdentityTokenAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId, tokenId, name }) => { + const { + data: { token } + } = await apiRequest.patch<{ token: IdentityAccessToken }>( + `/api/v1/auth/token-auth/identities/${identityId}/tokens/${tokenId}`, + { + name + } + ); + + return token; + }, + onSuccess: (_, { identityId }) => { + queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId)); + } + }); +}; + +export const useRevokeToken = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ tokenId }) => { + const { data } = await apiRequest.post("/api/v1/auth/token/revoke-by-id", { + tokenId + }); + + return data; + }, + onSuccess: (_, { identityId }) => { + queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId)); } }); }; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 4b74f086ce..5a72ecb781 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -4,14 +4,17 @@ import { apiRequest } from "@app/config/request"; import { ClientSecretData, + IdentityAccessToken, IdentityAwsAuth, IdentityAzureAuth, IdentityGcpAuth, IdentityKubernetesAuth, + IdentityMembershipOrg, IdentityTokenAuth, IdentityUniversalAuth} from "./types"; export const identitiesKeys = { + getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const, getIdentityUniversalAuth: (identityId: string) => [{ identityId }, "identity-universal-auth"] as const, getIdentityUniversalAuthClientSecrets: (identityId: string) => @@ -21,7 +24,39 @@ export const identitiesKeys = { getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const, getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const, getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const, - getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const + getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const, + getIdentityTokensTokenAuth: (identityId: string) => + [{ identityId }, "identity-tokens-token-auth"] as const, + getIdentityProjectMemberships: (identityId: string) => + [{ identityId }, "identity-project-memberships"] as const +}; + +export const useGetIdentityById = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityById(identityId), + queryFn: async () => { + const { + data: { identity } + } = await apiRequest.get<{ identity: IdentityMembershipOrg }>( + `/api/v1/identities/${identityId}` + ); + return identity; + } + }); +}; + +export const useGetIdentityProjectMemberships = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityProjectMemberships(identityId), + queryFn: async () => { + const { + data: { identityMemberships } + } = await apiRequest.get(`/api/v1/identities/${identityId}/identity-memberships`); + return identityMemberships; + } + }); }; export const useGetIdentityUniversalAuth = (identityId: string) => { @@ -35,7 +70,9 @@ export const useGetIdentityUniversalAuth = (identityId: string) => { `/api/v1/auth/universal-auth/identities/${identityId}` ); return identityUniversalAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -65,7 +102,9 @@ export const useGetIdentityGcpAuth = (identityId: string) => { `/api/v1/auth/gcp-auth/identities/${identityId}` ); return identityGcpAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -80,7 +119,9 @@ export const useGetIdentityAwsAuth = (identityId: string) => { `/api/v1/auth/aws-auth/identities/${identityId}` ); return identityAwsAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -95,7 +136,9 @@ export const useGetIdentityAzureAuth = (identityId: string) => { `/api/v1/auth/azure-auth/identities/${identityId}` ); return identityAzureAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -110,7 +153,9 @@ export const useGetIdentityKubernetesAuth = (identityId: string) => { `/api/v1/auth/kubernetes-auth/identities/${identityId}` ); return identityKubernetesAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -125,6 +170,23 @@ export const useGetIdentityTokenAuth = (identityId: string) => { `/api/v1/auth/token-auth/identities/${identityId}` ); return identityTokenAuth; + }, + staleTime: 0, + cacheTime: 0 + }); +}; + +export const useGetIdentityTokensTokenAuth = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityTokensTokenAuth(identityId), + queryFn: async () => { + const { + data: { tokens } + } = await apiRequest.get<{ tokens: IdentityAccessToken[] }>( + `/api/v1/auth/token-auth/identities/${identityId}/tokens` + ); + return tokens; } }); }; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 117fcbb88a..96dc705473 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -16,6 +16,22 @@ export type Identity = { updatedAt: string; }; +export type IdentityAccessToken = { + id: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUses: number; + accessTokenNumUsesLimit: number; + accessTokenLastUsedAt: string | null; + accessTokenLastRenewedAt: string | null; + isAccessTokenRevoked: boolean; + identityUAClientSecretId: string | null; + identityId: string; + createdAt: string; + updatedAt: string; + name: string | null; +}; + export type IdentityMembershipOrg = { id: string; identity: Identity; @@ -113,6 +129,11 @@ export type UpdateIdentityUniversalAuthDTO = { }[]; }; +export type DeleteIdentityUniversalAuthDTO = { + organizationId: string; + identityId: string; +}; + export type IdentityGcpAuth = { identityId: string; type: "iam" | "gce"; @@ -155,6 +176,11 @@ export type UpdateIdentityGcpAuthDTO = { }[]; }; +export type DeleteIdentityGcpAuthDTO = { + organizationId: string; + identityId: string; +}; + export type IdentityAwsAuth = { identityId: string; type: "iam"; @@ -195,6 +221,11 @@ export type UpdateIdentityAwsAuthDTO = { }[]; }; +export type DeleteIdentityAwsAuthDTO = { + organizationId: string; + identityId: string; +}; + export type IdentityAzureAuth = { identityId: string; tenantId: string; @@ -234,6 +265,11 @@ export type UpdateIdentityAzureAuthDTO = { }[]; }; +export type DeleteIdentityAzureAuthDTO = { + organizationId: string; + identityId: string; +}; + export type IdentityKubernetesAuth = { identityId: string; kubernetesHost: string; @@ -282,6 +318,11 @@ export type UpdateIdentityKubernetesAuthDTO = { }[]; }; +export type DeleteIdentityKubernetesAuthDTO = { + organizationId: string; + identityId: string; +}; + export type CreateIdentityUniversalAuthClientSecretDTO = { identityId: string; description?: string; @@ -342,9 +383,14 @@ export type UpdateIdentityTokenAuthDTO = { }[]; }; +export type DeleteIdentityTokenAuthDTO = { + organizationId: string; + identityId: string; +}; + export type CreateTokenIdentityTokenAuthDTO = { identityId: string; - organizationId: string; + name: string; }; export type CreateTokenIdentityTokenAuthRes = { @@ -353,3 +399,18 @@ export type CreateTokenIdentityTokenAuthRes = { expiresIn: number; accessTokenMaxTTL: number; }; + +export type UpdateTokenIdentityTokenAuthDTO = { + identityId: string; + tokenId: string; + name?: string; +}; + +export type RevokeTokenDTO = { + identityId: string; + tokenId: string; +}; + +export type RevokeTokenRes = { + message: string; +}; diff --git a/frontend/src/pages/org/[id]/identities/[identityId]/index.tsx b/frontend/src/pages/org/[id]/identities/[identityId]/index.tsx new file mode 100644 index 0000000000..22d80a7d83 --- /dev/null +++ b/frontend/src/pages/org/[id]/identities/[identityId]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { IdentityPage } from "@app/views/Org/IdentityPage"; + +export default function Identity() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.org.title") })} + + + + + ); +} + +Identity.requireAuth = true; diff --git a/frontend/src/views/Org/IdentityPage/IdentityPage.tsx b/frontend/src/views/Org/IdentityPage/IdentityPage.tsx new file mode 100644 index 0000000000..548ea22667 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/IdentityPage.tsx @@ -0,0 +1,332 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useRouter } from "next/router"; +import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip, + UpgradePlanModal +} from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { withPermission } from "@app/hoc"; +import { + useDeleteIdentity, + useGetIdentityById, + useRevokeIdentityUniversalAuthClientSecret, + useRevokeToken +} from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; +import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal"; +import { IdentityUniversalAuthClientSecretModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal"; +import { + IdentityAuthenticationSection, + IdentityClientSecretModal, + IdentityDetailsSection, + IdentityProjectsSection, + IdentityTokenListModal, + IdentityTokenModal} from "./components"; + +export const IdentityPage = withPermission( + () => { + const router = useRouter(); + const identityId = router.query.identityId as string; + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { data } = useGetIdentityById(identityId); + const { mutateAsync: deleteIdentity } = useDeleteIdentity(); + const { mutateAsync: revokeToken } = useRevokeToken(); + const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "identity", + "deleteIdentity", + "identityAuthMethod", + "token", + "tokenList", + "revokeToken", + "clientSecret", + "revokeClientSecret", + "universalAuthClientSecret", // list of client secrets + "upgradePlan" + ] as const); + + const onDeleteIdentitySubmit = async (id: string) => { + try { + await deleteIdentity({ + identityId: id, + organizationId: orgId + }); + + createNotification({ + text: "Successfully deleted identity", + type: "success" + }); + + handlePopUpClose("deleteIdentity"); + router.push(`/org/${orgId}/members`); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to delete identity"; + + createNotification({ + text, + type: "error" + }); + } + }; + + const onRevokeTokenSubmit = async ({ + identityId: parentIdentityId, + tokenId, + name + }: { + identityId: string; + tokenId: string; + name: string; + }) => { + try { + await revokeToken({ + identityId: parentIdentityId, + tokenId + }); + + handlePopUpClose("revokeToken"); + + createNotification({ + text: `Successfully revoked token ${name ?? ""}`, + type: "success" + }); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to delete identity"; + + createNotification({ + text, + type: "error" + }); + } + }; + + const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => { + try { + if (!data?.identity.id) return; + + await revokeClientSecret({ + identityId: data?.identity.id, + clientSecretId + }); + + handlePopUpToggle("revokeClientSecret", false); + + createNotification({ + text: "Successfully deleted client secret", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete client secret", + type: "error" + }); + } + }; + + return ( +
+ {data && ( +
+ +
+

{data.identity.name}

+ + +
+ + + +
+
+ + + {(isAllowed) => ( + { + handlePopUpOpen("identity", { + identityId, + name: data.identity.name, + role: data.role, + customRole: data.customRole + }); + }} + disabled={!isAllowed} + > + Edit Identity + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen("identityAuthMethod", { + identityId, + name: data.identity.name, + authMethod: data.identity.authMethod + }); + }} + disabled={!isAllowed} + > + {`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`} + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen("deleteIdentity", { + identityId, + name: data.identity.name + }); + }} + disabled={!isAllowed} + > + Delete Identity + + )} + + +
+
+
+
+ + +
+ +
+
+ )} + + + + + + + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> + handlePopUpToggle("deleteIdentity", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteIdentitySubmit( + (popUp?.deleteIdentity?.data as { identityId: string })?.identityId + ) + } + /> + handlePopUpToggle("revokeToken", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const revokeTokenData = popUp?.revokeToken?.data as { + identityId: string; + tokenId: string; + name: string; + }; + + return onRevokeTokenSubmit(revokeTokenData); + }} + /> + handlePopUpToggle("revokeClientSecret", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const deleteClientSecretData = popUp?.revokeClientSecret?.data as { + clientSecretId: string; + clientSecretPrefix: string; + }; + + return onDeleteClientSecretSubmit({ + clientSecretId: deleteClientSecretData.clientSecretId + }); + }} + /> +
+ ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Identity } +); diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx new file mode 100644 index 0000000000..7aad5c558e --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx @@ -0,0 +1,98 @@ +import { faPencil } 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useGetIdentityById } from "@app/hooks/api"; +import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityClientSecrets } from "./IdentityClientSecrets"; +import { IdentityTokens } from "./IdentityTokens"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState< + [ + "clientSecret", + "identityAuthMethod", + "revokeClientSecret", + "token", + "revokeToken", + "universalAuthClientSecret", + "tokenList" + ] + >, + data?: {} + ) => void; +}; + +export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => { + const { data } = useGetIdentityById(identityId); + return data ? ( +
+
+

Authentication

+ + {(isAllowed) => { + return ( + + + handlePopUpOpen("identityAuthMethod", { + identityId, + name: data.identity.name, + authMethod: data.identity.authMethod + }) + } + > + + + + ); + }} + +
+
+
+

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/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx new file mode 100644 index 0000000000..781bb915c9 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx @@ -0,0 +1,120 @@ +import { faKey, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, IconButton, Tooltip } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { + useGetIdentityById, + useGetIdentityUniversalAuth, + useGetIdentityUniversalAuthClientSecrets +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState< + ["clientSecret", "revokeClientSecret", "universalAuthClientSecret"] + >, + data?: {} + ) => void; +}; + +const SHOW_LIMIT = 3; + +export const IdentityClientSecrets = ({ identityId, handlePopUpOpen }: Props) => { + const { data } = useGetIdentityById(identityId); + const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(identityId); + const { data: clientSecrets } = useGetIdentityUniversalAuthClientSecrets(identityId); + return ( +
+
+

Client ID

+

{identityUniversalAuth?.clientId ?? ""}

+
+ {clientSecrets?.length ? ( +
+

{`Client Secrets (${clientSecrets.length})`}

+ +
+ ) : ( +
+ )} + {clientSecrets + ?.slice(0, SHOW_LIMIT) + .map(({ id, clientSecretTTL, clientSecretPrefix, createdAt }) => { + let expiresAt; + if (clientSecretTTL > 0) { + expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000); + } + + return ( +
+
+ +
+

+ {`${clientSecretPrefix}****`} +

+

+ {expiresAt ? `Expires on ${format(expiresAt, "yyyy-MM-dd")}` : "No Expiry"} +

+
+
+
+ + { + handlePopUpOpen("revokeClientSecret", { + clientSecretId: id, + clientSecretPrefix + }); + }} + > + + + +
+
+ ); + })} + + {(isAllowed) => { + return ( + + ); + }} + +
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx new file mode 100644 index 0000000000..e97b2faec5 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx @@ -0,0 +1,119 @@ +import { faEllipsis, faKey } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip +} from "@app/components/v2"; +import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["token", "tokenList", "revokeToken"]>, + data?: {} + ) => void; +}; + +export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => { + const { data } = useGetIdentityById(identityId); + const { data: tokens } = useGetIdentityTokensTokenAuth(identityId); + return ( +
+ {tokens?.length ? ( +
+

{`Access Tokens (${tokens.length})`}

+ +
+ ) : ( +
+ )} + {tokens?.map((token) => { + const expiresAt = new Date( + new Date(token.createdAt).getTime() + token.accessTokenMaxTTL * 1000 + ); + return ( +
+
+ +
+

+ {token.name ? token.name : "-"} +

+

+ {token.isAccessTokenRevoked + ? "Revoked" + : `Expires on ${format(expiresAt, "yyyy-MM-dd")}`} +

+
+
+ + +
+ + + +
+
+ + { + handlePopUpOpen("token", { + identityId, + tokenId: token.id, + name: token.name + }); + }} + > + Edit Token + + { + handlePopUpOpen("revokeToken", { + identityId, + tokenId: token.id, + name: token.name + }); + }} + > + Revoke Token + + +
+
+ ); + })} + +
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/index.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/index.tsx new file mode 100644 index 0000000000..401c930d89 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/index.tsx @@ -0,0 +1 @@ +export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection"; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx new file mode 100644 index 0000000000..49c21587da --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx @@ -0,0 +1,191 @@ +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + Input, + Modal, + ModalContent, + Tooltip +} from "@app/components/v2"; +import { useTimedReset } from "@app/hooks"; +import { useCreateIdentityUniversalAuthClientSecret } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + description: z.string(), + ttl: z.string(), + numUsesLimit: z.string() + }) + .required(); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["clientSecret"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret"]>, state?: boolean) => void; +}; + +export const IdentityClientSecretModal = ({ popUp, handlePopUpToggle }: Props) => { + const { mutateAsync: createClientSecret } = useCreateIdentityUniversalAuthClientSecret(); + const [token, setToken] = useState(""); + const [copyTextToken, isCopyingToken, setCopyTextToken] = useTimedReset({ + initialState: "Copy to clipboard" + }); + const hasToken = Boolean(token); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + description: "", + ttl: "", + numUsesLimit: "" + } + }); + + const popUpData = popUp?.clientSecret?.data as { + identityId: string; + }; + + const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => { + try { + const { clientSecret } = await createClientSecret({ + identityId: popUpData.identityId, + description, + ttl: Number(ttl), + numUsesLimit: Number(numUsesLimit) + }); + + setToken(clientSecret); + + createNotification({ + text: "Successfully created client secret", + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to create client secret"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("clientSecret", isOpen); + reset(); + setToken(""); + }} + > + + {!hasToken ? ( +
+ ( + + + + )} + /> + ( + +
+ +
+
+ )} + /> + ( + + + + )} + /> +
+ + +
+ + ) : ( +
+

{token}

+ + { + navigator.clipboard.writeText(token); + setCopyTextToken("Copied"); + }} + > + + + +
+ )} +
+
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx new file mode 100644 index 0000000000..6a6843d851 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx @@ -0,0 +1,67 @@ +import { faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { IconButton,Tooltip } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useGetIdentityById } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "token", "clientSecret"]>, + data?: {} + ) => void; +}; + +export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) => { + const { data } = useGetIdentityById(identityId); + return data ? ( +
+
+

Details

+ + {(isAllowed) => { + return ( + + { + handlePopUpOpen("identity", { + identityId, + name: data.identity.name, + role: data.role, + customRole: data.customRole + }); + }} + > + + + + ); + }} + +
+
+
+

ID

+

{data.identity.id}

+
+
+

Name

+

{data.identity.name}

+
+
+

Organization Role

+

{data.role}

+
+
+
+ ) : ( +
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection.tsx new file mode 100644 index 0000000000..abf8f950bd --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection.tsx @@ -0,0 +1,57 @@ +import { faKey } from "@fortawesome/free-solid-svg-icons"; + +import { + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useGetIdentityProjectMemberships } from "@app/hooks/api"; + +type Props = { + identityId: string; +}; + +export const IdentityProjectsSection = ({ identityId }: Props) => { + const { data: projectMemberships, isLoading } = useGetIdentityProjectMemberships(identityId); + return ( +
+
+

Projects

+
+
+ + + + + + + + + + {isLoading && } + {!isLoading && + projectMemberships?.map((membership: any) => { + // TODO: fix any + return ( + + + + + ); + })} + +
NameRole
{membership.project.name}{membership.roles[0].role}
+ {!isLoading && !projectMemberships?.length && ( + + )} +
+
+
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityTokenListModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityTokenListModal.tsx new file mode 100644 index 0000000000..4fecad1bd8 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityTokenListModal.tsx @@ -0,0 +1,269 @@ +import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format } from "date-fns"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + EmptyState, + FormControl, + IconButton, + Input, + Modal, + ModalContent, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { + useCreateTokenIdentityTokenAuth, + useGetIdentityTokensTokenAuth, + useGetIdentityUniversalAuthClientSecrets} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z.object({ + name: z.string() +}); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["tokenList", "revokeToken"]>; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["revokeToken"]>, data?: {}) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["tokenList", "revokeToken"]>, + state?: boolean + ) => void; +}; + +export const IdentityTokenListModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { + const { t } = useTranslation(); + + const [token, setToken] = useState(""); + const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false); + const [isClientIdCopied, setIsClientIdCopied] = useToggle(false); + + const popUpData = popUp?.tokenList?.data as { + identityId: string; + name: string; + }; + + const { data: tokens } = useGetIdentityTokensTokenAuth(popUpData?.identityId ?? ""); + const { data, isLoading } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? ""); + + const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "" + } + }); + + useEffect(() => { + let timer: NodeJS.Timeout; + if (isClientSecretCopied) { + timer = setTimeout(() => setIsClientSecretCopied.off(), 2000); + } + + return () => clearTimeout(timer); + }, [isClientSecretCopied]); + + useEffect(() => { + let timer: NodeJS.Timeout; + if (isClientIdCopied) { + timer = setTimeout(() => setIsClientIdCopied.off(), 2000); + } + + return () => clearTimeout(timer); + }, [isClientIdCopied]); + + const onFormSubmit = async ({ name }: FormData) => { + try { + if (!popUpData?.identityId) return; + + const newTokenData = await createToken({ + identityId: popUpData.identityId, + name + }); + + setToken(newTokenData.accessToken); + + createNotification({ + text: "Successfully created token", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to create token", + type: "error" + }); + } + }; + + const hasToken = Boolean(token); + + return ( + { + handlePopUpToggle("tokenList", isOpen); + reset(); + setToken(""); + }} + > + +

New Token

+ {hasToken ? ( +
+
+

We will only show this token once

+ +
+
+

{token}

+ { + navigator.clipboard.writeText(token); + setIsClientSecretCopied.on(); + }} + > + + + {t("common.click-to-copy")} + + +
+
+ ) : ( +
+ ( + +
+ + +
+
+ )} + /> + + )} +

Tokens

+ + + + + + + + + + + + {isLoading && } + {!isLoading && + tokens?.map( + ({ + id, + createdAt, + name, + accessTokenNumUses, + accessTokenNumUsesLimit, + accessTokenMaxTTL, + isAccessTokenRevoked + }) => { + const expiresAt = new Date( + new Date(createdAt).getTime() + accessTokenMaxTTL * 1000 + ); + + return ( + + + + + + + + ); + } + )} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
nameNum UsesCreated AtMax Expires At +
{name === "" ? "-" : name}{`${accessTokenNumUses}${ + accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : "" + }`}{format(new Date(createdAt), "yyyy-MM-dd")} + {isAccessTokenRevoked ? "Revoked" : `${format(expiresAt, "yyyy-MM-dd")}`} + + {!isAccessTokenRevoked && ( + { + handlePopUpOpen("revokeToken", { + identityId: popUpData?.identityId, + tokenId: id, + name + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + > + + + )} +
+ +
+
+
+
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx new file mode 100644 index 0000000000..24a2a6bafb --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx @@ -0,0 +1,183 @@ +import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + Input, + Modal, + ModalContent, + Tooltip +} from "@app/components/v2"; +import { useTimedReset } from "@app/hooks"; +import { useCreateTokenIdentityTokenAuth, useUpdateTokenIdentityTokenAuth } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + name: z.string() + }) + .required(); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["token"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["token"]>, state?: boolean) => void; +}; + +export const IdentityTokenModal = ({ popUp, handlePopUpToggle }: Props) => { + const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); + const { mutateAsync: updateToken } = useUpdateTokenIdentityTokenAuth(); + const [token, setToken] = useState(""); + const [copyTextToken, isCopyingToken, setCopyTextToken] = useTimedReset({ + initialState: "Copy to clipboard" + }); + const hasToken = Boolean(token); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "" + } + }); + + const tokenData = popUp?.token?.data as { + identityId: string; + tokenId?: string; + name?: string; + }; + + useEffect(() => { + if (tokenData?.tokenId && tokenData?.name) { + reset({ + name: tokenData.name + }); + } else { + reset({ + name: "" + }); + } + }, [popUp?.token?.data]); + + const onFormSubmit = async ({ name }: FormData) => { + try { + if (tokenData?.tokenId) { + // update + + await updateToken({ + identityId: tokenData.identityId, + tokenId: tokenData.tokenId, + name + }); + + handlePopUpToggle("token", false); + } else { + // create + + const newTokenData = await createToken({ + identityId: tokenData.identityId, + name + }); + + setToken(newTokenData.accessToken); + // note: may be helpful to tell user ttl etc. + } + + createNotification({ + text: `Successfully ${popUp?.token?.data ? "updated" : "created"} token`, + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = + error?.response?.data?.message ?? + `Failed to ${popUp?.token?.data ? "update" : "create"} token`; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("token", isOpen); + reset(); + setToken(""); + }} + > + + {!hasToken ? ( +
+ ( + + + + )} + /> +
+ + +
+ + ) : ( +
+

{token}

+ + { + navigator.clipboard.writeText(token); + setCopyTextToken("Copied"); + }} + > + + + +
+ )} +
+
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/index.tsx b/frontend/src/views/Org/IdentityPage/components/index.tsx new file mode 100644 index 0000000000..f9a810b7db --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/index.tsx @@ -0,0 +1,6 @@ +export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection/IdentityAuthenticationSection"; +export { IdentityClientSecretModal } from "./IdentityClientSecretModal"; +export { IdentityDetailsSection } from "./IdentityDetailsSection"; +export { IdentityProjectsSection } from "./IdentityProjectsSection"; +export { IdentityTokenListModal } from "./IdentityTokenListModal"; +export { IdentityTokenModal } from "./IdentityTokenModal"; diff --git a/frontend/src/views/Org/IdentityPage/index.tsx b/frontend/src/views/Org/IdentityPage/index.tsx new file mode 100644 index 0000000000..089416083c --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/index.tsx @@ -0,0 +1 @@ +export { IdentityPage } from "./IdentityPage"; 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 d347d422d8..3fbd15026b 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 @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityAwsAuth, + useDeleteIdentityAwsAuth, useGetIdentityAwsAuth, - useUpdateIdentityAwsAuth -} from "@app/hooks/api"; + useUpdateIdentityAwsAuth} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -63,6 +63,7 @@ export const IdentityAwsAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityAwsAuth(); const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? ""); @@ -329,23 +330,43 @@ export const IdentityAwsAuthForm = ({ Add IP Address
-
- - +
+
+ + +
+ {identityAuthMethodData?.authMethod && ( + + )}
); 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 8a99c633b7..ff6570d6c2 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 @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityAzureAuth, + useDeleteIdentityAzureAuth, useGetIdentityAzureAuth, - useUpdateIdentityAzureAuth -} from "@app/hooks/api"; + useUpdateIdentityAzureAuth} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -61,6 +61,7 @@ export const IdentityAzureAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityAzureAuth(); const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? ""); @@ -327,23 +328,43 @@ export const IdentityAzureAuthForm = ({ Add IP Address
-
- - +
+
+ + +
+ {identityAuthMethodData?.authMethod && ( + + )}
); 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 430115d353..2a5a10d610 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 @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityGcpAuth, + useDeleteIdentityGcpAuth, useGetIdentityGcpAuth, - useUpdateIdentityGcpAuth -} from "@app/hooks/api"; + useUpdateIdentityGcpAuth} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -62,6 +62,7 @@ export const IdentityGcpAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityGcpAuth(); const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? ""); @@ -361,23 +362,43 @@ export const IdentityGcpAuthForm = ({ Add IP Address
-
- - +
+
+ + +
+ {identityAuthMethodData?.authMethod && ( + + )}
); 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 abb541cfb5..f212aebbf4 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 @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input, TextArea } from "@app/component import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityKubernetesAuth, + useDeleteIdentityKubernetesAuth, useGetIdentityKubernetesAuth, - useUpdateIdentityKubernetesAuth -} from "@app/hooks/api"; + useUpdateIdentityKubernetesAuth} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -64,6 +64,7 @@ export const IdentityKubernetesAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityKubernetesAuth(); const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? ""); @@ -382,23 +383,43 @@ export const IdentityKubernetesAuthForm = ({ Add IP Address
-
- - +
+
+ + +
+ {identityAuthMethodData?.authMethod && ( + + )}
); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index e8b16cb5db..e5a1f8a02d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; +import { useRouter } from "next/router"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; @@ -16,8 +17,8 @@ import { import { useOrganization } from "@app/context"; import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; import { - IdentityAuthMethod - // useAddIdentityUniversalAuth + // IdentityAuthMethod, + useAddIdentityUniversalAuth } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -32,18 +33,19 @@ export type FormData = yup.InferType; type Props = { popUp: UsePopUpState<["identity"]>; - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["identityAuthMethod"]>, - data: { - identityId: string; - name: string; - authMethod?: IdentityAuthMethod; - } - ) => void; + // handlePopUpOpen: ( + // popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + // data: { + // identityId: string; + // name: string; + // authMethod?: IdentityAuthMethod; + // } + // ) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; }; -export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { +export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { + const router = useRouter(); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -51,7 +53,7 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro const { mutateAsync: createMutateAsync } = useCreateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); - // const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); + const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); const { control, @@ -113,32 +115,30 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro } else { // create - const { - id: createdId, - name: createdName, - authMethod - } = await createMutateAsync({ + const { id: createdId } = await createMutateAsync({ name, role: role || undefined, organizationId: orgId }); - // await addMutateAsync({ - // organizationId: orgId, - // identityId: createdId, - // clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], - // accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], - // accessTokenTTL: 2592000, - // accessTokenMaxTTL: 2592000, - // accessTokenNumUsesLimit: 0 - // }); - - handlePopUpToggle("identity", false); - handlePopUpOpen("identityAuthMethod", { + await addMutateAsync({ + organizationId: orgId, identityId: createdId, - name: createdName, - authMethod + clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0 }); + + handlePopUpToggle("identity", false); + router.push(`/org/${orgId}/identities/${createdId}`); + + // handlePopUpOpen("identityAuthMethod", { + // identityId: createdId, + // name: createdName, + // authMethod + // }); } createNotification({ diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModalV2.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModalV2.tsx deleted file mode 100644 index ea272fab5f..0000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModalV2.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { Controller, useForm } from "react-hook-form"; -import { faKey } from "@fortawesome/free-solid-svg-icons"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - -import { - Button, - EmptyState, - FormControl, - Input, - Modal, - ModalContent, - Tab, - Table, - TableContainer, - TabList, - TabPanel, - Tabs, - TBody, - Td, - Th, - THead, - Tr} from "@app/components/v2"; -import { IdentityAuthMethod } from "@app/hooks/api/identities"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -type Props = { - popUp: UsePopUpState<["identityModalV2", "upgradePlan"]>; - handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; - handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityModalV2", "upgradePlan"]>, - state?: boolean - ) => void; -}; - -enum TabSections { - AccessTokens = "access-tokens", - AuthMethod = "auth-method" -} - -const schema = z.object({ - name: z.string(), - role: z.string() -}); - -type FormData = z.infer; - -export const IdentityModalV2 = ({ popUp, handlePopUpToggle }: Props) => { - const { - control, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(schema), - defaultValues: { - name: "", - role: "" - } - }); - - const identityData = popUp?.identityModalV2?.data as { - identityId: string; - name: string; - authMethod?: IdentityAuthMethod; - }; - - return ( - { - handlePopUpToggle("identityModalV2", isOpen); - }} - > - -
-
-
-
-
-

Some ID

-
- ( - - - - )} - /> - ( - - - - )} - /> -
- -
-
-
- - - Access Tokens - Auth Method - - -
-
-

Access Token

- -
-

- Create an access token to authenticate with the API -

-
- - - - - - - - - - - - - -
TokenExpires -
- -
-
-
- Amx -
-
-
- {/* ( - - - - )} - /> */} - {/* {renderIdentityAuthForm()} */} - {/* handlePopUpToggle("upgradePlan", isOpen)} - text="You can use IP allowlisting if you switch to Infisical's Pro plan." - /> */} -
-
- ); -}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index dce8adcb13..5cc18ff13b 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -17,11 +17,9 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal"; import { IdentityModal } from "./IdentityModal"; -// new -import { IdentityModalV2 } from "./IdentityModalV2"; import { IdentityTable } from "./IdentityTable"; import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; -import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal"; +// import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal"; export const IdentitySection = withPermission( () => { @@ -32,7 +30,6 @@ export const IdentitySection = withPermission( const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "identity", - "identityModalV2", // TODO: update "identityAuthMethod", "deleteIdentity", "universalAuthClientSecret", @@ -111,27 +108,18 @@ export const IdentitySection = withPermission( )}
- - - + + - + /> */} , - data?: { - identityId?: string; - name?: string; - authMethod?: string; - role?: string; - customRole?: { - name: string; - slug: string; - }; - accessToken?: string; - } - ) => void; -}; +import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; -export const IdentityTable = ({ handlePopUpOpen }: Props) => { +export const IdentityTable = () => { + const router = useRouter(); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); - const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); const { data, isLoading } = useGetIdentityMembershipOrgs(orgId); const { data: roles } = useGetOrgRoles(orgId); @@ -108,7 +63,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { Name Role - Auth Method @@ -117,22 +71,11 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { {!isLoading && data && data.length > 0 && - data.map(({ identity: { id, name, authMethod }, role, customRole }) => { + data.map(({ identity: { id, name }, role, customRole }) => { return ( - + {name} { }} - {authMethod ? identityAuthToNameMap[authMethod] : "Not configured"}
- {authMethod === IdentityAuthMethod.TOKEN_AUTH && ( - - { - const newTokenData = await createToken({ - identityId: id, - organizationId: orgId - }); - - handlePopUpOpen("tokenAuthToken", { - accessToken: newTokenData.accessToken - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - > - - - - )} - {authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && ( - - { - handlePopUpOpen("universalAuthClientSecret", { - identityId: id, - name - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - > - - - - )} - router.push(`/org/${orgId}/identities/${id}`)} > - {(isAllowed) => ( - - { - handlePopUpOpen("identityAuthMethod", { - identityId: id, - name, - authMethod - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - isDisabled={!isAllowed} - > - - - - )} - - - -
- - - -
-
- - - {(isAllowed) => ( - { - if (!isAllowed) return; - handlePopUpOpen("identity", { - identityId: id, - name, - role, - customRole - }); - }} - disabled={!isAllowed} - icon={} - > - Update identity - - )} - - - {(isAllowed) => ( - { - if (!isAllowed) return; - handlePopUpOpen("deleteIdentity", { - identityId: id, - name - }); - }} - icon={} - > - Delete identity - - )} - - { - navigator.clipboard.writeText(id); - createNotification({ - text: "Copied identity internal ID to clipboard", - type: "success" - }); - }} - icon={} - > - Copy Identity ID - - -
+ +
diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx index b8e5c22a73..09e486d1d8 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx @@ -9,9 +9,9 @@ import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityTokenAuth, + useDeleteIdentityTokenAuth, useGetIdentityTokenAuth, - useUpdateIdentityTokenAuth -} from "@app/hooks/api"; + useUpdateIdentityTokenAuth} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -56,6 +56,7 @@ export const IdentityTokenAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth(); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityTokenAuth(); const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? ""); @@ -239,23 +240,43 @@ export const IdentityTokenAuthForm = ({ Add IP Address
-
- - +
+
+ + +
+ {identityAuthMethodData?.authMethod && ( + + )}
); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx index 11ad28f7be..226743739b 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx @@ -10,7 +10,7 @@ import * as yup from "yup"; import { createNotification } from "@app/components/notifications"; import { Button, - DeleteActionModal, + // DeleteActionModal, EmptyState, FormControl, IconButton, @@ -30,8 +30,7 @@ import { useToggle } from "@app/hooks"; import { useCreateIdentityUniversalAuthClientSecret, useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets, - useRevokeIdentityUniversalAuthClientSecret + useGetIdentityUniversalAuthClientSecrets } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -44,18 +43,16 @@ const schema = yup.object({ export type FormData = yup.InferType; type Props = { - popUp: UsePopUpState<["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]>; + popUp: UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>; handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteUniversalAuthClientSecret"]>, + popUpName: keyof UsePopUpState<["revokeClientSecret"]>, data?: { clientSecretPrefix: string; clientSecretId: string; } ) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState< - ["universalAuthClientSecret", "deleteUniversalAuthClientSecret"] - >, + popUpName: keyof UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>, state?: boolean ) => void; }; @@ -66,7 +63,7 @@ export const IdentityUniversalAuthClientSecretModal = ({ handlePopUpToggle }: Props) => { const { t } = useTranslation(); - + const [token, setToken] = useState(""); const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false); const [isClientIdCopied, setIsClientIdCopied] = useToggle(false); @@ -81,8 +78,6 @@ export const IdentityUniversalAuthClientSecretModal = ({ const { mutateAsync: createClientSecretMutateAsync } = useCreateIdentityUniversalAuthClientSecret(); - const { mutateAsync: revokeClientSecretMutateAsync } = - useRevokeIdentityUniversalAuthClientSecret(); const { control, @@ -142,41 +137,6 @@ export const IdentityUniversalAuthClientSecretModal = ({ } }; - const onDeleteClientSecretSubmit = async ({ - clientSecretId, - clientSecretPrefix - }: { - clientSecretId: string; - clientSecretPrefix: string; - }) => { - try { - if (!popUpData?.identityId) return; - - await revokeClientSecretMutateAsync({ - identityId: popUpData.identityId, - clientSecretId - }); - - if (token.startsWith(clientSecretPrefix)) { - reset(); - setToken(""); - } - - handlePopUpToggle("deleteUniversalAuthClientSecret", false); - - createNotification({ - text: "Successfully deleted client secret", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete client secret", - type: "error" - }); - } - }; - const hasToken = Boolean(token); return ( @@ -346,7 +306,7 @@ export const IdentityUniversalAuthClientSecretModal = ({ { - handlePopUpOpen("deleteUniversalAuthClientSecret", { + handlePopUpOpen("revokeClientSecret", { clientSecretPrefix, clientSecretId: id }); @@ -376,26 +336,6 @@ export const IdentityUniversalAuthClientSecretModal = ({ - handlePopUpToggle("deleteUniversalAuthClientSecret", isOpen)} - deleteKey="confirm" - onDeleteApproved={() => { - const deleteClientSecretData = popUp?.deleteUniversalAuthClientSecret?.data as { - clientSecretId: string; - clientSecretPrefix: string; - }; - - return onDeleteClientSecretSubmit({ - clientSecretId: deleteClientSecretData.clientSecretId, - clientSecretPrefix: deleteClientSecretData.clientSecretPrefix - }); - }} - /> ); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx index af82082a51..be9235b377 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityUniversalAuth, + useDeleteIdentityUniversalAuth, useGetIdentityUniversalAuth, - useUpdateIdentityUniversalAuth -} from "@app/hooks/api"; + useUpdateIdentityUniversalAuth} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -68,6 +68,7 @@ export const IdentityUniversalAuthForm = ({ const { subscription } = useSubscription(); const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth(); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityUniversalAuth(); const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? ""); const { @@ -368,23 +369,43 @@ export const IdentityUniversalAuthForm = ({ Add IP Address
-
- - +
+
+ + +
+ {identityAuthMethodData?.authMethod && ( + + )}
); From 59ccabec6955a22ccd744cb3c67fdc117eb1b539 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 9 Jul 2024 11:51:21 +0700 Subject: [PATCH 5/5] Make fixes based on review --- backend/src/server/routes/index.ts | 3 +- .../routes/v1/identity-access-token-router.ts | 35 ------ .../routes/v1/identity-token-auth-router.ts | 59 +++++++-- .../identity-access-token-service.ts | 52 +------- .../identity-access-token-types.ts | 6 - .../identity-token-auth-service.ts | 99 ++++++++++----- .../identity-token-auth-types.ts | 11 +- .../src/services/identity/identity-service.ts | 4 +- frontend/src/hooks/api/identities/index.tsx | 9 +- .../src/hooks/api/identities/mutations.tsx | 17 +-- .../views/Org/IdentityPage/IdentityPage.tsx | 11 +- .../IdentityAuthenticationSection.tsx | 12 -- .../IdentityClientSecrets.tsx | 24 +++- .../IdentityTokens.tsx | 24 ++-- .../components/IdentityDetailsSection.tsx | 26 +++- .../components/IdentityTokenModal.tsx | 4 +- .../IdentityAuthMethodModal.tsx | 113 +++++++++++++++++- .../IdentitySection/IdentityAwsAuthForm.tsx | 18 +-- .../IdentitySection/IdentityAzureAuthForm.tsx | 18 +-- .../IdentitySection/IdentityGcpAuthForm.tsx | 18 +-- .../IdentityKubernetesAuthForm.tsx | 18 +-- .../IdentitySection/IdentitySection.tsx | 6 +- .../IdentitySection/IdentityTable.tsx | 10 +- .../IdentitySection/IdentityTokenAuthForm.tsx | 18 +-- .../IdentityUniversalAuthForm.tsx | 18 +-- 25 files changed, 357 insertions(+), 276 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 15221e55be..069d615693 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -816,8 +816,7 @@ export const registerRoutes = async ( }); const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL, - identityOrgMembershipDAL, - permissionService + identityOrgMembershipDAL }); const identityProjectService = identityProjectServiceFactory({ permissionService, diff --git a/backend/src/server/routes/v1/identity-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts index 8f65a99849..7ed62e6798 100644 --- a/backend/src/server/routes/v1/identity-access-token-router.ts +++ b/backend/src/server/routes/v1/identity-access-token-router.ts @@ -2,8 +2,6 @@ import { z } from "zod"; import { UNIVERSAL_AUTH } from "@app/lib/api-docs"; import { writeLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvider) => { server.route({ @@ -63,37 +61,4 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid }; } }); - - server.route({ - url: "/token/revoke-by-id", - method: "POST", - config: { - rateLimit: writeLimit - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - schema: { - description: "Revoke access token by the id of the token", - body: z.object({ - tokenId: z.string().trim() - }), - response: { - 200: z.object({ - message: z.string() - }) - } - }, - handler: async (req) => { - await server.services.identityAccessToken.revokeAccessTokenById({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); - - return { - message: "Successfully revoked access token" - }; - } - }); }; diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index 6e06c5b6ab..ac36c3c587 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -253,6 +253,15 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }); + // proposed + // update token by id: PATCH /token-auth/tokens/:tokenId + // revoke token by id: POST /token-auth/tokens/:tokenId/revoke + + // current + // revoke token by id: POST /token/revoke-by-id + + // token-auth/identities/:identityId/tokens + server.route({ method: "POST", url: "/token-auth/identities/:identityId/tokens", @@ -284,7 +293,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, handler: async (req) => { const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } = - await server.services.identityTokenAuth.createTokenTokenAuth({ + await server.services.identityTokenAuth.createTokenAuthToken({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -342,7 +351,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }, handler: async (req) => { - const { tokens, identityMembershipOrg } = await server.services.identityTokenAuth.getTokensTokenAuth({ + const { tokens, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokens({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -368,7 +377,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider server.route({ method: "PATCH", - url: "/token-auth/identities/:identityId/tokens/:tokenId", + url: "/token-auth/tokens/:tokenId", config: { rateLimit: writeLimit }, @@ -381,7 +390,6 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } ], params: z.object({ - identityId: z.string(), tokenId: z.string() }), body: z.object({ @@ -394,12 +402,11 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }, handler: async (req) => { - const { token, identityMembershipOrg } = await server.services.identityTokenAuth.updateTokenTokenAuth({ + const { token, identityMembershipOrg } = await server.services.identityTokenAuth.updateTokenAuthToken({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - identityId: req.params.identityId, tokenId: req.params.tokenId, ...req.body }); @@ -410,7 +417,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider event: { type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH, metadata: { - identityId: req.params.identityId, + identityId: token.identityId, tokenId: token.id, name: req.body.name } @@ -420,4 +427,42 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider return { token }; } }); + + server.route({ + method: "POST", + url: "/token-auth/tokens/:tokenId/revoke", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Revoke token for identity with Token Auth configured", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + tokenId: z.string() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.identityTokenAuth.revokeTokenAuthToken({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + tokenId: req.params.tokenId + }); + + return { + message: "Successfully revoked access token" + }; + } + }); }; 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 5998c172cf..1da8c06eed 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,9 +1,6 @@ -import { ForbiddenError } from "@casl/ability"; import jwt, { JwtPayload } from "jsonwebtoken"; import { TableName, TIdentityAccessTokens } from "@app/db/schemas"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; @@ -11,24 +8,18 @@ import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; import { AuthTokenType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; -import { - TIdentityAccessTokenJwtPayload, - TRenewAccessTokenDTO, - TRevokeAccessTokenByIdDTO -} from "./identity-access-token-types"; +import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; type TIdentityAccessTokenServiceFactoryDep = { identityAccessTokenDAL: TIdentityAccessTokenDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory; - permissionService: Pick; }; export type TIdentityAccessTokenServiceFactory = ReturnType; export const identityAccessTokenServiceFactory = ({ identityAccessTokenDAL, - identityOrgMembershipDAL, - permissionService + identityOrgMembershipDAL }: TIdentityAccessTokenServiceFactoryDep) => { const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => { const { @@ -147,43 +138,6 @@ export const identityAccessTokenServiceFactory = ({ return { revokedToken }; }; - const revokeAccessTokenById = async ({ - tokenId, - actorId, - actor, - actorAuthMethod, - actorOrgId - }: TRevokeAccessTokenByIdDTO) => { - const identityAccessToken = await identityAccessTokenDAL.findOne({ - [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, - isAccessTokenRevoked: false - }); - if (!identityAccessToken) throw new UnauthorizedError(); - - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ - identityId: identityAccessToken.identityId - }); - - if (!identityOrgMembership) { - throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); - } - - const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - identityOrgMembership.orgId, - actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - - const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, { - isAccessTokenRevoked: true - }); - - return { revokedToken }; - }; - const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, @@ -221,5 +175,5 @@ export const identityAccessTokenServiceFactory = ({ return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; }; - return { renewAccessToken, revokeAccessToken, revokeAccessTokenById, fnValidateIdentityAccessToken }; + return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; }; diff --git a/backend/src/services/identity-access-token/identity-access-token-types.ts b/backend/src/services/identity-access-token/identity-access-token-types.ts index 5c67454ad1..86967df76d 100644 --- a/backend/src/services/identity-access-token/identity-access-token-types.ts +++ b/backend/src/services/identity-access-token/identity-access-token-types.ts @@ -1,5 +1,3 @@ -import { TProjectPermission } from "@app/lib/types"; - export type TRenewAccessTokenDTO = { accessToken: string; }; @@ -10,7 +8,3 @@ export type TIdentityAccessTokenJwtPayload = { identityAccessTokenId: string; authTokenType: string; }; - -export type TRevokeAccessTokenByIdDTO = { - tokenId: string; -} & Omit; 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 c331472385..198b76266f 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 @@ -1,13 +1,13 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { IdentityAuthMethod, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -18,12 +18,13 @@ import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identit import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal"; import { TAttachTokenAuthDTO, - TCreateTokenTokenAuthDTO, + TCreateTokenAuthTokenDTO, TGetTokenAuthDTO, - TGetTokensTokenAuthDTO, + TGetTokenAuthTokensDTO, TRevokeTokenAuthDTO, + TRevokeTokenAuthTokenDTO, TUpdateTokenAuthDTO, - TUpdateTokenTokenAuthDTO + TUpdateTokenAuthTokenDTO } from "./identity-token-auth-types"; type TIdentityTokenAuthServiceFactoryDep = { @@ -33,7 +34,10 @@ type TIdentityTokenAuthServiceFactoryDep = { >; identityDAL: Pick; identityOrgMembershipDAL: Pick; - identityAccessTokenDAL: Pick; + identityAccessTokenDAL: Pick< + TIdentityAccessTokenDALFactory, + "create" | "find" | "update" | "findById" | "findOne" | "updateById" + >; permissionService: Pick; licenseService: Pick; }; @@ -243,7 +247,7 @@ export const identityTokenAuthServiceFactory = ({ ); const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); if (!hasPriviledge) - throw new ForbiddenRequestError({ + throw new UnauthorizedError({ message: "Failed to revoke Token Auth of identity with more privileged role" }); @@ -255,14 +259,14 @@ export const identityTokenAuthServiceFactory = ({ return revokedIdentityTokenAuth; }; - const createTokenTokenAuth = async ({ + const createTokenAuthToken = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId, name - }: TCreateTokenTokenAuthDTO) => { + }: TCreateTokenAuthTokenDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) @@ -328,7 +332,7 @@ export const identityTokenAuthServiceFactory = ({ return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg }; }; - const getTokensTokenAuth = async ({ + const getTokenAuthTokens = async ({ identityId, offset = 0, limit = 20, @@ -336,7 +340,7 @@ export const identityTokenAuthServiceFactory = ({ actor, actorAuthMethod, actorOrgId - }: TGetTokensTokenAuthDTO) => { + }: TGetTokenAuthTokensDTO) => { const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) @@ -350,20 +354,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, - actorAuthMethod, - actorOrgId - ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPriviledge) - throw new ForbiddenRequestError({ - message: "Failed to get tokens for identity with more privileged role" - }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); const tokens = await identityAccessTokenDAL.find( { @@ -375,16 +366,17 @@ export const identityTokenAuthServiceFactory = ({ return { tokens, identityMembershipOrg }; }; - const updateTokenTokenAuth = async ({ - identityId, + const updateTokenAuthToken = async ({ tokenId, name, actorId, actor, actorAuthMethod, actorOrgId - }: TUpdateTokenTokenAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + }: TUpdateTokenAuthTokenDTO) => { + const foundToken = await identityAccessTokenDAL.findById(tokenId); + if (!foundToken) throw new NotFoundError({ message: "Failed to find token" }); + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId }); if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) throw new BadRequestError({ @@ -414,7 +406,7 @@ export const identityTokenAuthServiceFactory = ({ const [token] = await identityAccessTokenDAL.update( { - identityId, + identityId: foundToken.identityId, id: tokenId }, { @@ -425,13 +417,54 @@ export const identityTokenAuthServiceFactory = ({ return { token, identityMembershipOrg }; }; + const revokeTokenAuthToken = async ({ + tokenId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeTokenAuthTokenDTO) => { + const identityAccessToken = await identityAccessTokenDAL.findOne({ + [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, + isAccessTokenRevoked: false + }); + if (!identityAccessToken) + throw new NotFoundError({ + message: "Failed to find token" + }); + + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ + identityId: identityAccessToken.identityId + }); + + if (!identityOrgMembership) { + throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityOrgMembership.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, { + isAccessTokenRevoked: true + }); + + return { revokedToken }; + }; + return { attachTokenAuth, updateTokenAuth, getTokenAuth, revokeIdentityTokenAuth, - createTokenTokenAuth, - getTokensTokenAuth, - updateTokenTokenAuth + createTokenAuthToken, + getTokenAuthTokens, + updateTokenAuthToken, + revokeTokenAuthToken }; }; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts index 4419abc36c..12c689728b 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-types.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -24,19 +24,22 @@ export type TRevokeTokenAuthDTO = { identityId: string; } & Omit; -export type TCreateTokenTokenAuthDTO = { +export type TCreateTokenAuthTokenDTO = { identityId: string; name?: string; } & Omit; -export type TGetTokensTokenAuthDTO = { +export type TGetTokenAuthTokensDTO = { identityId: string; offset: number; limit: number; } & Omit; -export type TUpdateTokenTokenAuthDTO = { - identityId: string; +export type TUpdateTokenAuthTokenDTO = { tokenId: string; name?: string; } & Omit; + +export type TRevokeTokenAuthTokenDTO = { + tokenId: string; +} & Omit; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index d3fc9c788b..78dcdda4e7 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -5,7 +5,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TOrgPermission } from "@app/lib/types"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; @@ -213,7 +213,7 @@ export const identityServiceFactory = ({ actorOrgId }: TListProjectIdentitiesByIdentityIdDTO) => { const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId }); - if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); + if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); const { permission } = await permissionService.getOrgPermission( actor, diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index b4d4f9e6d2..ceece2c13d 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -17,16 +17,16 @@ export { useDeleteIdentityKubernetesAuth, useDeleteIdentityTokenAuth, useDeleteIdentityUniversalAuth, + useRevokeIdentityTokenAuthToken, useRevokeIdentityUniversalAuthClientSecret, - useRevokeToken, useUpdateIdentity, useUpdateIdentityAwsAuth, useUpdateIdentityAzureAuth, useUpdateIdentityGcpAuth, useUpdateIdentityKubernetesAuth, useUpdateIdentityTokenAuth, - useUpdateIdentityUniversalAuth, - useUpdateTokenIdentityTokenAuth} from "./mutations"; + useUpdateIdentityTokenAuthToken, + useUpdateIdentityUniversalAuth} from "./mutations"; export { useGetIdentityAwsAuth, useGetIdentityAzureAuth, @@ -37,4 +37,5 @@ export { useGetIdentityTokenAuth, useGetIdentityTokensTokenAuth, useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets} from "./queries"; + useGetIdentityUniversalAuthClientSecrets +} from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 323b45fd53..86fca63e99 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -42,7 +42,8 @@ import { UpdateIdentityKubernetesAuthDTO, UpdateIdentityTokenAuthDTO, UpdateIdentityUniversalAuthDTO, - UpdateTokenIdentityTokenAuthDTO} from "./types"; + UpdateTokenIdentityTokenAuthDTO +} from "./types"; export const useCreateIdentity = () => { const queryClient = useQueryClient(); @@ -706,14 +707,14 @@ export const useCreateTokenIdentityTokenAuth = () => { }); }; -export const useUpdateTokenIdentityTokenAuth = () => { +export const useUpdateIdentityTokenAuthToken = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ identityId, tokenId, name }) => { + mutationFn: async ({ tokenId, name }) => { const { data: { token } } = await apiRequest.patch<{ token: IdentityAccessToken }>( - `/api/v1/auth/token-auth/identities/${identityId}/tokens/${tokenId}`, + `/api/v1/auth/token-auth/tokens/${tokenId}`, { name } @@ -727,13 +728,13 @@ export const useUpdateTokenIdentityTokenAuth = () => { }); }; -export const useRevokeToken = () => { +export const useRevokeIdentityTokenAuthToken = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ tokenId }) => { - const { data } = await apiRequest.post("/api/v1/auth/token/revoke-by-id", { - tokenId - }); + const { data } = await apiRequest.post( + `/api/v1/auth/token-auth/tokens/${tokenId}/revoke` + ); return data; }, diff --git a/frontend/src/views/Org/IdentityPage/IdentityPage.tsx b/frontend/src/views/Org/IdentityPage/IdentityPage.tsx index 548ea22667..1be789baa8 100644 --- a/frontend/src/views/Org/IdentityPage/IdentityPage.tsx +++ b/frontend/src/views/Org/IdentityPage/IdentityPage.tsx @@ -21,9 +21,8 @@ import { withPermission } from "@app/hoc"; import { useDeleteIdentity, useGetIdentityById, - useRevokeIdentityUniversalAuthClientSecret, - useRevokeToken -} from "@app/hooks/api"; + useRevokeIdentityTokenAuthToken, + useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; @@ -35,7 +34,8 @@ import { IdentityDetailsSection, IdentityProjectsSection, IdentityTokenListModal, - IdentityTokenModal} from "./components"; + IdentityTokenModal +} from "./components"; export const IdentityPage = withPermission( () => { @@ -45,13 +45,14 @@ export const IdentityPage = withPermission( const orgId = currentOrg?.id || ""; const { data } = useGetIdentityById(identityId); const { mutateAsync: deleteIdentity } = useDeleteIdentity(); - const { mutateAsync: revokeToken } = useRevokeToken(); + const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken(); const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "identity", "deleteIdentity", "identityAuthMethod", + "revokeAuthMethod", "token", "tokenList", "revokeToken", diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx index 7aad5c558e..be7c744ced 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx @@ -66,18 +66,6 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P

Auth Method

- {/* */}

{data.identity.authMethod diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx index 781bb915c9..21402fb6da 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx @@ -1,10 +1,11 @@ -import { faKey, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faCopy,faKey, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button, IconButton, Tooltip } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useTimedReset } from "@app/hooks"; import { useGetIdentityById, useGetIdentityUniversalAuth, @@ -25,6 +26,10 @@ type Props = { const SHOW_LIMIT = 3; export const IdentityClientSecrets = ({ identityId, handlePopUpOpen }: Props) => { + const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset({ + initialState: "Copy Client ID to clipboard" + }); + const { data } = useGetIdentityById(identityId); const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(identityId); const { data: clientSecrets } = useGetIdentityUniversalAuthClientSecrets(identityId); @@ -32,7 +37,22 @@ export const IdentityClientSecrets = ({ identityId, handlePopUpOpen }: Props) =>

Client ID

-

{identityUniversalAuth?.clientId ?? ""}

+
+

{identityUniversalAuth?.clientId ?? ""}

+ + { + navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? ""); + setCopyTextClientId("Copied"); + }} + > + + + +
{clientSecrets?.length ? (
diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx index e97b2faec5..64a4c96b20 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx @@ -86,17 +86,19 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => { > Edit Token - { - handlePopUpOpen("revokeToken", { - identityId, - tokenId: token.id, - name: token.name - }); - }} - > - Revoke Token - + {!token.isAccessTokenRevoked && ( + { + handlePopUpOpen("revokeToken", { + identityId, + tokenId: token.id, + name: token.name + }); + }} + > + Revoke Token + + )}
diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx index 6a6843d851..c6d7f0ce14 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx @@ -1,9 +1,10 @@ -import { faPencil } from "@fortawesome/free-solid-svg-icons"; +import { faCheck,faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OrgPermissionCan } from "@app/components/permissions"; -import { IconButton,Tooltip } from "@app/components/v2"; +import { IconButton, Tooltip } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useTimedReset } from "@app/hooks"; import { useGetIdentityById } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -16,6 +17,10 @@ type Props = { }; export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) => { + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + const { data } = useGetIdentityById(identityId); return data ? (
@@ -49,7 +54,22 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =

ID

-

{data.identity.id}

+
+

{data.identity.id}

+ + { + navigator.clipboard.writeText(data.identity.id); + setCopyTextId("Copied"); + }} + > + + + +

Name

diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx index 24a2a6bafb..dfc26592e4 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx @@ -16,7 +16,7 @@ import { Tooltip } from "@app/components/v2"; import { useTimedReset } from "@app/hooks"; -import { useCreateTokenIdentityTokenAuth, useUpdateTokenIdentityTokenAuth } from "@app/hooks/api"; +import { useCreateTokenIdentityTokenAuth, useUpdateIdentityTokenAuthToken } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z @@ -34,7 +34,7 @@ type Props = { export const IdentityTokenModal = ({ popUp, handlePopUpToggle }: Props) => { const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth(); - const { mutateAsync: updateToken } = useUpdateTokenIdentityTokenAuth(); + const { mutateAsync: updateToken } = useUpdateIdentityTokenAuthToken(); const [token, setToken] = useState(""); const [copyTextToken, isCopyingToken, setCopyTextToken] = useTimedReset({ initialState: "Copy to clipboard" 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 bee7b65a6b..5acdd6e3b6 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 @@ -3,15 +3,24 @@ 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 { + DeleteActionModal, FormControl, Modal, ModalContent, Select, SelectItem, - UpgradePlanModal -} from "@app/components/v2"; -import { IdentityAuthMethod } from "@app/hooks/api/identities"; + UpgradePlanModal} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useDeleteIdentityAwsAuth, + useDeleteIdentityAzureAuth, + useDeleteIdentityGcpAuth, + useDeleteIdentityKubernetesAuth, + useDeleteIdentityTokenAuth, + useDeleteIdentityUniversalAuth} from "@app/hooks/api"; +import { IdentityAuthMethod , identityAuthToNameMap } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; @@ -22,10 +31,10 @@ import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; type Props = { - popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>; + popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>, state?: boolean ) => void; }; @@ -48,6 +57,16 @@ const schema = yup 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 { control, watch, setValue } = useForm({ resolver: yupResolver(schema), defaultValues: { @@ -128,13 +147,84 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog /> ); } - default: { return
; } } }; + const onRevokeAuthMethodSubmit = async () => { + if (!identityAuthMethodData.authMethod) return; + if (!orgId) return; + try { + console.log("onRevokeAuthMethodSubmit identityId: ", identityAuthMethodData); + switch (identityAuthMethodData.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; + } + default: + break; + } + + createNotification({ + text: `Successfully removed ${ + identityAuthToNameMap[identityAuthMethodData.authMethod] + } on ${identityAuthMethodData.name}`, + type: "success" + }); + + handlePopUpToggle("revokeAuthMethod", false); + handlePopUpToggle("identityAuthMethod", false); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to remove ${identityAuthToNameMap[identityAuthMethodData.authMethod]} on ${ + identityAuthMethodData.name + }`, + type: "error" + }); + } + }; + return ( handlePopUpToggle("upgradePlan", isOpen)} text="You can use IP allowlisting if you switch to Infisical's Pro plan." /> + handlePopUpToggle("revokeAuthMethod", isOpen)} + deleteKey="confirm" + onDeleteApproved={onRevokeAuthMethodSubmit} + /> ); 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 3fbd15026b..9c81f308a0 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 @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityAwsAuth, - useDeleteIdentityAwsAuth, useGetIdentityAwsAuth, - useUpdateIdentityAwsAuth} from "@app/hooks/api"; + useUpdateIdentityAwsAuth +} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -42,7 +42,7 @@ export type FormData = yup.InferType; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, state?: boolean ) => void; identityAuthMethodData: { @@ -63,7 +63,6 @@ export const IdentityAwsAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityAwsAuth(); const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? ""); @@ -346,7 +345,7 @@ export const IdentityAwsAuthForm = ({ variant="plain" onClick={() => handlePopUpToggle("identityAuthMethod", false)} > - {identityAuthMethodData?.authMethod ? "Cancel" : "Skip"} + Cancel
{identityAuthMethodData?.authMethod && ( @@ -355,14 +354,7 @@ export const IdentityAwsAuthForm = ({ colorSchema="danger" isLoading={isSubmitting} isDisabled={isSubmitting} - onClick={async () => { - await deleteMutateAsync({ - identityId: identityAuthMethodData.identityId, - organizationId: orgId - }); - - handlePopUpToggle("identityAuthMethod", false); - }} + onClick={() => handlePopUpToggle("revokeAuthMethod", true)} > Remove Auth Method 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 ff6570d6c2..c0aaca2f6a 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 @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityAzureAuth, - useDeleteIdentityAzureAuth, useGetIdentityAzureAuth, - useUpdateIdentityAzureAuth} from "@app/hooks/api"; + useUpdateIdentityAzureAuth +} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -40,7 +40,7 @@ export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, state?: boolean ) => void; identityAuthMethodData: { @@ -61,7 +61,6 @@ export const IdentityAzureAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityAzureAuth(); const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? ""); @@ -344,7 +343,7 @@ export const IdentityAzureAuthForm = ({ variant="plain" onClick={() => handlePopUpToggle("identityAuthMethod", false)} > - {identityAuthMethodData?.authMethod ? "Cancel" : "Skip"} + Cancel
{identityAuthMethodData?.authMethod && ( @@ -353,14 +352,7 @@ export const IdentityAzureAuthForm = ({ colorSchema="danger" isLoading={isSubmitting} isDisabled={isSubmitting} - onClick={async () => { - await deleteMutateAsync({ - identityId: identityAuthMethodData.identityId, - organizationId: orgId - }); - - handlePopUpToggle("identityAuthMethod", false); - }} + onClick={() => handlePopUpToggle("revokeAuthMethod", true)} > Remove Auth Method 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 2a5a10d610..eb07c3c364 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 @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityGcpAuth, - useDeleteIdentityGcpAuth, useGetIdentityGcpAuth, - useUpdateIdentityGcpAuth} from "@app/hooks/api"; + useUpdateIdentityGcpAuth +} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -41,7 +41,7 @@ export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, state?: boolean ) => void; identityAuthMethodData: { @@ -62,7 +62,6 @@ export const IdentityGcpAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityGcpAuth(); const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? ""); @@ -378,7 +377,7 @@ export const IdentityGcpAuthForm = ({ variant="plain" onClick={() => handlePopUpToggle("identityAuthMethod", false)} > - {identityAuthMethodData?.authMethod ? "Cancel" : "Skip"} + Cancel
{identityAuthMethodData?.authMethod && ( @@ -387,14 +386,7 @@ export const IdentityGcpAuthForm = ({ colorSchema="danger" isLoading={isSubmitting} isDisabled={isSubmitting} - onClick={async () => { - await deleteMutateAsync({ - identityId: identityAuthMethodData.identityId, - organizationId: orgId - }); - - handlePopUpToggle("identityAuthMethod", false); - }} + onClick={() => handlePopUpToggle("revokeAuthMethod", true)} > Remove Auth Method 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 f212aebbf4..6baa6a4050 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 @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input, TextArea } from "@app/component import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityKubernetesAuth, - useDeleteIdentityKubernetesAuth, useGetIdentityKubernetesAuth, - useUpdateIdentityKubernetesAuth} from "@app/hooks/api"; + useUpdateIdentityKubernetesAuth +} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -43,7 +43,7 @@ export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, state?: boolean ) => void; identityAuthMethodData: { @@ -64,7 +64,6 @@ export const IdentityKubernetesAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityKubernetesAuth(); const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? ""); @@ -399,7 +398,7 @@ export const IdentityKubernetesAuthForm = ({ variant="plain" onClick={() => handlePopUpToggle("identityAuthMethod", false)} > - {identityAuthMethodData?.authMethod ? "Cancel" : "Skip"} + Cancel
{identityAuthMethodData?.authMethod && ( @@ -408,14 +407,7 @@ export const IdentityKubernetesAuthForm = ({ colorSchema="danger" isLoading={isSubmitting} isDisabled={isSubmitting} - onClick={async () => { - await deleteMutateAsync({ - identityId: identityAuthMethodData.identityId, - organizationId: orgId - }); - - handlePopUpToggle("identityAuthMethod", false); - }} + onClick={() => handlePopUpToggle("revokeAuthMethod", true)} > Remove Auth Method diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 5cc18ff13b..5cb615ebba 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -15,7 +15,7 @@ import { withPermission } from "@app/hoc"; import { useDeleteIdentity } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; -import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal"; +// import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal"; import { IdentityModal } from "./IdentityModal"; import { IdentityTable } from "./IdentityTable"; import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; @@ -110,11 +110,11 @@ export const IdentitySection = withPermission(
- + /> */} {/* { {isLoading && } {!isLoading && - data && - data.length > 0 && - data.map(({ identity: { id, name }, role, customRole }) => { + data?.map(({ identity: { id, name }, role, customRole }) => { return ( - + router.push(`/org/${orgId}/identities/${id}`)} + > {name} diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx index 09e486d1d8..b28af537ff 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx @@ -9,9 +9,9 @@ import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityTokenAuth, - useDeleteIdentityTokenAuth, useGetIdentityTokenAuth, - useUpdateIdentityTokenAuth} from "@app/hooks/api"; + useUpdateIdentityTokenAuth +} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -35,7 +35,7 @@ export type FormData = z.infer; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, state?: boolean ) => void; identityAuthMethodData: { @@ -56,7 +56,6 @@ export const IdentityTokenAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth(); - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityTokenAuth(); const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? ""); @@ -256,7 +255,7 @@ export const IdentityTokenAuthForm = ({ variant="plain" onClick={() => handlePopUpToggle("identityAuthMethod", false)} > - {identityAuthMethodData?.authMethod ? "Cancel" : "Skip"} + Cancel
{identityAuthMethodData?.authMethod && ( @@ -265,14 +264,7 @@ export const IdentityTokenAuthForm = ({ colorSchema="danger" isLoading={isSubmitting} isDisabled={isSubmitting} - onClick={async () => { - await deleteMutateAsync({ - identityId: identityAuthMethodData.identityId, - organizationId: orgId - }); - - handlePopUpToggle("identityAuthMethod", false); - }} + onClick={() => handlePopUpToggle("revokeAuthMethod", true)} > Remove Auth Method diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx index be9235b377..2e3df29f2c 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx @@ -10,9 +10,9 @@ import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityUniversalAuth, - useDeleteIdentityUniversalAuth, useGetIdentityUniversalAuth, - useUpdateIdentityUniversalAuth} from "@app/hooks/api"; + useUpdateIdentityUniversalAuth +} from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -48,7 +48,7 @@ export type FormData = yup.InferType; type Props = { handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, state?: boolean ) => void; identityAuthMethodData: { @@ -68,7 +68,6 @@ export const IdentityUniversalAuthForm = ({ const { subscription } = useSubscription(); const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth(); - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityUniversalAuth(); const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? ""); const { @@ -385,7 +384,7 @@ export const IdentityUniversalAuthForm = ({ variant="plain" onClick={() => handlePopUpToggle("identityAuthMethod", false)} > - {identityAuthMethodData?.authMethod ? "Cancel" : "Skip"} + Cancel
{identityAuthMethodData?.authMethod && ( @@ -394,14 +393,7 @@ export const IdentityUniversalAuthForm = ({ colorSchema="danger" isLoading={isSubmitting} isDisabled={isSubmitting} - onClick={async () => { - await deleteMutateAsync({ - identityId: identityAuthMethodData.identityId, - organizationId: orgId - }); - - handlePopUpToggle("identityAuthMethod", false); - }} + onClick={() => handlePopUpToggle("revokeAuthMethod", true)} > Remove Auth Method