Merge pull request #2477 from meetcshah19/meet/eng-1519-allow-users-to-change-auth-method-in-the-ui-easily

feat: allow users to replace auth methods
This commit is contained in:
Maidul Islam
2024-09-30 23:38:02 -04:00
committed by GitHub
9 changed files with 1921 additions and 1539 deletions
+87 -23
View File
@@ -1,4 +1,4 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
@@ -13,7 +13,8 @@ import {
IdentityMembershipOrg, IdentityMembershipOrg,
IdentityOidcAuth, IdentityOidcAuth,
IdentityTokenAuth, IdentityTokenAuth,
IdentityUniversalAuth} from "./types"; IdentityUniversalAuth
} from "./types";
export const identitiesKeys = { export const identitiesKeys = {
getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const, getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const,
@@ -64,9 +65,16 @@ export const useGetIdentityProjectMemberships = (identityId: string) => {
}); });
}; };
export const useGetIdentityUniversalAuth = (identityId: string) => { export const useGetIdentityUniversalAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityUniversalAuth,
unknown,
IdentityUniversalAuth,
ReturnType<typeof identitiesKeys.getIdentityUniversalAuth>
>
) => {
return useQuery({ return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityUniversalAuth(identityId), queryKey: identitiesKeys.getIdentityUniversalAuth(identityId),
queryFn: async () => { queryFn: async () => {
const { const {
@@ -76,8 +84,10 @@ export const useGetIdentityUniversalAuth = (identityId: string) => {
); );
return identityUniversalAuth; return identityUniversalAuth;
}, },
cacheTime: 0,
staleTime: 0, staleTime: 0,
cacheTime: 0 ...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
}); });
}; };
@@ -96,9 +106,16 @@ export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) =>
}); });
}; };
export const useGetIdentityGcpAuth = (identityId: string) => { export const useGetIdentityGcpAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityGcpAuth,
unknown,
IdentityGcpAuth,
ReturnType<typeof identitiesKeys.getIdentityGcpAuth>
>
) => {
return useQuery({ return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityGcpAuth(identityId), queryKey: identitiesKeys.getIdentityGcpAuth(identityId),
queryFn: async () => { queryFn: async () => {
const { const {
@@ -109,13 +126,22 @@ export const useGetIdentityGcpAuth = (identityId: string) => {
return identityGcpAuth; return identityGcpAuth;
}, },
staleTime: 0, staleTime: 0,
cacheTime: 0 cacheTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
}); });
}; };
export const useGetIdentityAwsAuth = (identityId: string) => { export const useGetIdentityAwsAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityAwsAuth,
unknown,
IdentityAwsAuth,
ReturnType<typeof identitiesKeys.getIdentityAwsAuth>
>
) => {
return useQuery({ return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityAwsAuth(identityId), queryKey: identitiesKeys.getIdentityAwsAuth(identityId),
queryFn: async () => { queryFn: async () => {
const { const {
@@ -126,13 +152,22 @@ export const useGetIdentityAwsAuth = (identityId: string) => {
return identityAwsAuth; return identityAwsAuth;
}, },
staleTime: 0, staleTime: 0,
cacheTime: 0 cacheTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
}); });
}; };
export const useGetIdentityAzureAuth = (identityId: string) => { export const useGetIdentityAzureAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityAzureAuth,
unknown,
IdentityAzureAuth,
ReturnType<typeof identitiesKeys.getIdentityAzureAuth>
>
) => {
return useQuery({ return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityAzureAuth(identityId), queryKey: identitiesKeys.getIdentityAzureAuth(identityId),
queryFn: async () => { queryFn: async () => {
const { const {
@@ -143,13 +178,22 @@ export const useGetIdentityAzureAuth = (identityId: string) => {
return identityAzureAuth; return identityAzureAuth;
}, },
staleTime: 0, staleTime: 0,
cacheTime: 0 cacheTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
}); });
}; };
export const useGetIdentityKubernetesAuth = (identityId: string) => { export const useGetIdentityKubernetesAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityKubernetesAuth,
unknown,
IdentityKubernetesAuth,
ReturnType<typeof identitiesKeys.getIdentityKubernetesAuth>
>
) => {
return useQuery({ return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityKubernetesAuth(identityId), queryKey: identitiesKeys.getIdentityKubernetesAuth(identityId),
queryFn: async () => { queryFn: async () => {
const { const {
@@ -160,13 +204,22 @@ export const useGetIdentityKubernetesAuth = (identityId: string) => {
return identityKubernetesAuth; return identityKubernetesAuth;
}, },
staleTime: 0, staleTime: 0,
cacheTime: 0 cacheTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
}); });
}; };
export const useGetIdentityTokenAuth = (identityId: string) => { export const useGetIdentityTokenAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityTokenAuth,
unknown,
IdentityTokenAuth,
ReturnType<typeof identitiesKeys.getIdentityTokenAuth>
>
) => {
return useQuery({ return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityTokenAuth(identityId), queryKey: identitiesKeys.getIdentityTokenAuth(identityId),
queryFn: async () => { queryFn: async () => {
const { const {
@@ -177,7 +230,9 @@ export const useGetIdentityTokenAuth = (identityId: string) => {
return identityTokenAuth; return identityTokenAuth;
}, },
staleTime: 0, staleTime: 0,
cacheTime: 0 cacheTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
}); });
}; };
@@ -196,9 +251,16 @@ export const useGetIdentityTokensTokenAuth = (identityId: string) => {
}); });
}; };
export const useGetIdentityOidcAuth = (identityId: string) => { export const useGetIdentityOidcAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityOidcAuth,
unknown,
IdentityOidcAuth,
ReturnType<typeof identitiesKeys.getIdentityOidcAuth>
>
) => {
return useQuery({ return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityOidcAuth(identityId), queryKey: identitiesKeys.getIdentityOidcAuth(identityId),
queryFn: async () => { queryFn: async () => {
const { const {
@@ -209,6 +271,8 @@ export const useGetIdentityOidcAuth = (identityId: string) => {
return identityOidcAuth; return identityOidcAuth;
}, },
staleTime: 0, staleTime: 0,
cacheTime: 0 cacheTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
}); });
}; };
@@ -21,7 +21,8 @@ import {
useDeleteIdentityKubernetesAuth, useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth, useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth, useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth} from "@app/hooks/api"; useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -54,7 +55,10 @@ const identityAuthMethods = [
const schema = yup const schema = yup
.object({ .object({
authMethod: yup.string().required("Auth method is required") authMethod: yup
.mixed<IdentityAuthMethod>()
.oneOf(Object.values(IdentityAuthMethod))
.required("Auth method is required")
}) })
.required(); .required();
@@ -72,14 +76,26 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { control, watch, setValue } = useForm<FormData>({ const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod;
const { control, watch, setValue, reset } = useForm<FormData>({
resolver: yupResolver(schema), resolver: yupResolver(schema),
defaultValues: { defaultValues: {
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH authMethod: initialAuthMethod
} }
}); });
const identityAuthMethodData = popUp?.identityAuthMethod?.data as { useEffect(() => {
// reset form on open
if (popUp.identityAuthMethod.isOpen)
reset({ authMethod: popUp?.identityAuthMethod?.data?.authMethod });
}, [popUp.identityAuthMethod.isOpen]);
const identityAuthMethodData = {
identityId: popUp?.identityAuthMethod.data?.identityId,
name: popUp?.identityAuthMethod?.data?.name,
authMethod: watch("authMethod")
} as {
identityId: string; identityId: string;
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
@@ -94,85 +110,10 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH); setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH);
}, [identityAuthMethodData?.authMethod]); }, [identityAuthMethodData?.authMethod]);
const authMethod = watch("authMethod"); const onRevokeAuthMethodSubmit = async (authMethod: IdentityAuthMethod) => {
if (!orgId || !authMethod) return;
const renderIdentityAuthForm = () => {
switch (identityAuthMethodData?.authMethod ?? authMethod) {
case IdentityAuthMethod.AWS_AUTH: {
return (
<IdentityAwsAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.KUBERNETES_AUTH: {
return (
<IdentityKubernetesAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.GCP_AUTH: {
return (
<IdentityGcpAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.AZURE_AUTH: {
return (
<IdentityAzureAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.UNIVERSAL_AUTH: {
return (
<IdentityUniversalAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.OIDC_AUTH: {
return (
<IdentityOidcAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.TOKEN_AUTH: {
return (
<IdentityTokenAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
default: {
return <div />;
}
}
};
const onRevokeAuthMethodSubmit = async () => {
if (!identityAuthMethodData.authMethod) return;
if (!orgId) return;
try { try {
console.log("onRevokeAuthMethodSubmit identityId: ", identityAuthMethodData); switch (authMethod) {
switch (identityAuthMethodData.authMethod) {
case IdentityAuthMethod.UNIVERSAL_AUTH: { case IdentityAuthMethod.UNIVERSAL_AUTH: {
await revokeUniversalAuth({ await revokeUniversalAuth({
identityId: identityAuthMethodData.identityId, identityId: identityAuthMethodData.identityId,
@@ -227,9 +168,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
} }
createNotification({ createNotification({
text: `Successfully removed ${ text: `Successfully removed ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
identityAuthToNameMap[identityAuthMethodData.authMethod]
} on ${identityAuthMethodData.name}`,
type: "success" type: "success"
}); });
@@ -238,13 +177,95 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createNotification({ createNotification({
text: `Failed to remove ${identityAuthToNameMap[identityAuthMethodData.authMethod]} on ${ text: `Failed to remove ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
identityAuthMethodData.name
}`,
type: "error" type: "error"
}); });
} }
}; };
const renderIdentityAuthForm = () => {
switch (identityAuthMethodData.authMethod) {
case IdentityAuthMethod.AWS_AUTH: {
return (
<IdentityAwsAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.KUBERNETES_AUTH: {
return (
<IdentityKubernetesAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.GCP_AUTH: {
return (
<IdentityGcpAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.AZURE_AUTH: {
return (
<IdentityAzureAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.UNIVERSAL_AUTH: {
return (
<IdentityUniversalAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.OIDC_AUTH: {
return (
<IdentityOidcAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.TOKEN_AUTH: {
return (
<IdentityTokenAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
default: {
return <div />;
}
}
};
return ( return (
<Modal <Modal
@@ -255,8 +276,10 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
> >
<ModalContent <ModalContent
title={`${ title={`${
identityAuthMethodData?.authMethod ? "Update" : "Configure" identityAuthMethodData.authMethod === initialAuthMethod ? "Update" : "Configure"
} Identity Auth Method for ${identityAuthMethodData?.name ?? ""}`} } Identity Auth Method for ${
identityAuthToNameMap[identityAuthMethodData.authMethod!] ?? ""
}`}
> >
<Controller <Controller
control={control} control={control}
@@ -267,9 +290,10 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
<Select <Select
defaultValue={field.value} defaultValue={field.value}
{...field} {...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => {
onChange(e);
}}
className="w-full" className="w-full"
isDisabled={!!identityAuthMethodData?.authMethod}
> >
{identityAuthMethods.map(({ label, value }) => ( {identityAuthMethods.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}> <SelectItem value={String(value || "")} key={label}>
@@ -295,7 +319,8 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
} on ${identityAuthMethodData?.name ?? ""}?`} } on ${identityAuthMethodData?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)} onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={onRevokeAuthMethodSubmit} buttonText="Remove"
onDeleteApproved={() => onRevokeAuthMethodSubmit(identityAuthMethodData.authMethod!)}
/> />
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityAwsAuth, useAddIdentityAwsAuth,
@@ -15,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup const schema = yup
.object({ .object({
@@ -64,12 +64,16 @@ type Props = {
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityAwsAuthForm = ({ export const IdentityAwsAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData identityAuthMethodData,
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -78,7 +82,12 @@ export const IdentityAwsAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? ""); const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod
});
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
@@ -175,9 +184,7 @@ export const IdentityAwsAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${ text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success" type: "success"
}); });
@@ -191,189 +198,216 @@ export const IdentityAwsAuthForm = ({
}; };
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <>
<Controller <form onSubmit={handleSubmit(onFormSubmit)}>
control={control} <Controller
defaultValue="2592000" control={control}
name="allowedPrincipalArns" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="allowedPrincipalArns"
<FormControl render={({ field, fieldState: { error } }) => (
label="Allowed Principal ARNs" <FormControl
isError={Boolean(error)} label="Allowed Principal ARNs"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input >
{...field} <Input
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..." {...field}
type="text" placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
/> type="text"
</FormControl> />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="allowedAccountIds" control={control}
render={({ field, fieldState: { error } }) => ( name="allowedAccountIds"
<FormControl render={({ field, fieldState: { error } }) => (
label="Allowed Account IDs" <FormControl
isError={Boolean(error)} label="Allowed Account IDs"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="123456789012, ..." /> >
</FormControl> <Input {...field} placeholder="123456789012, ..." />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="https://sts.amazonaws.com/" control={control}
name="stsEndpoint" defaultValue="https://sts.amazonaws.com/"
render={({ field, fieldState: { error } }) => ( name="stsEndpoint"
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}> render={({ field, fieldState: { error } }) => (
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" /> <FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
</FormControl> <Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenTTL" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="accessTokenTTL"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token TTL (seconds)" <FormControl
isError={Boolean(error)} label="Access Token TTL (seconds)"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenMaxTTL" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="accessTokenMaxTTL"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max TTL (seconds)" <FormControl
isError={Boolean(error)} label="Access Token Max TTL (seconds)"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="0" control={control}
name="accessTokenNumUsesLimit" defaultValue="0"
render={({ field, fieldState: { error } }) => ( name="accessTokenNumUsesLimit"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max Number of Uses" <FormControl
isError={Boolean(error)} label="Access Token Max Number of Uses"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="0" type="number" min="0" step="1" /> >
</FormControl> <Input {...field} placeholder="0" type="number" min="0" step="1" />
)} </FormControl>
/> )}
{accessTokenTrustedIpsFields.map(({ id }, index) => ( />
<div className="mb-3 flex items-end space-x-2" key={id}> {accessTokenTrustedIpsFields.map(({ id }, index) => (
<Controller <div className="mb-3 flex items-end space-x-2" key={id}>
control={control} <Controller
name={`accessTokenTrustedIps.${index}.ipAddress`} control={control}
defaultValue="0.0.0.0/0" name={`accessTokenTrustedIps.${index}.ipAddress`}
render={({ field, fieldState: { error } }) => { defaultValue="0.0.0.0/0"
return ( render={({ field, fieldState: { error } }) => {
<FormControl return (
className="mb-0 flex-grow" <FormControl
label={index === 0 ? "Access Token Trusted IPs" : undefined} className="mb-0 flex-grow"
isError={Boolean(error)} label={index === 0 ? "Access Token Trusted IPs" : undefined}
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input >
value={field.value} <Input
onChange={(e) => { value={field.value}
if (subscription?.ipAllowlisting) { onChange={(e) => {
field.onChange(e); if (subscription?.ipAllowlisting) {
return; field.onChange(e);
} return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="danger" size="xs"
variant="plain"
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> Add IP Address
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
{identityAuthMethodData?.authMethod && ( <div className="flex justify-between">
<Button <div className="flex items-center">
size="sm" {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
colorSchema="danger" <Button
isLoading={isSubmitting} className="mr-4"
isDisabled={isSubmitting} size="sm"
onClick={() => handlePopUpToggle("revokeAuthMethod", true)} isLoading={isSubmitting}
> isDisabled={isSubmitting}
Remove Auth Method onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
</Button> >
)} Overwrite
</div> </Button>
</form> ) : (
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };
@@ -6,7 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityAzureAuth, useAddIdentityAzureAuth,
@@ -15,11 +15,11 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
tenantId: z.string(), tenantId: z.string().min(1),
resource: z.string(), resource: z.string(),
allowedServicePrincipalIds: z.string(), allowedServicePrincipalIds: z.string(),
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, { accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
@@ -52,12 +52,16 @@ type Props = {
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityAzureAuthForm = ({ export const IdentityAzureAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData identityAuthMethodData,
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -66,12 +70,18 @@ export const IdentityAzureAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? ""); const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod
});
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
trigger,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@@ -163,9 +173,7 @@ export const IdentityAzureAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${ text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success" type: "success"
}); });
@@ -179,189 +187,225 @@ export const IdentityAzureAuthForm = ({
}; };
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <>
<Controller <form onSubmit={handleSubmit(onFormSubmit)}>
control={control} <Controller
defaultValue="2592000" control={control}
name="tenantId" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="tenantId"
<FormControl render={({ field, fieldState: { error } }) => (
label="Tenant ID" <FormControl
isError={Boolean(error)} label="Tenant ID"
errorText={error?.message} isError={Boolean(error)}
isRequired errorText={error?.message}
> isRequired
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" /> >
</FormControl> <Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="resource" control={control}
render={({ field, fieldState: { error } }) => ( name="resource"
<FormControl render={({ field, fieldState: { error } }) => (
label="Resource / Audience" <FormControl
isError={Boolean(error)} label="Resource / Audience"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="https://management.azure.com/" /> >
</FormControl> <Input {...field} placeholder="https://management.azure.com/" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="allowedServicePrincipalIds" control={control}
render={({ field, fieldState: { error } }) => ( name="allowedServicePrincipalIds"
<FormControl render={({ field, fieldState: { error } }) => (
label="Allowed Service Principal IDs" <FormControl
isError={Boolean(error)} label="Allowed Service Principal IDs"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." /> >
</FormControl> <Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenTTL" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="accessTokenTTL"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token TTL (seconds)" <FormControl
isError={Boolean(error)} label="Access Token TTL (seconds)"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenMaxTTL" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="accessTokenMaxTTL"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max TTL (seconds)" <FormControl
isError={Boolean(error)} label="Access Token Max TTL (seconds)"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="0" control={control}
name="accessTokenNumUsesLimit" defaultValue="0"
render={({ field, fieldState: { error } }) => ( name="accessTokenNumUsesLimit"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max Number of Uses" <FormControl
isError={Boolean(error)} label="Access Token Max Number of Uses"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="0" type="number" min="0" step="1" /> >
</FormControl> <Input {...field} placeholder="0" type="number" min="0" step="1" />
)} </FormControl>
/> )}
{accessTokenTrustedIpsFields.map(({ id }, index) => ( />
<div className="mb-3 flex items-end space-x-2" key={id}> {accessTokenTrustedIpsFields.map(({ id }, index) => (
<Controller <div className="mb-3 flex items-end space-x-2" key={id}>
control={control} <Controller
name={`accessTokenTrustedIps.${index}.ipAddress`} control={control}
defaultValue="0.0.0.0/0" name={`accessTokenTrustedIps.${index}.ipAddress`}
render={({ field, fieldState: { error } }) => { defaultValue="0.0.0.0/0"
return ( render={({ field, fieldState: { error } }) => {
<FormControl return (
className="mb-0 flex-grow" <FormControl
label={index === 0 ? "Access Token Trusted IPs" : undefined} className="mb-0 flex-grow"
isError={Boolean(error)} label={index === 0 ? "Access Token Trusted IPs" : undefined}
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input >
value={field.value} <Input
onChange={(e) => { value={field.value}
if (subscription?.ipAllowlisting) { onChange={(e) => {
field.onChange(e); if (subscription?.ipAllowlisting) {
return; field.onChange(e);
} return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="danger" size="xs"
variant="plain"
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> Add IP Address
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
{identityAuthMethodData?.authMethod && ( <div className="flex justify-between">
<Button <div className="flex items-center">
size="sm" {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
colorSchema="danger" <Button
isLoading={isSubmitting} className="mr-4"
isDisabled={isSubmitting} size="sm"
onClick={() => handlePopUpToggle("revokeAuthMethod", true)} isLoading={isSubmitting}
> isDisabled={isSubmitting}
Remove Auth Method onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
</Button> >
)} Overwrite
</div> </Button>
</form> ) : (
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
const result = await trigger();
if (result) {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
} else {
createNotification({
text: "Please fill in all required fields",
type: "error"
});
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
}
}}
/>
</>
); );
}; };
@@ -6,7 +6,15 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2"; import {
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityGcpAuth, useAddIdentityGcpAuth,
@@ -15,7 +23,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
@@ -53,12 +61,16 @@ type Props = {
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityGcpAuthForm = ({ export const IdentityGcpAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData identityAuthMethodData,
revokeAuth,
initialAuthMethod
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -67,7 +79,11 @@ export const IdentityGcpAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? ""); const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod
});
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
@@ -100,7 +116,7 @@ export const IdentityGcpAuthForm = ({
useEffect(() => { useEffect(() => {
if (data) { if (data) {
reset({ reset({
type: data.type, type: data.type || "gce",
allowedServiceAccounts: data.allowedServiceAccounts, allowedServiceAccounts: data.allowedServiceAccounts,
allowedProjects: data.allowedProjects, allowedProjects: data.allowedProjects,
allowedZones: data.allowedZones, allowedZones: data.allowedZones,
@@ -117,7 +133,7 @@ export const IdentityGcpAuthForm = ({
}); });
} else { } else {
reset({ reset({
type: "iam", type: "gce",
allowedServiceAccounts: "", allowedServiceAccounts: "",
allowedProjects: "", allowedProjects: "",
allowedZones: "", allowedZones: "",
@@ -173,9 +189,7 @@ export const IdentityGcpAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${ text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success" type: "success"
}); });
@@ -189,213 +203,244 @@ export const IdentityGcpAuthForm = ({
}; };
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <>
<Controller <form onSubmit={handleSubmit(onFormSubmit)}>
control={control}
name="type"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
<SelectItem value="gce" key="gcp-type-gce">
GCP ID Token Auth (Recommended)
</SelectItem>
<SelectItem value="iam" key="gcpiam">
GCP IAM Auth
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="allowedServiceAccounts"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Account Emails"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="[email protected], [email protected]"
type="text"
/>
</FormControl>
)}
/>
{watchedType === "gce" && (
<Controller <Controller
control={control} control={control}
name="allowedProjects" name="type"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
<SelectItem value="gce" key="gce">
GCP ID Token Auth (Recommended)
</SelectItem>
<SelectItem value="iam" key="iam">
GCP IAM Auth
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="allowedServiceAccounts"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Allowed Projects" label="Allowed Service Account Emails"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} placeholder="my-gcp-project, ..." /> <Input
{...field}
placeholder="[email protected], [email protected]"
type="text"
/>
</FormControl> </FormControl>
)} )}
/> />
)} {watchedType === "gce" && (
{watchedType === "gce" && (
<Controller
control={control}
name="allowedZones"
render={({ field, fieldState: { error } }) => (
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller <Controller
control={control} control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`} name="allowedProjects"
defaultValue="0.0.0.0/0" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => { <FormControl
return ( label="Allowed Projects"
<FormControl isError={Boolean(error)}
className="mb-0 flex-grow" errorText={error?.message}
label={index === 0 ? "Access Token Trusted IPs" : undefined} >
isError={Boolean(error)} <Input {...field} placeholder="my-gcp-project, ..." />
errorText={error?.message} </FormControl>
> )}
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/> />
<IconButton )}
{watchedType === "gce" && (
<Controller
control={control}
name="allowedZones"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Zones"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="danger" size="xs"
variant="plain"
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> Add IP Address
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
{identityAuthMethodData?.authMethod && ( <div className="flex justify-between">
<Button <div className="flex items-center">
size="sm" {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
colorSchema="danger" <Button
isLoading={isSubmitting} className="mr-4"
isDisabled={isSubmitting} size="sm"
onClick={() => handlePopUpToggle("revokeAuthMethod", true)} isLoading={isSubmitting}
> isDisabled={isSubmitting}
Remove Auth Method onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
</Button> >
)} Overwrite
</div> </Button>
</form> ) : (
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
buttonText="Overwrite"
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };
@@ -6,7 +6,14 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2"; import {
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
TextArea
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityKubernetesAuth, useAddIdentityKubernetesAuth,
@@ -15,12 +22,12 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
kubernetesHost: z.string(), kubernetesHost: z.string().min(1),
tokenReviewerJwt: z.string(), tokenReviewerJwt: z.string().min(1),
allowedNames: z.string(), allowedNames: z.string(),
allowedNamespaces: z.string(), allowedNamespaces: z.string(),
allowedAudience: z.string(), allowedAudience: z.string(),
@@ -55,12 +62,16 @@ type Props = {
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityKubernetesAuthForm = ({ export const IdentityKubernetesAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData identityAuthMethodData,
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -69,12 +80,17 @@ export const IdentityKubernetesAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? ""); const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod
});
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
trigger,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@@ -184,9 +200,7 @@ export const IdentityKubernetesAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${ text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success" type: "success"
}); });
@@ -200,239 +214,277 @@ export const IdentityKubernetesAuthForm = ({
}; };
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <>
<Controller <form onSubmit={handleSubmit(onFormSubmit)}>
control={control} <Controller
defaultValue="2592000" control={control}
name="kubernetesHost" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="kubernetesHost"
<FormControl render={({ field, fieldState: { error } }) => (
label="Kubernetes Host / Base Kubernetes API URL " <FormControl
isError={Boolean(error)} label="Kubernetes Host / Base Kubernetes API URL "
errorText={error?.message} isError={Boolean(error)}
tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'" errorText={error?.message}
isRequired tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'"
> isRequired
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Token Reviewer JWT"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
isRequired
>
<Input {...field} placeholder="" type="password" />
</FormControl>
)}
/>
<Controller
control={control}
name="allowedNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Account Names"
isError={Boolean(error)}
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
errorText={error?.message}
>
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="allowedNamespaces"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Namespaces"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any namespaces."
>
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="allowedAudience"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Audience"
isError={Boolean(error)} errorText={error?.message}
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
> >
<Input {...field} placeholder="" type="text" /> <Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="caCert" name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="CA Certificate" label="Token Reviewer JWT"
errorText={error?.message} isError={Boolean(error)} isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server." errorText={error?.message}
> tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." /> isRequired
</FormControl> >
)} <Input {...field} placeholder="" type="password" />
/> </FormControl>
<Controller )}
control={control} />
defaultValue="2592000" <Controller
name="accessTokenTTL" control={control}
render={({ field, fieldState: { error } }) => ( name="allowedNames"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token TTL (seconds)" <FormControl
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time." label="Allowed Service Account Names"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="service-account-1-name, service-account-1-name" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenMaxTTL" defaultValue=""
render={({ field, fieldState: { error } }) => ( name="allowedNamespaces"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max TTL (seconds)" <FormControl
isError={Boolean(error)} label="Allowed Namespaces"
errorText={error?.message} isError={Boolean(error)}
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time." errorText={error?.message}
> tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any namespaces."
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="namespaceA, namespaceB" type="text" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="0" control={control}
name="accessTokenNumUsesLimit" defaultValue=""
render={({ field, fieldState: { error } }) => ( name="allowedAudience"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max Number of Uses" <FormControl
isError={Boolean(error)} label="Allowed Audience"
errorText={error?.message} isError={Boolean(error)}
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses." errorText={error?.message}
> tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
<Input {...field} placeholder="0" type="number" min="0" step="1" /> >
</FormControl> <Input {...field} placeholder="" type="text" />
)} </FormControl>
/> )}
{accessTokenTrustedIpsFields.map(({ id }, index) => ( />
<div className="mb-3 flex items-end space-x-2" key={id}> <Controller
<Controller control={control}
control={control} name="caCert"
name={`accessTokenTrustedIps.${index}.ipAddress`} render={({ field, fieldState: { error } }) => (
defaultValue="0.0.0.0/0" <FormControl
render={({ field, fieldState: { error } }) => { label="CA Certificate"
return ( errorText={error?.message}
<FormControl isError={Boolean(error)}
className="mb-0 flex-grow" tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
label={index === 0 ? "Access Token Trusted IPs" : undefined} >
isError={Boolean(error)} <TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
errorText={error?.message} </FormControl>
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address." )}
> />
<Input <Controller
value={field.value} control={control}
onChange={(e) => { defaultValue="2592000"
if (subscription?.ipAllowlisting) { name="accessTokenTTL"
field.onChange(e); render={({ field, fieldState: { error } }) => (
return; <FormControl
} label="Access Token TTL (seconds)"
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="danger" size="xs"
variant="plain"
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> Add IP Address
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
{identityAuthMethodData?.authMethod && ( <div className="flex justify-between">
<Button <div className="flex items-center">
size="sm" {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
colorSchema="danger" <Button
isLoading={isSubmitting} className="mr-4"
isDisabled={isSubmitting} size="sm"
onClick={() => handlePopUpToggle("revokeAuthMethod", true)} isLoading={isSubmitting}
> isDisabled={isSubmitting}
Remove Auth Method onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
</Button> >
)} Overwrite
</div> </Button>
</form> ) : (
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
const result = await trigger();
if (result) {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
} else {
createNotification({
text: "Please fill in all required fields",
type: "error"
});
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
}
}}
/>
</>
); );
}; };
@@ -7,13 +7,21 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2"; import {
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api"; import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries"; import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({ const schema = z.object({
accessTokenTrustedIps: z accessTokenTrustedIps: z
@@ -56,12 +64,16 @@ type Props = {
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityOidcAuthForm = ({ export const IdentityOidcAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData identityAuthMethodData,
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -70,12 +82,17 @@ export const IdentityOidcAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? ""); const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod
});
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
trigger,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@@ -193,9 +210,7 @@ export const IdentityOidcAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${ text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success" type: "success"
}); });
@@ -209,314 +224,350 @@ export const IdentityOidcAuthForm = ({
}; };
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <>
<Controller <form onSubmit={handleSubmit(onFormSubmit)}>
control={control} <Controller
name="oidcDiscoveryUrl" control={control}
render={({ field, fieldState: { error } }) => ( name="oidcDiscoveryUrl"
<FormControl render={({ field, fieldState: { error } }) => (
isRequired <FormControl
label="OIDC Discovery URL" isRequired
isError={Boolean(error)} label="OIDC Discovery URL"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input >
{...field} <Input
placeholder="https://token.actions.githubusercontent.com" {...field}
type="text" placeholder="https://token.actions.githubusercontent.com"
/> type="text"
</FormControl> />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="boundIssuer" control={control}
render={({ field, fieldState: { error } }) => ( name="boundIssuer"
<FormControl render={({ field, fieldState: { error } }) => (
isRequired <FormControl
label="Issuer" isRequired
isError={Boolean(error)} label="Issuer"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input >
{...field} <Input
type="text" {...field}
placeholder="https://token.actions.githubusercontent.com" type="text"
/> placeholder="https://token.actions.githubusercontent.com"
</FormControl> />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="caCert" control={control}
render={({ field, fieldState: { error } }) => ( name="caCert"
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}> render={({ field, fieldState: { error } }) => (
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." /> <FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
</FormControl> <TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="boundSubject" control={control}
render={({ field, fieldState: { error } }) => ( name="boundSubject"
<FormControl render={({ field, fieldState: { error } }) => (
label="Subject" <FormControl
isError={Boolean(error)} label="Subject"
errorText={error?.message} isError={Boolean(error)}
icon={ errorText={error?.message}
<Tooltip icon={
className="text-center" <Tooltip
content={<span>This field supports glob patterns</span>} className="text-center"
> content={<span>This field supports glob patterns</span>}
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<Input {...field} type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="boundAudiences"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Audiences"
isError={Boolean(error)}
errorText={error?.message}
icon={
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<Input {...field} type="text" placeholder="service1, service2" />
</FormControl>
)}
/>
{boundClaimsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`boundClaims.${index}.key`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Claims" : undefined}
icon={
index === 0 ? (
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
) : undefined
}
isError={Boolean(error)}
errorText={error?.message}
> >
<Input <FontAwesomeIcon icon={faQuestionCircle} size="sm" />
value={field.value} </Tooltip>
onChange={(e) => field.onChange(e)} }
placeholder="property" >
/> <Input {...field} type="text" />
</FormControl> </FormControl>
); )}
}} />
/> <Controller
<Controller control={control}
control={control} name="boundAudiences"
name={`boundClaims.${index}.value`} render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => { <FormControl
return ( label="Audiences"
<FormControl isError={Boolean(error)}
className="mb-0 flex-grow" errorText={error?.message}
isError={Boolean(error)} icon={
errorText={error?.message} <Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
> >
<Input <FontAwesomeIcon icon={faQuestionCircle} size="sm" />
value={field.value} </Tooltip>
onChange={(e) => field.onChange(e)} }
placeholder="value1, value2" >
/> <Input {...field} type="text" placeholder="service1, service2" />
</FormControl> </FormControl>
); )}
}} />
/> {boundClaimsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`boundClaims.${index}.key`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Claims" : undefined}
icon={
index === 0 ? (
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
) : undefined
}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="property"
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name={`boundClaims.${index}.value`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="value1, value2"
/>
</FormControl>
);
}}
/>
<IconButton <IconButton
onClick={() => removeBoundClaimField(index)} onClick={() => removeBoundClaimField(index)}
size="lg" size="lg"
colorSchema="danger" colorSchema="danger"
variant="plain" variant="plain"
ariaLabel="update" ariaLabel="update"
className="p-3" className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() =>
appendBoundClaimField({
key: "",
value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
<FontAwesomeIcon icon={faXmark} /> Add Claims
</IconButton> </Button>
</div> </div>
))} <Controller
<div className="my-4 ml-1"> control={control}
<Button defaultValue="2592000"
variant="outline_bg" name="accessTokenTTL"
onClick={() => render={({ field, fieldState: { error } }) => (
appendBoundClaimField({ <FormControl
key: "", label="Access Token TTL (seconds)"
value: "" isError={Boolean(error)}
}) errorText={error?.message}
} >
leftIcon={<FontAwesomeIcon icon={faPlus} />} <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
size="xs" </FormControl>
> )}
Add Claims />
</Button> <Controller
</div> control={control}
<Controller defaultValue="2592000"
control={control} name="accessTokenMaxTTL"
defaultValue="2592000" render={({ field, fieldState: { error } }) => (
name="accessTokenTTL" <FormControl
render={({ field, fieldState: { error } }) => ( label="Access Token Max TTL (seconds)"
<FormControl isError={Boolean(error)}
label="Access Token TTL (seconds)" errorText={error?.message}
isError={Boolean(error)} >
errorText={error?.message} <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
> </FormControl>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> )}
</FormControl> />
)} <Controller
/> control={control}
<Controller defaultValue="0"
control={control} name="accessTokenNumUsesLimit"
defaultValue="2592000" render={({ field, fieldState: { error } }) => (
name="accessTokenMaxTTL" <FormControl
render={({ field, fieldState: { error } }) => ( label="Access Token Max Number of Uses"
<FormControl isError={Boolean(error)}
label="Access Token Max TTL (seconds)" errorText={error?.message}
isError={Boolean(error)} >
errorText={error?.message} <Input {...field} placeholder="0" type="number" min="0" step="1" />
> </FormControl>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> )}
</FormControl> />
)} {accessTokenTrustedIpsFields.map(({ id }, index) => (
/> <div className="mb-3 flex items-end space-x-2" key={id}>
<Controller <Controller
control={control} control={control}
defaultValue="0" name={`accessTokenTrustedIps.${index}.ipAddress`}
name="accessTokenNumUsesLimit" defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<FormControl return (
label="Access Token Max Number of Uses" <FormControl
isError={Boolean(error)} className="mb-0 flex-grow"
errorText={error?.message} label={index === 0 ? "Access Token Trusted IPs" : undefined}
> isError={Boolean(error)}
<Input {...field} placeholder="0" type="number" min="0" step="1" /> errorText={error?.message}
</FormControl> >
)} <Input
/> value={field.value}
{accessTokenTrustedIpsFields.map(({ id }, index) => ( onChange={(e) => {
<div className="mb-3 flex items-end space-x-2" key={id}> if (subscription?.ipAllowlisting) {
<Controller field.onChange(e);
control={control} return;
name={`accessTokenTrustedIps.${index}.ipAddress`} }
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="danger" size="xs"
variant="plain"
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> Add IP Address
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
{identityAuthMethodData?.authMethod && ( <div className="flex justify-between">
<Button <div className="flex items-center">
size="sm" {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
colorSchema="danger" <Button
isLoading={isSubmitting} className="mr-4"
isDisabled={isSubmitting} size="sm"
onClick={() => handlePopUpToggle("revokeAuthMethod", true)} isLoading={isSubmitting}
> isDisabled={isSubmitting}
Remove Auth Method onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
</Button> >
)} Overwrite
</div> </Button>
</form> ) : (
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
const result = await trigger();
if (result) {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
} else {
createNotification({
text: "Please fill in all required fields",
type: "error"
});
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
}
}}
/>
</>
); );
}; };
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityTokenAuth, useAddIdentityTokenAuth,
@@ -13,7 +13,7 @@ import {
useUpdateIdentityTokenAuth useUpdateIdentityTokenAuth
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
@@ -47,12 +47,16 @@ type Props = {
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityTokenAuthForm = ({ export const IdentityTokenAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData identityAuthMethodData,
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -61,7 +65,11 @@ export const IdentityTokenAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? ""); const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod
});
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
@@ -116,164 +124,189 @@ export const IdentityTokenAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${ text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isCurrentAuthMethod ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <>
<Controller <form onSubmit={handleSubmit(onFormSubmit)}>
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenTTL" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="accessTokenTTL"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token TTL (seconds)" <FormControl
isError={Boolean(error)} label="Access Token TTL (seconds)"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenMaxTTL" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="accessTokenMaxTTL"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max TTL (seconds)" <FormControl
isError={Boolean(error)} label="Access Token Max TTL (seconds)"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="0" control={control}
name="accessTokenNumUsesLimit" defaultValue="0"
render={({ field, fieldState: { error } }) => ( name="accessTokenNumUsesLimit"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max Number of Uses" <FormControl
isError={Boolean(error)} label="Access Token Max Number of Uses"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="0" type="number" min="0" step="1" /> >
</FormControl> <Input {...field} placeholder="0" type="number" min="0" step="1" />
)} </FormControl>
/> )}
{accessTokenTrustedIpsFields.map(({ id }, index) => ( />
<div className="mb-3 flex items-end space-x-2" key={id}> {accessTokenTrustedIpsFields.map(({ id }, index) => (
<Controller <div className="mb-3 flex items-end space-x-2" key={id}>
control={control} <Controller
name={`accessTokenTrustedIps.${index}.ipAddress`} control={control}
defaultValue="0.0.0.0/0" name={`accessTokenTrustedIps.${index}.ipAddress`}
render={({ field, fieldState: { error } }) => { defaultValue="0.0.0.0/0"
return ( render={({ field, fieldState: { error } }) => {
<FormControl return (
className="mb-0 flex-grow" <FormControl
label={index === 0 ? "Access Token Trusted IPs" : undefined} className="mb-0 flex-grow"
isError={Boolean(error)} label={index === 0 ? "Access Token Trusted IPs" : undefined}
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input >
value={field.value} <Input
onChange={(e) => { value={field.value}
if (subscription?.ipAllowlisting) { onChange={(e) => {
field.onChange(e); if (subscription?.ipAllowlisting) {
return; field.onChange(e);
} return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="danger" size="xs"
variant="plain"
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> Add IP Address
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
{identityAuthMethodData?.authMethod && ( <div className="flex justify-between">
<Button <div className="flex items-center">
size="sm" {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
colorSchema="danger" <Button
isLoading={isSubmitting} className="mr-4"
isDisabled={isSubmitting} size="sm"
onClick={() => handlePopUpToggle("revokeAuthMethod", true)} isLoading={isSubmitting}
> isDisabled={isSubmitting}
Remove Auth Method onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
</Button> >
)} Overwrite
</div> </Button>
</form> ) : (
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };
@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityUniversalAuth, useAddIdentityUniversalAuth,
@@ -15,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup const schema = yup
.object({ .object({
@@ -70,19 +70,28 @@ type Props = {
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityUniversalAuthForm = ({ export const IdentityUniversalAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData identityAuthMethodData,
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "");
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod;
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod
});
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
@@ -181,9 +190,7 @@ export const IdentityUniversalAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${ text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`,
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success" type: "success"
}); });
@@ -203,216 +210,243 @@ export const IdentityUniversalAuthForm = ({
}; };
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <>
<Controller <form onSubmit={handleSubmit(onFormSubmit)}>
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenTTL" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="accessTokenTTL"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token TTL (seconds)" <FormControl
isError={Boolean(error)} label="Access Token TTL (seconds)"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="2592000" control={control}
name="accessTokenMaxTTL" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="accessTokenMaxTTL"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max TTL (seconds)" <FormControl
isError={Boolean(error)} label="Access Token Max TTL (seconds)"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> >
</FormControl> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
defaultValue="0" control={control}
name="accessTokenNumUsesLimit" defaultValue="0"
render={({ field, fieldState: { error } }) => ( name="accessTokenNumUsesLimit"
<FormControl render={({ field, fieldState: { error } }) => (
label="Access Token Max Number of Uses" <FormControl
isError={Boolean(error)} label="Access Token Max Number of Uses"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="0" type="number" min="0" step="1" /> >
</FormControl> <Input {...field} placeholder="0" type="number" min="0" step="1" />
)} </FormControl>
/> )}
{clientSecretTrustedIpsFields.map(({ id }, index) => ( />
<div className="mb-3 flex items-end space-x-2" key={id}> {clientSecretTrustedIpsFields.map(({ id }, index) => (
<Controller <div className="mb-3 flex items-end space-x-2" key={id}>
control={control} <Controller
name={`clientSecretTrustedIps.${index}.ipAddress`} control={control}
defaultValue="0.0.0.0/0" name={`clientSecretTrustedIps.${index}.ipAddress`}
render={({ field, fieldState: { error } }) => { defaultValue="0.0.0.0/0"
return ( render={({ field, fieldState: { error } }) => {
<FormControl return (
className="mb-0 flex-grow" <FormControl
label={index === 0 ? "Client Secret Trusted IPs" : undefined} className="mb-0 flex-grow"
isError={Boolean(error)} label={index === 0 ? "Client Secret Trusted IPs" : undefined}
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input >
value={field.value} <Input
onChange={(e) => { value={field.value}
if (subscription?.ipAllowlisting) { onChange={(e) => {
field.onChange(e); if (subscription?.ipAllowlisting) {
return; field.onChange(e);
} return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeClientSecretTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeClientSecretTrustedIp(index); appendClientSecretTrustedIp({
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="danger" size="xs"
variant="plain"
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> Add IP Address
</IconButton> </Button>
</div> </div>
))} {accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="my-4 ml-1"> <div className="mb-3 flex items-end space-x-2" key={id}>
<Button <Controller
variant="outline_bg" control={control}
onClick={() => { name={`accessTokenTrustedIps.${index}.ipAddress`}
if (subscription?.ipAllowlisting) { defaultValue="0.0.0.0/0"
appendClientSecretTrustedIp({ render={({ field, fieldState: { error } }) => {
ipAddress: "0.0.0.0/0" return (
}); <FormControl
return; className="mb-0 flex-grow"
} label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} placeholder="123.456.789.0"
size="xs" />
> </FormControl>
Add IP Address );
</Button> }}
</div> />
{accessTokenTrustedIpsFields.map(({ id }, index) => ( <IconButton
<div className="mb-3 flex items-end space-x-2" key={id}> onClick={() => {
<Controller if (subscription?.ipAllowlisting) {
control={control} removeAccessTokenTrustedIp(index);
name={`accessTokenTrustedIps.${index}.ipAddress`} return;
defaultValue="0.0.0.0/0" }
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" size="lg"
/> colorSchema="danger"
</FormControl> variant="plain"
); ariaLabel="update"
}} className="p-3"
/> >
<IconButton <FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="danger" size="xs"
variant="plain"
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> Add IP Address
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
{identityAuthMethodData?.authMethod && ( <div className="flex justify-between">
<Button <div className="flex items-center">
size="sm" {initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
colorSchema="danger" <Button
isLoading={isSubmitting} className="mr-4"
isDisabled={isSubmitting} size="sm"
onClick={() => handlePopUpToggle("revokeAuthMethod", true)} isLoading={isSubmitting}
> isDisabled={isSubmitting}
Remove Auth Method onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
</Button> >
)} Overwrite
</div> </Button>
</form> ) : (
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };