Skip to content

Commit

Permalink
token_header_implementation (#993)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
michaelwang13 authored Dec 12, 2024
1 parent 95764ee commit 6483174
Show file tree
Hide file tree
Showing 12 changed files with 122 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions src/deploy/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface DeployEndpointResponse {
status: ProvisionableStatus;
created_at: string;
updated_at: string;
token_header: string | undefined;
_links: {
service: LinkResponse;
certificate: LinkResponse;
Expand Down Expand Up @@ -92,6 +93,7 @@ export const defaultEndpointResponse = (
ip_whitelist: [],
platform: "elb",
type: "unknown",
token_header: undefined,
user_domain: "",
virtual_domain: "",
security_group_id: "",
Expand Down Expand Up @@ -135,6 +137,7 @@ export const deserializeDeployEndpoint = (
status: payload.status,
serviceId: extractIdFromLink(payload._links.service),
certificateId: extractIdFromLink(payload._links.certificate),
tokenHeader: payload.token_header,
};
};

Expand Down Expand Up @@ -457,6 +460,7 @@ interface CreateEndpointBase {
internal: boolean;
ipAllowlist: string[];
containerPort?: string;
tokenHeader: string | undefined;
}

interface CreateDefaultEndpoint extends CreateEndpointBase {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -731,6 +736,7 @@ interface EndpointPatchProps {
ipAllowlist: string[];
containerPort: string;
certId: string;
tokenHeader: string | undefined;
}

export interface EndpointUpdateProps extends EndpointPatchProps {
Expand All @@ -747,6 +753,7 @@ const patchEndpoint = api.patch<EndpointPatchProps>(
const data: Record<string, any> = {
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}`;
Expand Down Expand Up @@ -786,6 +793,7 @@ export const updateEndpoint = thunks.create<EndpointUpdateProps>(
ipAllowlist: ctx.payload.ipAllowlist,
containerPort: ctx.payload.containerPort,
certId,
tokenHeader: ctx.payload.tokenHeader,
});
if (!patchCtx.json.ok) {
yield* schema.update(
Expand Down Expand Up @@ -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<DeployEndpoint, "containerPort" | "containerPorts">,
exposedPorts: number[],
Expand Down Expand Up @@ -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),
};
};

Expand Down
17 changes: 17 additions & 0 deletions src/organizations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/schema/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export const defaultConfig = (e: Partial<Config> = {}): 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",
Expand Down Expand Up @@ -165,6 +168,7 @@ export const defaultDeployEndpoint = (
ipWhitelist: [],
platform: "elb",
type: "unknown",
tokenHeader: undefined,
createdAt: now,
updatedAt: now,
userDomain: "",
Expand Down
1 change: 1 addition & 0 deletions src/types/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface DeployEndpoint extends Provisionable, Timestamps {
virtualDomain: string;
serviceId: string;
certificateId: string;
tokenHeader: string | undefined;
}

export type OnboardingStatus =
Expand Down
1 change: 1 addition & 0 deletions src/types/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface Config {
stripePublishableKey: string;
origin: "app";
betaFeatureOrgIds: string;
tokenHeaderOrgIds: string;
}

export interface Feedback {
Expand Down
7 changes: 7 additions & 0 deletions src/ui/layouts/endpoint-detail-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -71,6 +72,7 @@ export function EndpointAppHeaderInfo({
selectImageById(s, { id: app.currentImageId }),
);
const portTxt = getContainerPort(enp, image.exposedPorts);
const hasTokenHeaderFeature = useSelector(selectHasTokenHeaderFeature);

return (
<DetailHeader>
Expand Down Expand Up @@ -116,6 +118,11 @@ export function EndpointAppHeaderInfo({
<DetailInfoItem title="Status">
<EndpointStatusPill status={enp.status} />
</DetailInfoItem>
{hasTokenHeaderFeature ? (
<DetailInfoItem title="Using Header Auth">
{txt.token_header}
</DetailInfoItem>
) : null}
</DetailInfoGrid>
</DetailHeader>
);
Expand Down
32 changes: 32 additions & 0 deletions src/ui/pages/app-create-endpoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
selectImageById,
selectStackById,
} from "@app/deploy";
import { selectHasTokenHeaderFeature } from "@app/organizations";
import {
useDispatch,
useLoader,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -110,6 +120,7 @@ export const AppCreateEndpointPage = () => {
internal: enpPlacement === "internal",
ipAllowlist: parseIpStr(ipAllowlist),
containerPort: port,
tokenHeader: tokenHeader,
};

if (enpType === "managed") {
Expand Down Expand Up @@ -297,6 +308,24 @@ export const AppCreateEndpointPage = () => {
</FormGroup>
);

const tokenHeaderForm = (
<FormGroup
label="Header Authentication Value"
htmlFor="token-header"
description="When set, only traffic with a 'X-Origin-Token' header matching this value will be allowed."
feedbackMessage={errors.tokenHeader}
feedbackVariant={errors.tokenHeader ? "danger" : "info"}
>
<Input
id="token-header"
name="token-header"
type="text"
value={tokenHeader}
onChange={(e) => setTokenHeader(e.currentTarget.value)}
/>
</FormGroup>
);

const getProtocolName = (trafficType: EndpointType) => {
switch (trafficType) {
case "grpc":
Expand All @@ -319,6 +348,7 @@ export const AppCreateEndpointPage = () => {
{transCert && usingNewCert ? certForm : null}
{transCert && usingNewCert ? privKeyForm : null}
{ipAllowlistForm}
{hasTokenHeaderFeature ? tokenHeaderForm : null}
</>
);
}
Expand All @@ -331,13 +361,15 @@ export const AppCreateEndpointPage = () => {
{usingNewCert ? certForm : null}
{usingNewCert ? privKeyForm : null}
{ipAllowlistForm}
{hasTokenHeaderFeature ? tokenHeaderForm : null}
</>
);
}
return (
<>
{enpPlacementForm}
{ipAllowlistForm}
{hasTokenHeaderFeature ? tokenHeaderForm : null}
</>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/ui/pages/app-deploy-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ const CreateEndpointForm = ({ app }: { app: DeployApp }) => {
internal: false,
ipAllowlist: [],
envId: app.environmentId,
tokenHeader: undefined,
});
const loader = useLoader(action);
const onClick = () => {
Expand Down
33 changes: 31 additions & 2 deletions src/ui/pages/endpoint-detail-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
selectServiceById,
updateEndpoint,
} from "@app/deploy";
import { selectHasTokenHeaderFeature } from "@app/organizations";
import {
useDispatch,
useLoader,
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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<
Expand Down Expand Up @@ -211,6 +219,26 @@ const EndpointSettings = ({ endpointId }: { endpointId: string }) => {
</FormGroup>
) : null;

const tokenEditForm =
(data.enpType === "http" ||
data.enpType === "http_proxy_protocol" ||
data.enpType === "grpc") &&
hasTokenHeaderFeature ? (
<FormGroup
label="Header Authentication Value"
htmlFor="token-header"
description={`The 'X-Origin-Token' header value. When set, clients will be required to pass a 'X-Origin-Token' header matching this value.`}
>
<Input
type="text"
id="token-header"
name="token-header"
value={tokenHeader}
onChange={(e) => setTokenHeader(e.currentTarget.value)}
/>
</FormGroup>
) : null;

return (
<Box>
<h1 className="text-lg text-gray-500 mb-4">Endpoint Settings</h1>
Expand All @@ -219,6 +247,7 @@ const EndpointSettings = ({ endpointId }: { endpointId: string }) => {

{portForm}
{certEditForm}
{tokenEditForm}

<FormGroup
label="IP Allowlist"
Expand Down
1 change: 1 addition & 0 deletions src/vite.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ interface ImportMetaEnv {
VITE_MINTLIFY_CHAT_KEY: string;
VITE_STRIPE_PUBLISHABLE_KEY: string;
VITE_FEATURE_BETA_ORG_IDS: string;
VITE_TOKEN_HEADER_ORG_IDS: string;
}

0 comments on commit 6483174

Please sign in to comment.