mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 06:29:37 +00:00
Merge branch 'main' into fix/user-stuck-on-invited
This commit is contained in:
+21
-1
@@ -131,7 +131,27 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipClassName="max-w-md"
|
||||
tooltipText="When a secret is synced, its key will be injected into the key schema before it reaches the destination. This is useful for organization."
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-3">
|
||||
<span>
|
||||
When a secret is synced, values will be injected into the key schema before it
|
||||
reaches the destination. This is useful for organization.
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<span>Available keys:</span>
|
||||
<ul className="list-disc pl-4 text-sm">
|
||||
<li>
|
||||
<code>{"{{secretKey}}"}</code> - The key of the secret
|
||||
</li>
|
||||
<li>
|
||||
<code>{"{{environment}}"}</code> - The environment which the secret is in
|
||||
(e.g. dev, staging, prod)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
|
||||
@@ -13,11 +13,27 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) =>
|
||||
!val || /^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/.test(val),
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
const allowedOptionalPlaceholders = ["{{environment}}"];
|
||||
|
||||
const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders]
|
||||
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
|
||||
.join("|");
|
||||
|
||||
const allowedContentRegex = new RegExp(
|
||||
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
|
||||
);
|
||||
const contentIsValid = allowedContentRegex.test(val);
|
||||
|
||||
const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length;
|
||||
|
||||
return contentIsValid && secretKeyCount === 1;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
|
||||
"Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -96,7 +96,7 @@ export const useUpdateOrgRole = () => {
|
||||
data: { role }
|
||||
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
|
||||
...dto,
|
||||
permissions: permissions?.length ? packRules(permissions) : undefined
|
||||
permissions: permissions ? packRules(permissions) : undefined
|
||||
});
|
||||
|
||||
return role;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
|
||||
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
@@ -72,6 +73,8 @@ export const AccessManagementPage = () => {
|
||||
}
|
||||
];
|
||||
|
||||
const hasNoAccess = tabSections.every((tab) => tab.isHidden);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
@@ -126,6 +129,7 @@ export const AccessManagementPage = () => {
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
{hasNoAccess && <OrgPermissionGuardBanner />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+133
-71
@@ -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,113 @@ 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) => {
|
||||
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 +417,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"
|
||||
|
||||
Reference in New Issue
Block a user