From eb2f433f43f27c2de7084e3a506267e13705cffe Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 30 Sep 2023 20:52:04 +0100 Subject: [PATCH] Add trusted IP rules to ST V3 --- .../src/controllers/v3/secretsController.ts | 13 + .../v3/serviceTokenDataController.ts | 42 +- backend/src/ee/models/auditLog/types.ts | 12 +- backend/src/helpers/secrets.ts | 5 +- backend/src/models/serviceTokenDataV3.ts | 40 +- backend/src/utils/ip/ip.ts | 39 +- backend/src/validation/serviceTokenDataV3.ts | 25 +- .../src/hooks/api/serviceTokens/queries.tsx | 2 + frontend/src/hooks/api/serviceTokens/types.ts | 14 + .../AddServiceTokenV3Modal.tsx | 392 ++++++++++-------- .../ServiceTokenV3Table.tsx | 25 +- 11 files changed, 404 insertions(+), 205 deletions(-) diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index ebe73292e..dd01174d1 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -106,6 +106,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -209,6 +210,7 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -281,6 +283,7 @@ export const createSecretRaw = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -380,6 +383,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -460,6 +464,7 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -555,6 +560,7 @@ export const getSecrets = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -640,6 +646,7 @@ export const getSecretByName = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -713,6 +720,7 @@ export const createSecret = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -812,6 +820,7 @@ export const updateSecretByName = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -888,6 +897,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -946,6 +956,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -995,6 +1006,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, @@ -1044,6 +1056,7 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => { } case ActorType.SERVICE_V3: { await validateServiceTokenDataV3ClientForWorkspace({ + authData: req.authData, serviceTokenData: req.authData.authPayload as IServiceTokenDataV3, workspaceId: new Types.ObjectId(workspaceId), environment, diff --git a/backend/src/controllers/v3/serviceTokenDataController.ts b/backend/src/controllers/v3/serviceTokenDataController.ts index 16505be06..749489062 100644 --- a/backend/src/controllers/v3/serviceTokenDataController.ts +++ b/backend/src/controllers/v3/serviceTokenDataController.ts @@ -7,7 +7,8 @@ import { ServiceTokenDataV3Key } from "../../models"; import { - Scope + IServiceTokenV3Scope, + IServiceTokenV3TrustedIp } from "../../models/serviceTokenDataV3"; import { ActorType, @@ -23,6 +24,7 @@ import { } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; +import { extractIPDetails, isValidIpOrCidr } from "../../utils/ip"; import { EEAuditLogService } from "../../ee/services"; import { getJwtServiceTokenSecret } from "../../config"; @@ -66,6 +68,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { workspaceId, publicKey, scopes, + trustedIps, expiresIn, encryptedKey, // for ServiceTokenDataV3Key nonce // for ServiceTokenDataV3Key @@ -77,6 +80,17 @@ export const createServiceTokenData = async (req: Request, res: Response) => { ProjectPermissionSub.ServiceTokens ); + // validate trusted ips + const reformattedTrustedIps = trustedIps.map((trustedIp) => { + const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(trustedIp.ipAddress); + }); + let expiresAt; if (expiresIn) { expiresAt = new Date(); @@ -95,6 +109,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { workspace: new Types.ObjectId(workspaceId), publicKey, usageCount: 0, + trustedIps: reformattedTrustedIps, scopes, isActive, expiresAt @@ -123,7 +138,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => { metadata: { name, isActive, - scopes: scopes as Array, + scopes: scopes as Array, + trustedIps: reformattedTrustedIps as Array, expiresAt } }, @@ -151,6 +167,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { name, isActive, scopes, + trustedIps, expiresIn } } = await validateRequest(reqValidator.UpdateServiceTokenV3, req); @@ -171,6 +188,20 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { ProjectPermissionSub.ServiceTokens ); + // validate trusted ips + let reformattedTrustedIps; + if (trustedIps) { + reformattedTrustedIps = trustedIps.map((trustedIp) => { + const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(trustedIp.ipAddress); + }); + } + let expiresAt; if (expiresIn) { expiresAt = new Date(); @@ -183,6 +214,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { name, isActive, scopes, + trustedIps: reformattedTrustedIps, expiresAt }, { @@ -201,7 +233,8 @@ export const updateServiceTokenData = async (req: Request, res: Response) => { metadata: { name: serviceTokenData.name, isActive, - scopes: scopes as Array, + scopes: scopes as Array, + trustedIps: reformattedTrustedIps as Array, expiresAt } }, @@ -254,7 +287,8 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { metadata: { name: serviceTokenData.name, isActive: serviceTokenData.isActive, - scopes: serviceTokenData.scopes as Array, + scopes: serviceTokenData.scopes as Array, + trustedIps: serviceTokenData.trustedIps as Array, expiresAt: serviceTokenData.expiresAt } }, diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 5975605a4..2f9fdb41c 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -3,7 +3,8 @@ import { EventType } from "./enums"; import { - Scope + IServiceTokenV3Scope, + IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3"; interface UserActorMetadata { @@ -229,7 +230,8 @@ interface CreateServiceTokenV3Event { metadata: { name: string; isActive: boolean; - scopes: Array; + scopes: Array; + trustedIps: Array; expiresAt?: Date; } } @@ -239,7 +241,8 @@ interface UpdateServiceTokenV3Event { metadata: { name?: string; isActive?: boolean; - scopes?: Array; + scopes?: Array; + trustedIps?: Array; expiresAt?: Date; } } @@ -249,8 +252,9 @@ interface DeleteServiceTokenV3Event { metadata: { name: string; isActive: boolean; - scopes: Array; + scopes: Array; expiresAt?: Date; + trustedIps: Array; } } diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index ac4563f25..6f718f5fe 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -19,10 +19,7 @@ import { ServiceTokenData, TFolderRootSchema } from "../models"; -import { - Scope, - Permission -} from "../models/serviceTokenDataV3"; +import { Permission } from "../models/serviceTokenDataV3"; import { EventType, SecretVersion } from "../ee/models"; import { BadRequestError, diff --git a/backend/src/models/serviceTokenDataV3.ts b/backend/src/models/serviceTokenDataV3.ts index 1a7277022..a9758422e 100644 --- a/backend/src/models/serviceTokenDataV3.ts +++ b/backend/src/models/serviceTokenDataV3.ts @@ -1,16 +1,23 @@ import { Document, Schema, Types, model } from "mongoose"; +import { IPType } from "../ee/models"; export enum Permission { READ = "read", WRITE = "write" } -export interface Scope { +export interface IServiceTokenV3Scope { environment: string; secretPath: string; permissions: Permission[]; } +export interface IServiceTokenV3TrustedIp { + ipAddress: string; + type: IPType; + prefix: number; +} + export interface IServiceTokenDataV3 extends Document { _id: Types.ObjectId; name: string; @@ -21,7 +28,8 @@ export interface IServiceTokenDataV3 extends Document { lastUsed?: Date; usageCount: number; expiresAt?: Date; - scopes: Array; + scopes: Array; + trustedIps: Array; } const serviceTokenDataV3Schema = new Schema( @@ -83,6 +91,34 @@ const serviceTokenDataV3Schema = new Schema( } ], required: true + }, + trustedIps: { + type: [ + { + ipAddress: { + type: String, + required: true + }, + type: { + type: String, + enum: [ + IPType.IPV4, + IPType.IPV6 + ], + required: true + }, + prefix: { + type: Number, + required: false + } + } + ], + default: [{ + ipAddress: "0.0.0.0", + type: IPType.IPV4.toString(), + prefix: 0 + }], + required: true } }, { diff --git a/backend/src/utils/ip/ip.ts b/backend/src/utils/ip/ip.ts index ac3b17149..bc314fa9a 100644 --- a/backend/src/utils/ip/ip.ts +++ b/backend/src/utils/ip/ip.ts @@ -1,6 +1,6 @@ import net from "net"; import { IPType } from "../../ee/models"; -import { InternalServerError } from "../errors"; +import { InternalServerError, UnauthorizedRequestError } from "../errors"; /** * Return details of IP [ip]: @@ -98,4 +98,39 @@ export const isValidIpOrCidr = (ip: string): boolean => { } return false; -} \ No newline at end of file +} + +/** + * Validates the IP address [ipAddress] against the trusted IPs [trustedIps]. + * @param {Object} obj + * @param {String} obj.ipAddress - IP address to check + * @param {Object[]} obj.trustedIps - IPs to trust in blocklist + */ +export const checkIPAgainstBlocklist = ({ + ipAddress, + trustedIps +}: { + ipAddress: string; + trustedIps: { + ipAddress: string; + type: IPType; + prefix: number; + }[] +}) => { + const blockList = new net.BlockList(); + + for (const trustedIp of trustedIps) { + if (trustedIp.prefix !== undefined) { + blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type); + } else { + blockList.addAddress(trustedIp.ipAddress, trustedIp.type); + } + } + + const { type } = extractIPDetails(ipAddress); + const check = blockList.check(ipAddress, type); + + if (!check) throw UnauthorizedRequestError({ + message: "Failed to authenticate" + }); +} diff --git a/backend/src/validation/serviceTokenDataV3.ts b/backend/src/validation/serviceTokenDataV3.ts index 9760d1af1..976f1a1e5 100644 --- a/backend/src/validation/serviceTokenDataV3.ts +++ b/backend/src/validation/serviceTokenDataV3.ts @@ -4,6 +4,8 @@ import { Permission } from "../models/serviceTokenDataV3"; import { z } from "zod"; import { UnauthorizedRequestError } from "../utils/errors"; import { isValidScopeV3 } from "../helpers"; +import { AuthData } from "../interfaces/middleware"; +import { checkIPAgainstBlocklist } from "../utils/ip"; /** * Validate that service token (client) can access workspace @@ -16,19 +18,27 @@ import { isValidScopeV3 } from "../helpers"; * @param {String[]} acceptedPermissions - accepted permissions as part of the endpoint */ export const validateServiceTokenDataV3ClientForWorkspace = async ({ + authData, serviceTokenData, workspaceId, environment, secretPath = "/", requiredPermissions }: { + authData: AuthData; serviceTokenData: IServiceTokenDataV3; workspaceId: Types.ObjectId; environment?: string; secretPath?: string; requiredPermissions: Permission[]; }) => { - + + // validate ST V3 IP address + checkIPAgainstBlocklist({ + ipAddress: authData.ipAddress, + trustedIps: serviceTokenData.trustedIps + }); + if (!serviceTokenData.workspace.equals(workspaceId)) { // case: invalid workspaceId passed throw UnauthorizedRequestError({ @@ -63,6 +73,12 @@ export const CreateServiceTokenV3 = z.object({ }) .array() .min(1), + trustedIps: z + .object({ + ipAddress: z.string().trim(), + }) + .array() + .min(1), expiresIn: z.number().optional(), encryptedKey: z.string().trim(), nonce: z.string().trim() @@ -85,6 +101,13 @@ export const UpdateServiceTokenV3 = z.object({ .array() .min(1) .optional(), + trustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional(), expiresIn: z.number().optional() }), }); diff --git a/frontend/src/hooks/api/serviceTokens/queries.tsx b/frontend/src/hooks/api/serviceTokens/queries.tsx index d05522f7b..900bb1d34 100644 --- a/frontend/src/hooks/api/serviceTokens/queries.tsx +++ b/frontend/src/hooks/api/serviceTokens/queries.tsx @@ -87,12 +87,14 @@ export const useUpdateServiceTokenV3 = () => { name, isActive, scopes, + trustedIps, expiresIn }) => { const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, { name, isActive, scopes, + trustedIps, expiresIn }); diff --git a/frontend/src/hooks/api/serviceTokens/types.ts b/frontend/src/hooks/api/serviceTokens/types.ts index 6ef513e4c..d47c3bc4c 100644 --- a/frontend/src/hooks/api/serviceTokens/types.ts +++ b/frontend/src/hooks/api/serviceTokens/types.ts @@ -44,6 +44,13 @@ export type ServiceTokenV3Scope = { secretPath: string; }; +export type ServiceTokenV3TrustedIp = { + _id: string; + ipAddress: string; + type: "ipv4" | "ipv6"; + prefix?: number; +} + export type ServiceTokenDataV3 = { _id: string; name: string; @@ -52,6 +59,7 @@ export type ServiceTokenDataV3 = { lastUsed?: string; usageCount: number; scopes: ServiceTokenV3Scope[]; + trustedIps: ServiceTokenV3TrustedIp[]; expiresAt?: string; createdAt: string; updatedAt: string; @@ -62,6 +70,9 @@ export type CreateServiceTokenDataV3DTO = { workspaceId: string; publicKey: string; scopes: ServiceTokenV3Scope[]; + trustedIps: { + ipAddress: string; + }[]; expiresIn?: number; encryptedKey: string; nonce: string; @@ -77,6 +88,9 @@ export type UpdateServiceTokenDataV3DTO = { isActive?: boolean; name?: string; scopes?: ServiceTokenV3Scope[]; + trustedIps?: { + ipAddress: string; + }[]; expiresIn?: number; } diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx index ac47420a2..0b46a3eb5 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenV3Section/AddServiceTokenV3Modal.tsx @@ -20,11 +20,7 @@ import { Modal, ModalContent, Select, - SelectItem, - // Accordion, - // AccordionItem, - // AccordionTrigger, - // AccordionContent + SelectItem } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { @@ -35,7 +31,10 @@ import { import { Permission } from "@app/hooks/api/serviceTokens/enums"; -import { ServiceTokenV3Scope } from "@app/hooks/api/serviceTokens/types"; +import { + ServiceTokenV3Scope, + ServiceTokenV3TrustedIp +} from "@app/hooks/api/serviceTokens/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; const expirations = [ @@ -58,23 +57,32 @@ const schema = yup.object({ name: yup.string().required("ST V3 name is required"), expiresIn: yup.string(), scopes: yup + .array( + yup.object({ + permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission"), + environment: yup.string().max(50).required().label("Environment"), + secretPath: yup + .string() + .required() + .default("/") + .label("Secret Path") + .transform((val) => + typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val + ) + }) + ) + .min(1) + .required() + .label("Scope"), + trustedIps: yup .array( yup.object({ - permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission"), - environment: yup.string().max(50).required().label("Environment"), - secretPath: yup - .string() - .required() - .default("/") - .label("Secret Path") - .transform((val) => - typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val - ) + ipAddress: yup.string().max(50).required().label("IP Address") }) ) .min(1) .required() - .label("Scope") + .label("Trusted IP") }).required(); export type FormData = yup.InferType; @@ -107,6 +115,9 @@ export const AddServiceTokenV3Modal = ({ permission: "read", environment: currentWorkspace?.environments?.[0]?.slug, secretPath: "/", + }], + trustedIps: [{ + ipAddress: "0.0.0.0/0" }] } }); @@ -116,6 +127,7 @@ export const AddServiceTokenV3Modal = ({ serviceTokenDataId: string; name: string; scopes: ServiceTokenV3Scope[]; + trustedIps: ServiceTokenV3TrustedIp[]; }; if (serviceTokenData) { @@ -132,6 +144,14 @@ export const AddServiceTokenV3Modal = ({ secretPath: "/", permission }) + }), + trustedIps: serviceTokenData.trustedIps.map(({ + ipAddress, + prefix + }: ServiceTokenV3TrustedIp) => { + return ({ + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }); }) }); } else { @@ -141,17 +161,22 @@ export const AddServiceTokenV3Modal = ({ permission: "read", environment: currentWorkspace?.environments?.[0]?.slug, secretPath: "/", + }], + trustedIps: [{ + ipAddress: "0.0.0.0/0" }] }); } }, [popUp?.serviceTokenV3?.data]); const { fields: tokenScopes, append, remove } = useFieldArray({ control, name: "scopes" }); + const { fields: tokenTrustedIps, append: appendTrustedIp, remove: removeTrustedIp } = useFieldArray({ control, name: "trustedIps" }); const onFormSubmit = async ({ name, expiresIn, - scopes + scopes, + trustedIps }: FormData) => { try { const serviceTokenData = popUp?.serviceTokenV3?.data as { @@ -167,14 +192,16 @@ export const AddServiceTokenV3Modal = ({ secretPath: scope.secretPath, permissions: permissionsMap[scope.permission] }); - }) + }); if (serviceTokenData) { // update + await updateMutateAsync({ serviceTokenDataId: serviceTokenData.serviceTokenDataId, name, scopes: reformattedScopes, + trustedIps, expiresIn: expiresIn === "" ? undefined : Number(expiresIn) }); } else { @@ -206,6 +233,7 @@ export const AddServiceTokenV3Modal = ({ workspaceId: currentWorkspace._id, publicKey, scopes: reformattedScopes, + trustedIps, expiresIn: expiresIn === "" ? undefined : Number(expiresIn), encryptedKey: ciphertext, nonce @@ -270,175 +298,171 @@ export const AddServiceTokenV3Modal = ({ )} /> - {tokenScopes.map(({ id }, index) => ( -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - remove(index)} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" + {tokenScopes.map(({ id }, index) => ( +
+ ( + - - -
- ))} -
-
+ ))} +
+ +
+ {tokenTrustedIps.map(({ id }, index) => ( +
+ ( + + + + )} + /> + removeTrustedIp(index)} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" > - Add Scope - + +
- ( - + +
+ ( + + onChange(e)} - className="w-full" - > - {expirations.map(({ label, value }) => ( - - {label} - - ))} - - - )} - /> - {/* - - Scopes - Description of Section 1 - - */} - {/*

Temporariness

- setIsTemporary(value)} - isChecked={isTemporary} - > -
-

This token will be deactivated after your specified duration.

-
-
- {isTemporary && ( - ( - - - - )} - /> - )} */} + {expirations.map(({ label, value }) => ( + + {label} + + ))} + + + )} + />