From 6483174804f2ba79137b364ef7888fec8e3bad15 Mon Sep 17 00:00:00 2001 From: michaelwang13 Date: Thu, 12 Dec 2024 15:47:58 -0500 Subject: [PATCH] token_header_implementation (#993) * initial pass - token header * tokenHeader feature flagging * fixes * linting changes * verbiage changes * formatting * add tokenheader to create endpoint * add tokenHeader default * formatting change * fix logic, fix base states * fix state so it stays undefined/nil * update default endpoint / detail settings page --- .github/workflows/deploy.yml | 1 + Dockerfile | 1 + src/deploy/endpoint/index.ts | 25 +++++++++++++++++ src/organizations/index.ts | 17 ++++++++++++ src/schema/factory.ts | 4 +++ src/types/deploy.ts | 1 + src/types/state.ts | 1 + src/ui/layouts/endpoint-detail-layout.tsx | 7 +++++ src/ui/pages/app-create-endpoint.tsx | 32 ++++++++++++++++++++++ src/ui/pages/app-deploy-status.tsx | 1 + src/ui/pages/endpoint-detail-settings.tsx | 33 +++++++++++++++++++++-- src/vite.d.ts | 1 + 12 files changed, 122 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a2366c803..7814bbd0d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,6 +37,7 @@ jobs: VITE_MINTLIFY_CHAT_KEY=${{ secrets.MINTLIFY_CHAT_KEY }} VITE_STRIPE_PUBLISHABLE_KEY=${{ secrets.STRIPE_PUBLISHABLE_KEY }} VITE_FEATURE_BETA_ORG_IDS=${{ vars.FEATURE_BETA_ORG_IDS }} + VITE_TOKEN_HEADER_ORG_IDS=${{ vars.TOKEN_HEADER_ORG_IDS }} deploy: runs-on: ubuntu-latest needs: build diff --git a/Dockerfile b/Dockerfile index 5b916d7ee..a97c7452e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ ARG VITE_SENTRY_DSN ARG VITE_MINTLIFY_CHAT_KEY ARG VITE_STRIPE_PUBLISHABLE_KEY ARG VITE_FEATURE_BETA_ORG_IDS=df0ee681-9e02-4c28-8916-3b215d539b08 +ARG VITE_TOKEN_HEADER_ORG_IDS=df0ee681-9e02-4c28-8916-3b215d539b08 RUN corepack enable RUN corepack prepare yarn@stable --activate diff --git a/src/deploy/endpoint/index.ts b/src/deploy/endpoint/index.ts index c35f64d6f..c9e131e37 100644 --- a/src/deploy/endpoint/index.ts +++ b/src/deploy/endpoint/index.ts @@ -58,6 +58,7 @@ export interface DeployEndpointResponse { status: ProvisionableStatus; created_at: string; updated_at: string; + token_header: string | undefined; _links: { service: LinkResponse; certificate: LinkResponse; @@ -92,6 +93,7 @@ export const defaultEndpointResponse = ( ip_whitelist: [], platform: "elb", type: "unknown", + token_header: undefined, user_domain: "", virtual_domain: "", security_group_id: "", @@ -135,6 +137,7 @@ export const deserializeDeployEndpoint = ( status: payload.status, serviceId: extractIdFromLink(payload._links.service), certificateId: extractIdFromLink(payload._links.certificate), + tokenHeader: payload.token_header, }; }; @@ -457,6 +460,7 @@ interface CreateEndpointBase { internal: boolean; ipAllowlist: string[]; containerPort?: string; + tokenHeader: string | undefined; } interface CreateDefaultEndpoint extends CreateEndpointBase { @@ -495,6 +499,7 @@ export const createEndpoint = api.post< internal: ctx.payload.internal, ip_whitelist: ctx.payload.ipAllowlist, container_port: ctx.payload.containerPort, + token_header: ctx.payload.tokenHeader, }; if (ctx.payload.certId) { @@ -731,6 +736,7 @@ interface EndpointPatchProps { ipAllowlist: string[]; containerPort: string; certId: string; + tokenHeader: string | undefined; } export interface EndpointUpdateProps extends EndpointPatchProps { @@ -747,6 +753,7 @@ const patchEndpoint = api.patch( const data: Record = { ip_whitelist: ctx.payload.ipAllowlist, container_port: ctx.payload.containerPort, + token_header: ctx.payload.tokenHeader, }; if (ctx.payload.certId) { data.certificate = `${env.apiUrl}/certificates/${ctx.payload.certId}`; @@ -786,6 +793,7 @@ export const updateEndpoint = thunks.create( ipAllowlist: ctx.payload.ipAllowlist, containerPort: ctx.payload.containerPort, certId, + tokenHeader: ctx.payload.tokenHeader, }); if (!patchCtx.json.ok) { yield* schema.update( @@ -896,6 +904,14 @@ export const isRequiresCert = (enp: DeployEndpoint) => { return (isHttp || isTls) && !enp.default && !enp.acme; }; +export const canHaveTokenHeader = (enp: DeployEndpoint) => { + return ( + enp.type === "http" || + enp.type === "http_proxy_protocol" || + enp.type === "grpc" + ); +}; + export const getContainerPort = ( enp: Pick, exposedPorts: number[], @@ -941,12 +957,21 @@ export const getEndpointUrl = (enp?: DeployEndpoint) => { return enp.virtualDomain || enp.externalHost || emptyEndpointName; }; +export const getTokenHeader = (enp?: DeployEndpoint) => { + if (!enp) return; + if (enp.tokenHeader) { + return "True"; + } + return "False"; +}; + export const getEndpointText = (enp: DeployEndpoint) => { return { url: getEndpointUrl(enp), placement: getPlacement(enp), ipAllowlist: getIpAllowlistText(enp), hostname: getEndpointDisplayHost(enp), + token_header: getTokenHeader(enp), }; }; diff --git a/src/organizations/index.ts b/src/organizations/index.ts index 3a0351ba5..ae2d3e533 100644 --- a/src/organizations/index.ts +++ b/src/organizations/index.ts @@ -123,6 +123,23 @@ export const selectHasBetaFeatures = createSelector( }, ); +export const selectHasTokenHeaderFeature = createSelector( + selectOrganizationSelectedId, + selectEnv, + (orgId, config) => { + if (config.isDev) { + return true; + } + + // Array of organization IDs that have access to Token Header integration feature + const tokenHeaderFeatureOrgIds = config.tokenHeaderOrgIds + .split(",") + .map((id) => id.trim()) + .filter(Boolean); + + return tokenHeaderFeatureOrgIds.includes(orgId); + }, +); function deserializeOrganization(o: OrganizationResponse): Organization { return { id: o.id, diff --git a/src/schema/factory.ts b/src/schema/factory.ts index 6a510e849..3cdf1c754 100644 --- a/src/schema/factory.ts +++ b/src/schema/factory.ts @@ -65,6 +65,9 @@ export const defaultConfig = (e: Partial = {}): Config => { betaFeatureOrgIds: import.meta.env.VITE_FEATURE_BETA_ORG_IDS || "df0ee681-9e02-4c28-8916-3b215d539b08", + tokenHeaderOrgIds: + import.meta.env.VITE_TOKEN_HEADER_ORG_IDS || + "df0ee681-9e02-4c28-8916-3b215d539b08", legacyDashboardUrl: import.meta.env.VITE_LEGACY_DASHBOARD_URL || "https://dashboard.aptible.com", @@ -165,6 +168,7 @@ export const defaultDeployEndpoint = ( ipWhitelist: [], platform: "elb", type: "unknown", + tokenHeader: undefined, createdAt: now, updatedAt: now, userDomain: "", diff --git a/src/types/deploy.ts b/src/types/deploy.ts index a12a5e7ea..5aa0aa4b3 100644 --- a/src/types/deploy.ts +++ b/src/types/deploy.ts @@ -109,6 +109,7 @@ export interface DeployEndpoint extends Provisionable, Timestamps { virtualDomain: string; serviceId: string; certificateId: string; + tokenHeader: string | undefined; } export type OnboardingStatus = diff --git a/src/types/state.ts b/src/types/state.ts index dea31b079..18008cb10 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -16,6 +16,7 @@ export interface Config { stripePublishableKey: string; origin: "app"; betaFeatureOrgIds: string; + tokenHeaderOrgIds: string; } export interface Feedback { diff --git a/src/ui/layouts/endpoint-detail-layout.tsx b/src/ui/layouts/endpoint-detail-layout.tsx index 0c5a9a0cf..ca2e5af7f 100644 --- a/src/ui/layouts/endpoint-detail-layout.tsx +++ b/src/ui/layouts/endpoint-detail-layout.tsx @@ -15,6 +15,7 @@ import { selectImageById, selectServiceById, } from "@app/deploy"; +import { selectHasTokenHeaderFeature } from "@app/organizations"; import { useLoader, useQuery } from "@app/react"; import { useDispatch, useSelector } from "@app/react"; import { @@ -71,6 +72,7 @@ export function EndpointAppHeaderInfo({ selectImageById(s, { id: app.currentImageId }), ); const portTxt = getContainerPort(enp, image.exposedPorts); + const hasTokenHeaderFeature = useSelector(selectHasTokenHeaderFeature); return ( @@ -116,6 +118,11 @@ export function EndpointAppHeaderInfo({ + {hasTokenHeaderFeature ? ( + + {txt.token_header} + + ) : null} ); diff --git a/src/ui/pages/app-create-endpoint.tsx b/src/ui/pages/app-create-endpoint.tsx index 15edaccfc..7b5787daa 100644 --- a/src/ui/pages/app-create-endpoint.tsx +++ b/src/ui/pages/app-create-endpoint.tsx @@ -13,6 +13,7 @@ import { selectImageById, selectStackById, } from "@app/deploy"; +import { selectHasTokenHeaderFeature } from "@app/organizations"; import { useDispatch, useLoader, @@ -67,9 +68,15 @@ const validators = { return "A private key is required for custom certificate"; } }, + tokenHeader: (data: CreateEndpointProps) => { + if (data.trafficType === "tcp" || data.trafficType === "tls") { + return "A HTTP or GRPC endpoint is required to pass token"; + } + }, }; export const AppCreateEndpointPage = () => { + const hasTokenHeaderFeature = useSelector(selectHasTokenHeaderFeature); const dispatch = useDispatch(); const navigate = useNavigate(); const { id = "" } = useParams(); @@ -97,6 +104,9 @@ export const AppCreateEndpointPage = () => { const [cert, setCert] = useState(""); const [certId, setCertId] = useState(""); const [privKey, setPrivKey] = useState(""); + const [tokenHeader, setTokenHeader] = useState( + undefined as string | undefined, + ); const portText = getContainerPort( { containerPort: port, containerPorts: [] }, image.exposedPorts, @@ -110,6 +120,7 @@ export const AppCreateEndpointPage = () => { internal: enpPlacement === "internal", ipAllowlist: parseIpStr(ipAllowlist), containerPort: port, + tokenHeader: tokenHeader, }; if (enpType === "managed") { @@ -297,6 +308,24 @@ export const AppCreateEndpointPage = () => { ); + const tokenHeaderForm = ( + + setTokenHeader(e.currentTarget.value)} + /> + + ); + const getProtocolName = (trafficType: EndpointType) => { switch (trafficType) { case "grpc": @@ -319,6 +348,7 @@ export const AppCreateEndpointPage = () => { {transCert && usingNewCert ? certForm : null} {transCert && usingNewCert ? privKeyForm : null} {ipAllowlistForm} + {hasTokenHeaderFeature ? tokenHeaderForm : null} ); } @@ -331,6 +361,7 @@ export const AppCreateEndpointPage = () => { {usingNewCert ? certForm : null} {usingNewCert ? privKeyForm : null} {ipAllowlistForm} + {hasTokenHeaderFeature ? tokenHeaderForm : null} ); } @@ -338,6 +369,7 @@ export const AppCreateEndpointPage = () => { <> {enpPlacementForm} {ipAllowlistForm} + {hasTokenHeaderFeature ? tokenHeaderForm : null} ); }; diff --git a/src/ui/pages/app-deploy-status.tsx b/src/ui/pages/app-deploy-status.tsx index a3d706bc7..c4437e4c2 100644 --- a/src/ui/pages/app-deploy-status.tsx +++ b/src/ui/pages/app-deploy-status.tsx @@ -692,6 +692,7 @@ const CreateEndpointForm = ({ app }: { app: DeployApp }) => { internal: false, ipAllowlist: [], envId: app.environmentId, + tokenHeader: undefined, }); const loader = useLoader(action); const onClick = () => { diff --git a/src/ui/pages/endpoint-detail-settings.tsx b/src/ui/pages/endpoint-detail-settings.tsx index 355d585b9..987bfe728 100644 --- a/src/ui/pages/endpoint-detail-settings.tsx +++ b/src/ui/pages/endpoint-detail-settings.tsx @@ -10,6 +10,7 @@ import { selectServiceById, updateEndpoint, } from "@app/deploy"; +import { selectHasTokenHeaderFeature } from "@app/organizations"; import { useDispatch, useLoader, @@ -62,14 +63,17 @@ const EndpointSettings = ({ endpointId }: { endpointId: string }) => { const image = useSelector((s) => selectImageById(s, { id: app.currentImageId }), ); + const hasTokenHeaderFeature = useSelector(selectHasTokenHeaderFeature); const exposedPorts = image.exposedPorts; - const origAllowlist = enp.ipWhitelist.join("\n"); const [ipAllowlist, setIpAllowlist] = useState(origAllowlist); const [port, setPort] = useState(enp.containerPort); const [certId, setCertId] = useState(enp.certificateId); const [cert, setCert] = useState(""); const [privKey, setPrivKey] = useState(""); + const [tokenHeader, setTokenHeader] = useState( + enp.tokenHeader as string | undefined, + ); const [usingNewCert, setUsingNewCert] = useState(false); useEffect(() => { @@ -90,12 +94,16 @@ const EndpointSettings = ({ endpointId }: { endpointId: string }) => { envId: service.environmentId, cert, privKey, + tokenHeader, requiresCert: isRequiresCert(enp), + enpType: enp.type, }; const ipsSame = origAllowlist === ipAllowlist; const portSame = enp.containerPort === port; const certSame = enp.certificateId === certId; - const isDisabled = ipsSame && portSame && certSame && cert === ""; + const tokenSame = enp.tokenHeader === tokenHeader; + const isDisabled = + ipsSame && portSame && certSame && cert === "" && tokenSame; const curPortText = getContainerPort(enp, exposedPorts); const loader = useLoader(updateEndpoint); const [errors, validate] = useValidator< @@ -211,6 +219,26 @@ const EndpointSettings = ({ endpointId }: { endpointId: string }) => { ) : null; + const tokenEditForm = + (data.enpType === "http" || + data.enpType === "http_proxy_protocol" || + data.enpType === "grpc") && + hasTokenHeaderFeature ? ( + + setTokenHeader(e.currentTarget.value)} + /> + + ) : null; + return (

Endpoint Settings

@@ -219,6 +247,7 @@ const EndpointSettings = ({ endpointId }: { endpointId: string }) => { {portForm} {certEditForm} + {tokenEditForm}