diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 166c5a3f99..7a5682b30d 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"; @@ -128,6 +129,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/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/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 18930cfe88..8d872afa5b 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,13 @@ 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", + 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", + 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 +454,66 @@ interface DeleteIdentityUniversalAuthEvent { }; } +interface CreateTokenIdentityTokenAuthEvent { + type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH; + metadata: { + identityId: string; + identityAccessTokenId: string; + }; +} + +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: { + 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: { @@ -1051,6 +1118,13 @@ export type Event = | UpdateIdentityUniversalAuthEvent | DeleteIdentityUniversalAuthEvent | GetIdentityUniversalAuthEvent + | CreateTokenIdentityTokenAuthEvent + | UpdateTokenIdentityTokenAuthEvent + | GetTokensIdentityTokenAuthEvent + | AddIdentityTokenAuthEvent + | UpdateIdentityTokenAuthEvent + | GetIdentityTokenAuthEvent + | DeleteIdentityTokenAuthEvent | LoginIdentityKubernetesAuthEvent | DeleteIdentityKubernetesAuthEvent | AddIdentityKubernetesAuthEvent diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index bc8b153e03..069d615693 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"; @@ -234,6 +236,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); @@ -808,6 +811,7 @@ export const registerRoutes = async ( permissionService, identityDAL, identityOrgMembershipDAL, + identityProjectDAL, licenseService }); const identityAccessTokenService = identityAccessTokenServiceFactory({ @@ -828,6 +832,14 @@ export const registerRoutes = async ( permissionService, identityProjectDAL }); + const identityTokenAuthService = identityTokenAuthServiceFactory({ + identityTokenAuthDAL, + identityDAL, + identityOrgMembershipDAL, + identityAccessTokenDAL, + permissionService, + licenseService + }); const identityUaService = identityUaServiceFactory({ identityOrgMembershipDAL, permissionService, @@ -970,6 +982,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-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 new file mode 100644 index 0000000000..ac36c3c587 --- /dev/null +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -0,0 +1,468 @@ +import { z } from "zod"; + +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"; +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 }; + } + }); + + // 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", + 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() + }), + body: z.object({ + name: z.string().optional() + }), + 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.createTokenAuthToken({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId, + ...req.body + }); + + 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 + }; + } + }); + + 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.getTokenAuthTokens({ + 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/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({ + 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.updateTokenAuthToken({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + 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: token.identityId, + tokenId: token.id, + name: req.body.name + } + } + }); + + 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/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index d5c5acbcf8..0e9e48abd7 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"; @@ -34,6 +35,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-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 3e7fe31a6f..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 @@ -131,7 +131,10 @@ 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 }; }; @@ -141,6 +144,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({ 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-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..198b76266f --- /dev/null +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -0,0 +1,470 @@ +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; + +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, NotFoundError, UnauthorizedError } 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, + TCreateTokenAuthTokenDTO, + TGetTokenAuthDTO, + TGetTokenAuthTokensDTO, + TRevokeTokenAuthDTO, + TRevokeTokenAuthTokenDTO, + TUpdateTokenAuthDTO, + TUpdateTokenAuthTokenDTO +} from "./identity-token-auth-types"; + +type TIdentityTokenAuthServiceFactoryDep = { + identityTokenAuthDAL: Pick< + TIdentityTokenAuthDALFactory, + "transaction" | "create" | "findOne" | "updateById" | "delete" + >; + identityDAL: Pick; + identityOrgMembershipDAL: Pick; + identityAccessTokenDAL: Pick< + TIdentityAccessTokenDALFactory, + "create" | "find" | "update" | "findById" | "findOne" | "updateById" + >; + 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 UnauthorizedError({ + 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 createTokenAuthToken = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId, + name + }: TCreateTokenAuthTokenDTO) => { + 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 create token for 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, + name + }, + 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 }; + }; + + const getTokenAuthTokens = async ({ + identityId, + offset = 0, + limit = 20, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TGetTokenAuthTokensDTO) => { + 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.Read, OrgPermissionSubjects.Identity); + + const tokens = await identityAccessTokenDAL.find( + { + identityId + }, + { offset, limit, sort: [["updatedAt", "desc"]] } + ); + + return { tokens, identityMembershipOrg }; + }; + + const updateTokenAuthToken = async ({ + tokenId, + name, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: 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({ + 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: foundToken.identityId, + id: tokenId + }, + { + name + } + ); + + 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, + 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 new file mode 100644 index 0000000000..12c689728b --- /dev/null +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -0,0 +1,45 @@ +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 TCreateTokenAuthTokenDTO = { + identityId: string; + name?: string; +} & Omit; + +export type TGetTokenAuthTokensDTO = { + identityId: string; + offset: number; + limit: number; +} & Omit; + +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 62c812a3ed..78dcdda4e7 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -5,17 +5,25 @@ 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"; 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 NotFoundError({ 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/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..ceece2c13d 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -5,23 +5,37 @@ export { useAddIdentityAzureAuth, useAddIdentityGcpAuth, useAddIdentityKubernetesAuth, + useAddIdentityTokenAuth, useAddIdentityUniversalAuth, useCreateIdentity, useCreateIdentityUniversalAuthClientSecret, + useCreateTokenIdentityTokenAuth, useDeleteIdentity, + useDeleteIdentityAwsAuth, + useDeleteIdentityAzureAuth, + useDeleteIdentityGcpAuth, + useDeleteIdentityKubernetesAuth, + useDeleteIdentityTokenAuth, + useDeleteIdentityUniversalAuth, + useRevokeIdentityTokenAuthToken, useRevokeIdentityUniversalAuthClientSecret, useUpdateIdentity, useUpdateIdentityAwsAuth, useUpdateIdentityAzureAuth, useUpdateIdentityGcpAuth, useUpdateIdentityKubernetesAuth, - useUpdateIdentityUniversalAuth -} from "./mutations"; + useUpdateIdentityTokenAuth, + useUpdateIdentityTokenAuthToken, + useUpdateIdentityUniversalAuth} 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 cb1fe4c170..86fca63e99 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -9,25 +9,40 @@ import { AddIdentityAzureAuthDTO, AddIdentityGcpAuthDTO, AddIdentityKubernetesAuthDTO, + AddIdentityTokenAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, CreateIdentityDTO, CreateIdentityUniversalAuthClientSecretDTO, 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, - UpdateIdentityUniversalAuthDTO + UpdateIdentityTokenAuthDTO, + UpdateIdentityUniversalAuthDTO, + UpdateTokenIdentityTokenAuthDTO } from "./types"; export const useCreateIdentity = () => { @@ -58,8 +73,9 @@ export const useUpdateIdentity = () => { return identity; }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { organizationId, identityId }) => { queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); } }); }; @@ -103,8 +119,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)); } }); }; @@ -131,8 +149,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)); } }); }; @@ -214,8 +251,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)); } }); }; @@ -252,8 +291,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)); } }); }; @@ -288,8 +346,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)); } }); }; @@ -324,8 +384,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)); } }); }; @@ -360,8 +439,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)); } }); }; @@ -402,8 +483,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)); } }); }; @@ -438,8 +521,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)); } }); }; @@ -480,8 +582,164 @@ 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)); + } + }); +}; + +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: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId)); + } + }); +}; + +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: (_, { 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)); + } + }); +}; + +export const useCreateTokenIdentityTokenAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId, name }) => { + const { data } = await apiRequest.post( + `/api/v1/auth/token-auth/identities/${identityId}/tokens`, + { + name + } + ); + + return data; + }, + onSuccess: (_, { identityId }) => { + queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId)); + } + }); +}; + +export const useUpdateIdentityTokenAuthToken = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ tokenId, name }) => { + const { + data: { token } + } = await apiRequest.patch<{ token: IdentityAccessToken }>( + `/api/v1/auth/token-auth/tokens/${tokenId}`, + { + name + } + ); + + return token; + }, + onSuccess: (_, { identityId }) => { + queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId)); + } + }); +}; + +export const useRevokeIdentityTokenAuthToken = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ tokenId }) => { + const { data } = await apiRequest.post( + `/api/v1/auth/token-auth/tokens/${tokenId}/revoke` + ); + + 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 eb04227eb6..5a72ecb781 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -4,13 +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) => @@ -19,7 +23,40 @@ 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, + 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) => { @@ -33,7 +70,9 @@ export const useGetIdentityUniversalAuth = (identityId: string) => { `/api/v1/auth/universal-auth/identities/${identityId}` ); return identityUniversalAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -63,7 +102,9 @@ export const useGetIdentityGcpAuth = (identityId: string) => { `/api/v1/auth/gcp-auth/identities/${identityId}` ); return identityGcpAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -78,7 +119,9 @@ export const useGetIdentityAwsAuth = (identityId: string) => { `/api/v1/auth/aws-auth/identities/${identityId}` ); return identityAwsAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -93,7 +136,9 @@ export const useGetIdentityAzureAuth = (identityId: string) => { `/api/v1/auth/azure-auth/identities/${identityId}` ); return identityAzureAuth; - } + }, + staleTime: 0, + cacheTime: 0 }); }; @@ -108,6 +153,40 @@ export const useGetIdentityKubernetesAuth = (identityId: string) => { `/api/v1/auth/kubernetes-auth/identities/${identityId}` ); return identityKubernetesAuth; + }, + staleTime: 0, + cacheTime: 0 + }); +}; + +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; + }, + 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 80d066c720..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; @@ -311,3 +352,65 @@ 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 DeleteIdentityTokenAuthDTO = { + organizationId: string; + identityId: string; +}; + +export type CreateTokenIdentityTokenAuthDTO = { + identityId: string; + name: string; +}; + +export type CreateTokenIdentityTokenAuthRes = { + accessToken: string; + tokenType: string; + 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..1be789baa8 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/IdentityPage.tsx @@ -0,0 +1,333 @@ +/* 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, + useRevokeIdentityTokenAuthToken, + useRevokeIdentityUniversalAuthClientSecret} 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 } = useRevokeIdentityTokenAuthToken(); + const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "identity", + "deleteIdentity", + "identityAuthMethod", + "revokeAuthMethod", + "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..be7c744ced --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx @@ -0,0 +1,86 @@ +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..21402fb6da --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx @@ -0,0 +1,140 @@ +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, + 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 [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset({ + initialState: "Copy Client ID to clipboard" + }); + + const { data } = useGetIdentityById(identityId); + const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(identityId); + const { data: clientSecrets } = useGetIdentityUniversalAuthClientSecrets(identityId); + return ( +
+
+

Client ID

+
+

{identityUniversalAuth?.clientId ?? ""}

+ + { + navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? ""); + setCopyTextClientId("Copied"); + }} + > + + + +
+
+ {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..64a4c96b20 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityTokens.tsx @@ -0,0 +1,121 @@ +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 + + {!token.isAccessTokenRevoked && ( + { + 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..c6d7f0ce14 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx @@ -0,0 +1,87 @@ +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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +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 [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + 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}

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

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..dfc26592e4 --- /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, useUpdateIdentityTokenAuthToken } 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 } = useUpdateIdentityTokenAuthToken(); + 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/IdentityAuthMethodModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx index a1dc5d678a..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,33 +3,44 @@ 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"; import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm"; import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; +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; }; 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 }, @@ -46,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: { @@ -117,12 +138,93 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog /> ); } + case IdentityAuthMethod.TOKEN_AUTH: { + return ( + + ); + } 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 d347d422d8..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 @@ -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: { @@ -329,23 +329,36 @@ 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..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 @@ -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: { @@ -327,23 +327,36 @@ 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..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 @@ -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: { @@ -361,23 +361,36 @@ 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 142b25dc30..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 @@ -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(), @@ -45,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: { @@ -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: "", @@ -384,23 +382,36 @@ 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/IdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index b8b1f1708c..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,10 +15,11 @@ 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 { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal"; +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 @@ -106,22 +108,19 @@ export const IdentitySection = withPermission( )}
- - + + {/* - - */} + {/* + /> */} + , - data?: { - identityId?: string; - name?: string; - authMethod?: string; - role?: string; - customRole?: { - name: string; - slug: string; - }; - } - ) => void; -}; -export const IdentityTable = ({ handlePopUpOpen }: Props) => { +export const IdentityTable = () => { + const router = useRouter(); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -94,19 +63,22 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { Name Role - Auth Method {isLoading && } {!isLoading && - data && - data.length > 0 && - data.map(({ identity: { id, name, authMethod }, role, customRole }) => { + data?.map(({ identity: { id, name }, role, customRole }) => { return ( - - {name} + router.push(`/org/${orgId}/identities/${id}`)} + > + + {name} + { }} - {authMethod ? identityAuthToNameMap[authMethod] : "Not configured"}
- {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 new file mode 100644 index 0000000000..b28af537ff --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm.tsx @@ -0,0 +1,275 @@ +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faXmark } 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 } from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityTokenAuth, + useGetIdentityTokenAuth, + useUpdateIdentityTokenAuth +} from "@app/hooks/api"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + accessTokenTTL: z.string(), + accessTokenMaxTTL: z.string(), + accessTokenNumUsesLimit: z.string(), + accessTokenTrustedIps: z + .array( + z.object({ + ipAddress: z.string().max(50) + }) + ) + .min(1) + }) + .required(); + +export type FormData = z.infer; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, + 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" + > + + +
+ ))} +
+ +
+
+
+ + +
+ {identityAuthMethodData?.authMethod && ( + + )} +
+ + ); +}; 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"); + }} + > + + + +
+ )} +
+
+ ); +}; 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..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 @@ -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: { @@ -368,23 +368,36 @@ export const IdentityUniversalAuthForm = ({ Add IP Address
-
- - +
+
+ + +
+ {identityAuthMethodData?.authMethod && ( + + )}
);