feat(identities/kubernetes-auth): use gateway as token reviewer

This commit is contained in:
Daniel Hougaard
2025-06-05 04:42:15 +04:00
parent 6ae7b5e996
commit 07902f7db9
14 changed files with 615 additions and 126 deletions
@@ -843,7 +843,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}) => {
const {
data: { identityKubernetesAuth }
@@ -860,7 +861,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}
);
@@ -950,7 +952,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}) => {
const {
data: { identityKubernetesAuth }
@@ -967,7 +970,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
gatewayId
gatewayId,
tokenReviewMode
}
);
@@ -379,10 +379,16 @@ export type DeleteIdentityAzureAuthDTO = {
identityId: string;
};
export enum IdentityKubernetesAuthTokenReviewMode {
Api = "api",
Gateway = "gateway"
}
export type IdentityKubernetesAuth = {
identityId: string;
kubernetesHost: string;
tokenReviewerJwt: string;
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
@@ -399,6 +405,7 @@ export type AddIdentityKubernetesAuthDTO = {
identityId: string;
kubernetesHost: string;
tokenReviewerJwt?: string;
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
@@ -417,6 +424,7 @@ export type UpdateIdentityKubernetesAuthDTO = {
identityId: string;
kubernetesHost?: string;
tokenReviewerJwt?: string | null;
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;
@@ -33,13 +33,19 @@ import {
useGetIdentityKubernetesAuth,
useUpdateIdentityKubernetesAuth
} from "@app/hooks/api";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import {
IdentityKubernetesAuthTokenReviewMode,
IdentityTrustedIp
} from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
tokenReviewMode: z
.nativeEnum(IdentityKubernetesAuthTokenReviewMode)
.default(IdentityKubernetesAuthTokenReviewMode.Api),
kubernetesHost: z.string().min(1),
tokenReviewerJwt: z.string().optional(),
gatewayId: z.string().optional().nullable(),
@@ -62,7 +68,15 @@ const schema = z
)
.min(1)
})
.required();
.superRefine((data, ctx) => {
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
ctx.addIssue({
path: ["gatewayId"],
code: z.ZodIssueCode.custom,
message: "When token review mode is set to Gateway, a gateway must be selected"
});
}
});
export type FormData = z.infer<typeof schema>;
@@ -100,11 +114,14 @@ export const IdentityKubernetesAuthForm = ({
control,
handleSubmit,
reset,
watch,
setValue,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode.Api,
kubernetesHost: "",
tokenReviewerJwt: "",
allowedNames: "",
@@ -128,6 +145,7 @@ export const IdentityKubernetesAuthForm = ({
useEffect(() => {
if (data) {
reset({
tokenReviewMode: data.tokenReviewMode,
kubernetesHost: data.kubernetesHost,
tokenReviewerJwt: data.tokenReviewerJwt,
allowedNames: data.allowedNames,
@@ -148,6 +166,7 @@ export const IdentityKubernetesAuthForm = ({
});
} else {
reset({
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode.Api,
kubernetesHost: "",
tokenReviewerJwt: "",
allowedNames: "",
@@ -173,6 +192,7 @@ export const IdentityKubernetesAuthForm = ({
accessTokenMaxTTL,
accessTokenNumUsesLimit,
gatewayId,
tokenReviewMode,
accessTokenTrustedIps
}: FormData) => {
try {
@@ -189,6 +209,7 @@ export const IdentityKubernetesAuthForm = ({
caCert,
identityId,
gatewayId: gatewayId || null,
tokenReviewMode,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@@ -205,6 +226,7 @@ export const IdentityKubernetesAuthForm = ({
allowedAudience: allowedAudience || "",
gatewayId: gatewayId || null,
caCert: caCert || "",
tokenReviewMode,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@@ -228,6 +250,8 @@ export const IdentityKubernetesAuthForm = ({
}
};
const tokenReviewMode = watch("tokenReviewMode");
return (
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
@@ -235,6 +259,7 @@ export const IdentityKubernetesAuthForm = ({
[
"kubernetesHost",
"tokenReviewerJwt",
"tokenReviewMode",
"gatewayId",
"accessTokenTTL",
"accessTokenMaxTTL",
@@ -269,21 +294,114 @@ export const IdentityKubernetesAuthForm = ({
</FormControl>
)}
/>
<Controller
control={control}
name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
label="Token Reviewer JWT"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
<div className="flex w-full items-center gap-2">
<div className="w-full flex-1">
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
<Input {...field} placeholder="" type="password" />
</FormControl>
)}
/>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
isOptional
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={(v) => {
console.log("v", v);
if (v !== "") {
onChange(v);
}
if (v === null) {
setValue(
"tokenReviewMode",
IdentityKubernetesAuthTokenReviewMode.Api,
{
shouldDirty: true,
shouldTouch: true
}
);
}
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => {
onChange(null);
}}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
</div>
<Controller
control={control}
name="tokenReviewMode"
render={({ field }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText="The method of which tokens are reviewed. If you select Gateway as Reviewer, the selected gateway will be used to review tokens with. If this option is enabled, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding."
label="Review Mode"
>
<Select value={field.value} onValueChange={field.onChange}>
<SelectItem value="gateway">Gateway as Reviewer</SelectItem>
<SelectItem value="api">Manual Token Reviewer JWT (API)</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
{tokenReviewMode === "api" && (
<Controller
control={control}
name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
label="Token Reviewer JWT"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
>
<Input {...field} placeholder="" type="password" />
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue=""
@@ -300,61 +418,6 @@ export const IdentityKubernetesAuthForm = ({
)}
/>
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
isOptional
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={(v) => {
if (v !== "") {
onChange(v);
}
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(null)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
<Controller
control={control}
name="allowedNames"