Compare commits

..

2 Commits

Author SHA1 Message Date
b949708f45 docs(sso): fixed azure attributes typo 2025-01-23 05:20:44 +01:00
2a6b6b03b9 docs(guides): updated python guide 2025-01-23 05:20:26 +01:00
37 changed files with 3024 additions and 3743 deletions

View File

@ -5,7 +5,7 @@ title: "Python"
This guide demonstrates how to use Infisical to manage secrets for your Python stack from local development to production. It uses: This guide demonstrates how to use Infisical to manage secrets for your Python stack from local development to production. It uses:
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets. - Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets.
- The [infisical-python](https://pypi.org/project/infisical-python/) Python client SDK to fetch secrets back to your Python application on demand. - The [infisicalsdk](https://pypi.org/project/infisicalsdk/) Python client SDK to fetch secrets back to your Python application on demand.
## Project Setup ## Project Setup
@ -36,40 +36,38 @@ python3 -m venv env
source env/bin/activate source env/bin/activate
``` ```
Install Flask and [infisical-python](https://pypi.org/project/infisical-python/), the client Python SDK for Infisical. Install Flask and [infisicalsdk](https://pypi.org/project/infisicalsdk/), the client Python SDK for Infisical.
```console ```console
pip install flask infisical-python pip install flask infisicalsdk
``` ```
Finally, create an `app.py` file containing the application code. Finally, create an `app.py` file containing the application code.
```py ```py
from flask import Flask from flask import Flask
from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions, AuthenticationOptions, UniversalAuthMethod from infisical_sdk import InfisicalSDKClient
app = Flask(__name__) app = Flask(__name__)
client = InfisicalClient(ClientSettings( client = InfisicalSDKClient(host="https://app.infisical.com") # host is optional, defaults to https://app.infisical.com
auth=AuthenticationOptions(
universal_auth=UniversalAuthMethod( client.auth.universal_auth.login(
client_id="CLIENT_ID", "<machine-identity-client-id>",
client_secret="CLIENT_SECRET", "<machine-identity-client-secret>"
) )
)
))
@app.route("/") @app.route("/")
def hello_world(): def hello_world():
# access value # access value
name = client.secrets.get_secret_by_name(
secret_name="NAME",
project_id="<project-id>",
environment_slug="dev",
secret_path="/"
)
name = client.getSecret(options=GetSecretOptions( return f"Hello! My name is: {name.secretValue}"
environment="dev",
project_id="PROJECT_ID",
secret_name="NAME"
))
return f"Hello! My name is: {name.secret_value}"
``` ```
Here, we initialized a `client` instance of the Infisical Python SDK with the Infisical Token Here, we initialized a `client` instance of the Infisical Python SDK with the Infisical Token
@ -89,15 +87,6 @@ At this stage, you know how to fetch secrets from Infisical back to your Python
## FAQ ## FAQ
<AccordionGroup> <AccordionGroup>
<Accordion title="Isn't it inefficient if my app makes a request every time it needs a secret?">
The client SDK caches every secret and implements a 5-minute waiting period before
re-requesting it. The waiting period can be controlled by setting the `cacheTTL` parameter at
the time of initializing the client.
</Accordion>
<Accordion title="What if a request for a secret fails?">
The SDK caches every secret and falls back to the cached value if a request fails. If no cached
value ever-existed, the SDK falls back to whatever value is on `process.env`.
</Accordion>
<Accordion title="What's the point if I still have to manage a token for the SDK?"> <Accordion title="What's the point if I still have to manage a token for the SDK?">
The token enables the SDK to authenticate with Infisical to fetch back your secrets. The token enables the SDK to authenticate with Infisical to fetch back your secrets.
Although the SDK requires you to pass in a token, it enables greater efficiency and security Although the SDK requires you to pass in a token, it enables greater efficiency and security
@ -114,6 +103,6 @@ At this stage, you know how to fetch secrets from Infisical back to your Python
See also: See also:
- Explore the [Python SDK](https://github.com/Infisical/sdk/tree/main/crates/infisical-py) - Explore the [Python SDK](https://github.com/Infisical/python-sdk-official)

View File

@ -48,7 +48,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
Back in the **Set up Single Sign-On with SAML** screen, select **Edit** in the **Attributes & Claims** section and configure the following map: Back in the **Set up Single Sign-On with SAML** screen, select **Edit** in the **Attributes & Claims** section and configure the following map:
- `email -> user.userprinciplename` - `email -> user.userprincipalname`
- `firstName -> user.givenname` - `firstName -> user.givenname`
- `lastName -> user.surname` - `lastName -> user.surname`

View File

@ -923,7 +923,7 @@ export const useAddIdentityTokenAuth = () => {
}); });
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: identitiesKeys.getIdentityTokenAuth(identityId) queryKey: identitiesKeys.getIdentityUniversalAuth(identityId)
}); });
} }
}); });
@ -959,7 +959,7 @@ export const useUpdateIdentityTokenAuth = () => {
}); });
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: identitiesKeys.getIdentityTokenAuth(identityId) queryKey: identitiesKeys.getIdentityUniversalAuth(identityId)
}); });
} }
}); });

View File

@ -182,7 +182,7 @@ export const queryClient = new QueryClient({
createNotification({ createNotification({
title: "Bad Request", title: "Bad Request",
type: "error", type: "error",
text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`, text: `${serverResponse.message}${serverResponse.message.endsWith(".") ? "" : "."}`,
copyActions: [ copyActions: [
{ {
value: serverResponse.reqId, value: serverResponse.reqId,

View File

@ -7,10 +7,10 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent"; import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent";
type Props = { type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>; popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
}; };
@ -34,7 +34,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
title={ title={
isSelectedAuthAlreadyConfigured isSelectedAuthAlreadyConfigured
? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}` ? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
: `Add ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}` : `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
} }
> >
<IdentityAuthMethodModalContent <IdentityAuthMethodModalContent

View File

@ -4,8 +4,30 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { Badge, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2"; import { createNotification } from "@app/components/notifications";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import {
Badge,
DeleteActionModal,
FormControl,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import {
IdentityAuthMethod,
identityAuthToNameMap,
useDeleteIdentityJwtAuth
} from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
@ -18,10 +40,10 @@ import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = { type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>; popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
@ -34,7 +56,13 @@ type Props = {
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void; setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
}; };
type TRevokeOptions = {
identityId: string;
organizationId: string;
};
type TRevokeMethods = { type TRevokeMethods = {
revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
render: () => JSX.Element; render: () => JSX.Element;
}; };
@ -68,6 +96,18 @@ export const IdentityAuthMethodModalContent = ({
initialAuthMethod, initialAuthMethod,
setSelectedAuthMethod setSelectedAuthMethod
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
const { control, watch } = useForm<FormData>({ const { control, watch } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: async () => { defaultValues: async () => {
@ -109,9 +149,10 @@ export const IdentityAuthMethodModalContent = ({
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = { const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: { [IdentityAuthMethod.UNIVERSAL_AUTH]: {
revokeMethod: revokeUniversalAuth,
render: () => ( render: () => (
<IdentityUniversalAuthForm <IdentityUniversalAuthForm
identityId={identityAuthMethodData.identityId} identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
@ -119,9 +160,10 @@ export const IdentityAuthMethodModalContent = ({
}, },
[IdentityAuthMethod.OIDC_AUTH]: { [IdentityAuthMethod.OIDC_AUTH]: {
revokeMethod: revokeOidcAuth,
render: () => ( render: () => (
<IdentityOidcAuthForm <IdentityOidcAuthForm
identityId={identityAuthMethodData.identityId} identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
@ -129,9 +171,10 @@ export const IdentityAuthMethodModalContent = ({
}, },
[IdentityAuthMethod.TOKEN_AUTH]: { [IdentityAuthMethod.TOKEN_AUTH]: {
revokeMethod: revokeTokenAuth,
render: () => ( render: () => (
<IdentityTokenAuthForm <IdentityTokenAuthForm
identityId={identityAuthMethodData.identityId} identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
@ -139,9 +182,10 @@ export const IdentityAuthMethodModalContent = ({
}, },
[IdentityAuthMethod.AZURE_AUTH]: { [IdentityAuthMethod.AZURE_AUTH]: {
revokeMethod: revokeAzureAuth,
render: () => ( render: () => (
<IdentityAzureAuthForm <IdentityAzureAuthForm
identityId={identityAuthMethodData.identityId} identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
@ -149,9 +193,10 @@ export const IdentityAuthMethodModalContent = ({
}, },
[IdentityAuthMethod.GCP_AUTH]: { [IdentityAuthMethod.GCP_AUTH]: {
revokeMethod: revokeGcpAuth,
render: () => ( render: () => (
<IdentityGcpAuthForm <IdentityGcpAuthForm
identityId={identityAuthMethodData.identityId} identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
@ -159,9 +204,10 @@ export const IdentityAuthMethodModalContent = ({
}, },
[IdentityAuthMethod.KUBERNETES_AUTH]: { [IdentityAuthMethod.KUBERNETES_AUTH]: {
revokeMethod: revokeKubernetesAuth,
render: () => ( render: () => (
<IdentityKubernetesAuthForm <IdentityKubernetesAuthForm
identityId={identityAuthMethodData.identityId} identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
@ -169,9 +215,10 @@ export const IdentityAuthMethodModalContent = ({
}, },
[IdentityAuthMethod.AWS_AUTH]: { [IdentityAuthMethod.AWS_AUTH]: {
revokeMethod: revokeAwsAuth,
render: () => ( render: () => (
<IdentityAwsAuthForm <IdentityAwsAuthForm
identityId={identityAuthMethodData.identityId} identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
@ -179,9 +226,10 @@ export const IdentityAuthMethodModalContent = ({
}, },
[IdentityAuthMethod.JWT_AUTH]: { [IdentityAuthMethod.JWT_AUTH]: {
revokeMethod: revokeJwtAuth,
render: () => ( render: () => (
<IdentityJwtAuthForm <IdentityJwtAuthForm
identityId={identityAuthMethodData.identityId} identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
@ -246,6 +294,42 @@ export const IdentityAuthMethodModalContent = ({
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan." text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/> />
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${
identityAuthMethodData?.authMethod
? identityAuthToNameMap[identityAuthMethodData.authMethod]
: "the auth method"
} on ${identityAuthMethodData?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={async () => {
if (!identityAuthMethodData.authMethod || !orgId || !selectedMethodItem) {
return;
}
try {
await selectedMethodItem.revokeMethod({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
createNotification({
text: "Successfully removed auth method",
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
handlePopUpToggle("identityAuthMethod", false);
} catch {
createNotification({
text: "Failed to remove auth method",
type: "error"
});
}
}}
/>
</> </>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,27 +6,17 @@ 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 { import { Button, FormControl, IconButton, Input } from "@app/components/v2";
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityAwsAuth, useAddIdentityAwsAuth,
useGetIdentityAwsAuth, useGetIdentityAwsAuth,
useUpdateIdentityAwsAuth useUpdateIdentityAwsAuth
} from "@app/hooks/api"; } from "@app/hooks/api";
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 { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z const schema = z
.object({ .object({
stsEndpoint: z.string(), stsEndpoint: z.string(),
@ -59,18 +49,21 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityId?: string; identityAuthMethodData: {
isUpdate?: boolean; identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
}; };
export const IdentityAwsAuthForm = ({ export const IdentityAwsAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityId, identityAuthMethodData
isUpdate
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@ -78,9 +71,11 @@ export const IdentityAwsAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityAwsAuth(identityId ?? "", { const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -148,7 +143,7 @@ export const IdentityAwsAuthForm = ({
accessTokenTrustedIps accessTokenTrustedIps
}: FormData) => { }: FormData) => {
try { try {
if (!identityId) return; if (!identityAuthMethodData) return;
if (data) { if (data) {
await updateMutateAsync({ await updateMutateAsync({
@ -156,7 +151,7 @@ export const IdentityAwsAuthForm = ({
stsEndpoint, stsEndpoint,
allowedPrincipalArns, allowedPrincipalArns,
allowedAccountIds, allowedAccountIds,
identityId, identityId: identityAuthMethodData.identityId,
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -165,7 +160,7 @@ export const IdentityAwsAuthForm = ({
} else { } else {
await addMutateAsync({ await addMutateAsync({
organizationId: orgId, organizationId: orgId,
identityId, identityId: identityAuthMethodData.identityId,
stsEndpoint: stsEndpoint || "", stsEndpoint: stsEndpoint || "",
allowedPrincipalArns: allowedPrincipalArns || "", allowedPrincipalArns: allowedPrincipalArns || "",
allowedAccountIds: allowedAccountIds || "", allowedAccountIds: allowedAccountIds || "",
@ -193,195 +188,190 @@ export const IdentityAwsAuthForm = ({
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => { <Controller
setTabValue( control={control}
["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) defaultValue="2592000"
? IdentityFormTab.Advanced name="allowedPrincipalArns"
: IdentityFormTab.Configuration render={({ field, fieldState: { error } }) => (
); <FormControl
})} label="Allowed Principal ARNs"
> isError={Boolean(error)}
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}> errorText={error?.message}
<TabList> >
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab> <Input
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab> {...field}
</TabList> placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
<TabPanel value={IdentityFormTab.Configuration}> type="text"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="allowedAccountIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Account IDs"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="123456789012, ..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="https://sts.amazonaws.com/"
name="stsEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
</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}
defaultValue="2592000" name={`accessTokenTrustedIps.${index}.ipAddress`}
name="allowedPrincipalArns" defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<FormControl return (
label="Allowed Principal ARNs" <FormControl
isError={Boolean(error)} className="mb-0 flex-grow"
errorText={error?.message} label={index === 0 ? "Access Token Trusted IPs" : undefined}
> isError={Boolean(error)}
<Input errorText={error?.message}
{...field} >
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..." <Input
type="text" value={field.value}
/> onChange={(e) => {
</FormControl> if (subscription?.ipAllowlisting) {
)} field.onChange(e);
/> return;
<Controller }
control={control}
name="allowedAccountIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Account IDs"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="123456789012, ..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="https://sts.amazonaws.com/"
name="stsEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
</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>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{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"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); removeAccessTokenTrustedIp(index);
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
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} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</div> </div>
))} ))}
<div className="my-4 ml-1"> <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>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button <Button
className="mr-4" variant="outline_bg"
size="sm" onClick={() => {
type="submit" if (subscription?.ipAllowlisting) {
isLoading={isSubmitting} appendAccessTokenTrustedIp({
isDisabled={isSubmitting} ipAddress: "0.0.0.0/0"
> });
{isUpdate ? "Update" : "Add"} return;
</Button> }
<Button handlePopUpOpen("upgradePlan");
colorSchema="secondary" }}
variant="plain" leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpToggle("identityAuthMethod", false)} size="xs"
> >
Cancel Add IP Address
</Button> </Button>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form> </form>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,27 +6,17 @@ 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 { import { Button, FormControl, IconButton, Input } from "@app/components/v2";
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityAzureAuth, useAddIdentityAzureAuth,
useGetIdentityAzureAuth, useGetIdentityAzureAuth,
useUpdateIdentityAzureAuth useUpdateIdentityAzureAuth
} from "@app/hooks/api"; } from "@app/hooks/api";
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 { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z const schema = z
.object({ .object({
tenantId: z.string().min(1), tenantId: z.string().min(1),
@ -54,18 +44,21 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityId?: string; identityAuthMethodData: {
isUpdate?: boolean; identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
}; };
export const IdentityAzureAuthForm = ({ export const IdentityAzureAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityId, identityAuthMethodData
isUpdate
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@ -73,9 +66,11 @@ export const IdentityAzureAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityAzureAuth(identityId ?? "", { const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -144,12 +139,12 @@ export const IdentityAzureAuthForm = ({
accessTokenTrustedIps accessTokenTrustedIps
}: FormData) => { }: FormData) => {
try { try {
if (!identityId) return; if (!identityAuthMethodData) return;
if (data) { if (data) {
await updateMutateAsync({ await updateMutateAsync({
organizationId: orgId, organizationId: orgId,
identityId, identityId: identityAuthMethodData.identityId,
tenantId, tenantId,
resource, resource,
allowedServicePrincipalIds, allowedServicePrincipalIds,
@ -161,7 +156,7 @@ export const IdentityAzureAuthForm = ({
} else { } else {
await addMutateAsync({ await addMutateAsync({
organizationId: orgId, organizationId: orgId,
identityId, identityId: identityAuthMethodData.identityId,
tenantId: tenantId || "", tenantId: tenantId || "",
resource: resource || "", resource: resource || "",
allowedServicePrincipalIds: allowedServicePrincipalIds || "", allowedServicePrincipalIds: allowedServicePrincipalIds || "",
@ -189,195 +184,190 @@ export const IdentityAzureAuthForm = ({
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => { <Controller
setTabValue( control={control}
["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) defaultValue="2592000"
? IdentityFormTab.Advanced name="tenantId"
: IdentityFormTab.Configuration render={({ field, fieldState: { error } }) => (
); <FormControl
})} label="Tenant ID"
> isError={Boolean(error)}
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}> errorText={error?.message}
<TabList> isRequired
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab> >
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab> <Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
</TabList> </FormControl>
<TabPanel value={IdentityFormTab.Configuration}> )}
/>
<Controller
control={control}
name="resource"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Resource / Audience"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="https://management.azure.com/" />
</FormControl>
)}
/>
<Controller
control={control}
name="allowedServicePrincipalIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Principal IDs"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
</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}
defaultValue="2592000" name={`accessTokenTrustedIps.${index}.ipAddress`}
name="tenantId" defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<FormControl return (
label="Tenant ID" <FormControl
isError={Boolean(error)} className="mb-0 flex-grow"
errorText={error?.message} label={index === 0 ? "Access Token Trusted IPs" : undefined}
isRequired isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" /> >
</FormControl> <Input
)} value={field.value}
/> onChange={(e) => {
<Controller if (subscription?.ipAllowlisting) {
control={control} field.onChange(e);
name="resource" return;
render={({ field, fieldState: { error } }) => ( }
<FormControl
label="Resource / Audience"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="https://management.azure.com/" />
</FormControl>
)}
/>
<Controller
control={control}
name="allowedServicePrincipalIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Principal IDs"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
</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>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{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"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); removeAccessTokenTrustedIp(index);
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
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} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</div> </div>
))} ))}
<div className="my-4 ml-1"> <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>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button <Button
className="mr-4" variant="outline_bg"
size="sm" onClick={() => {
type="submit" if (subscription?.ipAllowlisting) {
isLoading={isSubmitting} appendAccessTokenTrustedIp({
isDisabled={isSubmitting} ipAddress: "0.0.0.0/0"
> });
{isUpdate ? "Update" : "Add"} return;
</Button> }
<Button handlePopUpOpen("upgradePlan");
colorSchema="secondary" }}
variant="plain" leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpToggle("identityAuthMethod", false)} size="xs"
> >
Cancel Add IP Address
</Button> </Button>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form> </form>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,29 +6,17 @@ 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 { import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityGcpAuth, useAddIdentityGcpAuth,
useGetIdentityGcpAuth, useGetIdentityGcpAuth,
useUpdateIdentityGcpAuth useUpdateIdentityGcpAuth
} from "@app/hooks/api"; } from "@app/hooks/api";
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 { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z const schema = z
.object({ .object({
type: z.enum(["iam", "gce"]), type: z.enum(["iam", "gce"]),
@ -57,18 +45,21 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityId?: string; identityAuthMethodData: {
isUpdate?: boolean; identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
}; };
export const IdentityGcpAuthForm = ({ export const IdentityGcpAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityId, identityAuthMethodData
isUpdate
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@ -76,9 +67,11 @@ export const IdentityGcpAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityGcpAuth(identityId ?? "", { const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -153,11 +146,11 @@ export const IdentityGcpAuthForm = ({
accessTokenTrustedIps accessTokenTrustedIps
}: FormData) => { }: FormData) => {
try { try {
if (!identityId) return; if (!identityAuthMethodData) return;
if (data) { if (data) {
await updateMutateAsync({ await updateMutateAsync({
identityId, identityId: identityAuthMethodData.identityId,
organizationId: orgId, organizationId: orgId,
type, type,
allowedServiceAccounts, allowedServiceAccounts,
@ -170,7 +163,7 @@ export const IdentityGcpAuthForm = ({
}); });
} else { } else {
await addMutateAsync({ await addMutateAsync({
identityId, identityId: identityAuthMethodData.identityId,
organizationId: orgId, organizationId: orgId,
type, type,
allowedServiceAccounts: allowedServiceAccounts || "", allowedServiceAccounts: allowedServiceAccounts || "",
@ -200,223 +193,214 @@ export const IdentityGcpAuthForm = ({
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => { <Controller
setTabValue( control={control}
["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) name="type"
? IdentityFormTab.Advanced render={({ field: { onChange, ...field }, fieldState: { error } }) => (
: IdentityFormTab.Configuration <FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
); <Select
})} defaultValue={field.value}
> {...field}
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}> onValueChange={(e) => onChange(e)}
<TabList> className="w-full"
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
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="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 } }) => (
<FormControl
label="Allowed Service Account Emails"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
type="text"
/>
</FormControl>
)}
/>
{watchedType === "gce" && (
<Controller
control={control}
name="allowedProjects"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Projects"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="my-gcp-project, ..." />
</FormControl>
)}
/>
)}
{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>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{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={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
Add IP Address <SelectItem value="gce" key="gce">
</Button> GCP ID Token Auth (Recommended)
</div> </SelectItem>
</TabPanel> <SelectItem value="iam" key="iam">
</Tabs> GCP IAM Auth
<div className="flex items-center"> </SelectItem>
<Button </Select>
className="mr-4" </FormControl>
size="sm" )}
type="submit" />
isLoading={isSubmitting} <Controller
isDisabled={isSubmitting} control={control}
> defaultValue="2592000"
{isUpdate ? "Update" : "Add"} name="allowedServiceAccounts"
</Button> render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Account Emails"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
type="text"
/>
</FormControl>
)}
/>
{watchedType === "gce" && (
<Controller
control={control}
name="allowedProjects"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Projects"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="my-gcp-project, ..." />
</FormControl>
)}
/>
)}
{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 <Button
colorSchema="secondary" variant="outline_bg"
variant="plain" onClick={() => {
onClick={() => handlePopUpToggle("identityAuthMethod", false)} if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
Cancel Add IP Address
</Button> </Button>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form> </form>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
@ -14,22 +14,17 @@ import {
Input, Input,
Select, Select,
SelectItem, SelectItem,
Tab,
TabList,
TabPanel,
Tabs,
TextArea, TextArea,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api"; import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums"; import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries"; import { useGetIdentityJwtAuth } 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 { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const commonSchema = z.object({ const commonSchema = z.object({
accessTokenTrustedIps: z accessTokenTrustedIps: z
.array( .array(
@ -90,18 +85,21 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityId?: string; identityAuthMethodData: {
isUpdate?: boolean; identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
}; };
export const IdentityJwtAuthForm = ({ export const IdentityJwtAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityId, identityAuthMethodData
isUpdate
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@ -109,9 +107,11 @@ export const IdentityJwtAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityJwtAuth(identityId ?? "", { const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityJwtAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -218,13 +218,13 @@ export const IdentityJwtAuthForm = ({
boundSubject boundSubject
}: FormData) => { }: FormData) => {
try { try {
if (!identityId) { if (!identityAuthMethodData) {
return; return;
} }
if (data) { if (data) {
await updateMutateAsync({ await updateMutateAsync({
identityId, identityId: identityAuthMethodData.identityId,
organizationId: orgId, organizationId: orgId,
configurationType, configurationType,
jwksUrl, jwksUrl,
@ -241,7 +241,7 @@ export const IdentityJwtAuthForm = ({
}); });
} else { } else {
await addMutateAsync({ await addMutateAsync({
identityId, identityId: identityAuthMethodData.identityId,
configurationType, configurationType,
jwksUrl, jwksUrl,
jwksCaCert, jwksCaCert,
@ -275,179 +275,56 @@ export const IdentityJwtAuthForm = ({
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => { <Controller
setTabValue( control={control}
["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) name="configurationType"
? IdentityFormTab.Advanced render={({ field: { onChange, ...field }, fieldState: { error } }) => (
: IdentityFormTab.Configuration <FormControl
); label="Configuration Type"
})} isError={Boolean(error)}
> errorText={error?.message}
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}> >
<TabList> <Select
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab> defaultValue={field.value}
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab> {...field}
</TabList> onValueChange={(e) => {
<TabPanel value={IdentityFormTab.Configuration}> if (e === IdentityJwtConfigurationType.JWKS) {
<Controller setValue("publicKeys", []);
control={control} } else {
name="configurationType" setValue("publicKeys", [
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( {
<FormControl
label="Configuration Type"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
if (e === IdentityJwtConfigurationType.JWKS) {
setValue("publicKeys", []);
} else {
setValue("publicKeys", [
{
value: ""
}
]);
setValue("jwksUrl", "");
setValue("jwksCaCert", "");
}
onChange(e);
}}
className="w-full"
>
<SelectItem value={IdentityJwtConfigurationType.JWKS} key="jwks">
JWKS
</SelectItem>
<SelectItem value={IdentityJwtConfigurationType.STATIC} key="static">
Static
</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedConfigurationType === IdentityJwtConfigurationType.JWKS && (
<>
<Controller
control={control}
name="jwksUrl"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="JWKS URL"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="jwksCaCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="JWKS CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
</>
)}
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
<>
{publicKeyFields.map(({ id }, index) => (
<div key={id} className="flex gap-2">
<Controller
control={control}
name={`publicKeys.${index}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-grow"
label={`Public Key ${index + 1}`}
errorText={error?.message}
isError={Boolean(error)}
icon={
<Tooltip
className="text-center"
content={<span>This field only accepts PEM-formatted public keys</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<TextArea {...field} placeholder="-----BEGIN PUBLIC KEY----- ..." />
</FormControl>
)}
/>
<IconButton
onClick={() => {
if (publicKeyFields.length === 1) {
createNotification({
type: "error",
text: "A public key is required for static configurations"
});
return;
}
removePublicKeyFields(index);
}}
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={() =>
appendPublicKeyFields({
value: "" value: ""
}) }
} ]);
leftIcon={<FontAwesomeIcon icon={faPlus} />} setValue("jwksUrl", "");
size="xs" setValue("jwksCaCert", "");
> }
Add Public Key onChange(e);
</Button> }}
</div> className="w-full"
</> >
)} <SelectItem value={IdentityJwtConfigurationType.JWKS} key="jwks">
JWKS
</SelectItem>
<SelectItem value={IdentityJwtConfigurationType.STATIC} key="static">
Static
</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedConfigurationType === IdentityJwtConfigurationType.JWKS && (
<>
<Controller <Controller
control={control} control={control}
name="boundIssuer" name="jwksUrl"
render={({ field, fieldState: { error } }) => (
<FormControl label="Issuer" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="boundSubject"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Subject" isRequired
label="JWKS URL"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} 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" /> <Input {...field} type="text" />
</FormControl> </FormControl>
@ -455,79 +332,58 @@ export const IdentityJwtAuthForm = ({
/> />
<Controller <Controller
control={control} control={control}
name="boundAudiences" name="jwksCaCert"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Audiences" label="JWKS CA Certificate"
isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
icon={ isError={Boolean(error)}
<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" /> <TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</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>
);
}}
/>
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
<>
{publicKeyFields.map(({ id }, index) => (
<div key={id} className="flex gap-2">
<Controller
control={control}
name={`publicKeys.${index}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-grow"
label={`Public Key ${index + 1}`}
errorText={error?.message}
isError={Boolean(error)}
icon={
<Tooltip
className="text-center"
content={<span>This field only accepts PEM-formatted public keys</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<TextArea {...field} placeholder="-----BEGIN PUBLIC KEY----- ..." />
</FormControl>
)}
/>
<IconButton <IconButton
onClick={() => removeBoundClaimField(index)} onClick={() => {
if (publicKeyFields.length === 1) {
createNotification({
type: "error",
text: "A public key is required for static configurations"
});
return;
}
removePublicKeyFields(index);
}}
size="lg" size="lg"
colorSchema="danger" colorSchema="danger"
variant="plain" variant="plain"
@ -542,150 +398,291 @@ export const IdentityJwtAuthForm = ({
<Button <Button
variant="outline_bg" variant="outline_bg"
onClick={() => onClick={() =>
appendBoundClaimField({ appendPublicKeyFields({
key: "",
value: "" value: ""
}) })
} }
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs" size="xs"
> >
Add Claims Add Public Key
</Button> </Button>
</div> </div>
<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>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{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"); <Controller
}} control={control}
placeholder="123.456.789.0" name="boundIssuer"
/> render={({ field, fieldState: { error } }) => (
</FormControl> <FormControl label="Issuer" isError={Boolean(error)} errorText={error?.message}>
); <Input {...field} type="text" />
}} </FormControl>
/> )}
<IconButton />
onClick={() => { <Controller
if (subscription?.ipAllowlisting) { control={control}
removeAccessTokenTrustedIp(index); name="boundSubject"
return; render={({ field, fieldState: { error } }) => (
<FormControl
label="Subject"
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" />
</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
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>
);
}}
/>
handlePopUpOpen("upgradePlan"); <IconButton
}} 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} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</div> </div>
))} ))}
<div className="my-4 ml-1"> <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>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button <Button
className="mr-4" variant="outline_bg"
size="sm" onClick={() =>
type="submit" appendBoundClaimField({
isLoading={isSubmitting} key: "",
isDisabled={isSubmitting} value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
{isUpdate ? "Update" : "Create"} Add Claims
</Button> </Button>
</div>
<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 <Button
colorSchema="secondary" variant="outline_bg"
variant="plain" onClick={() => {
onClick={() => handlePopUpToggle("identityAuthMethod", false)} if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
Cancel Add IP Address
</Button> </Button>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form> </form>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,28 +6,17 @@ 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 { import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs,
TextArea
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityKubernetesAuth, useAddIdentityKubernetesAuth,
useGetIdentityKubernetesAuth, useGetIdentityKubernetesAuth,
useUpdateIdentityKubernetesAuth useUpdateIdentityKubernetesAuth
} from "@app/hooks/api"; } from "@app/hooks/api";
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 { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z const schema = z
.object({ .object({
kubernetesHost: z.string().min(1), kubernetesHost: z.string().min(1),
@ -58,18 +47,21 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityId?: string; identityAuthMethodData: {
isUpdate?: boolean; identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
}; };
export const IdentityKubernetesAuthForm = ({ export const IdentityKubernetesAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityId, identityAuthMethodData
isUpdate
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@ -77,9 +69,11 @@ export const IdentityKubernetesAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityKubernetesAuth(identityId ?? "", { const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -160,7 +154,7 @@ export const IdentityKubernetesAuthForm = ({
accessTokenTrustedIps accessTokenTrustedIps
}: FormData) => { }: FormData) => {
try { try {
if (!identityId) return; if (!identityAuthMethodData) return;
if (data) { if (data) {
await updateMutateAsync({ await updateMutateAsync({
@ -171,7 +165,7 @@ export const IdentityKubernetesAuthForm = ({
allowedNamespaces, allowedNamespaces,
allowedAudience, allowedAudience,
caCert, caCert,
identityId, identityId: identityAuthMethodData.identityId,
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -180,7 +174,7 @@ export const IdentityKubernetesAuthForm = ({
} else { } else {
await addMutateAsync({ await addMutateAsync({
organizationId: orgId, organizationId: orgId,
identityId, identityId: identityAuthMethodData.identityId,
kubernetesHost: kubernetesHost || "", kubernetesHost: kubernetesHost || "",
tokenReviewerJwt, tokenReviewerJwt,
allowedNames: allowedNames || "", allowedNames: allowedNames || "",
@ -211,255 +205,242 @@ export const IdentityKubernetesAuthForm = ({
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => { <Controller
setTabValue( control={control}
[ defaultValue="2592000"
"kubernetesHost", name="kubernetesHost"
"tokenReviewerJwt", render={({ field, fieldState: { error } }) => (
"accessTokenTTL", <FormControl
"accessTokenMaxTTL", label="Kubernetes Host / Base Kubernetes API URL "
"accessTokenNumUsesLimit", isError={Boolean(error)}
"allowedNames", errorText={error?.message}
"allowedNamespaces" 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'"
].includes(Object.keys(fields)[0]) isRequired
? IdentityFormTab.Configuration >
: IdentityFormTab.Advanced <Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
); </FormControl>
})} )}
> />
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}> <Controller
<TabList> control={control}
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab> name="tokenReviewerJwt"
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab> render={({ field, fieldState: { error } }) => (
</TabList> <FormControl
<TabPanel value={IdentityFormTab.Configuration}> 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="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
>
<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" />
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
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."
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<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 <Controller
control={control} control={control}
defaultValue="2592000" name={`accessTokenTrustedIps.${index}.ipAddress`}
name="kubernetesHost" defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<FormControl return (
label="Kubernetes Host / Base Kubernetes API URL " <FormControl
isError={Boolean(error)} className="mb-0 flex-grow"
errorText={error?.message} label={index === 0 ? "Access Token Trusted IPs" : undefined}
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'" isError={Boolean(error)}
isRequired 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 {...field} placeholder="https://my-example-k8s-api-host.com" type="text" /> >
</FormControl> <Input
)} value={field.value}
/> onChange={(e) => {
<Controller if (subscription?.ipAllowlisting) {
control={control} field.onChange(e);
name="tokenReviewerJwt" return;
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}
defaultValue=""
name="allowedNamespaces"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Namespaces"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
>
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
</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="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<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>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
<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" />
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
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."
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</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={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); removeAccessTokenTrustedIp(index);
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
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} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</div> </div>
))} ))}
<div className="my-4 ml-1"> <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>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button <Button
className="mr-4" variant="outline_bg"
size="sm" onClick={() => {
type="submit" if (subscription?.ipAllowlisting) {
isLoading={isSubmitting} appendAccessTokenTrustedIp({
isDisabled={isSubmitting} ipAddress: "0.0.0.0/0"
> });
{isUpdate ? "Update" : "Add"} return;
</Button> }
<Button handlePopUpOpen("upgradePlan");
colorSchema="secondary" }}
variant="plain" leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpToggle("identityAuthMethod", false)} size="xs"
> >
Cancel Add IP Address
</Button> </Button>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form> </form>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
@ -7,26 +7,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 { import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2";
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs,
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 { 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 { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z.object({ const schema = z.object({
accessTokenTrustedIps: z accessTokenTrustedIps: z
.array( .array(
@ -60,18 +48,21 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityId?: string; identityAuthMethodData: {
isUpdate?: boolean; identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
}; };
export const IdentityOidcAuthForm = ({ export const IdentityOidcAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityId, identityAuthMethodData
isUpdate
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@ -79,9 +70,11 @@ export const IdentityOidcAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityOidcAuth(identityId ?? "", { const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -167,13 +160,13 @@ export const IdentityOidcAuthForm = ({
boundSubject boundSubject
}: FormData) => { }: FormData) => {
try { try {
if (!identityId) { if (!identityAuthMethodData) {
return; return;
} }
if (data) { if (data) {
await updateMutateAsync({ await updateMutateAsync({
identityId, identityId: identityAuthMethodData.identityId,
organizationId: orgId, organizationId: orgId,
oidcDiscoveryUrl, oidcDiscoveryUrl,
caCert, caCert,
@ -188,7 +181,7 @@ export const IdentityOidcAuthForm = ({
}); });
} else { } else {
await addMutateAsync({ await addMutateAsync({
identityId, identityId: identityAuthMethodData.identityId,
oidcDiscoveryUrl, oidcDiscoveryUrl,
caCert, caCert,
boundIssuer, boundIssuer,
@ -220,324 +213,315 @@ export const IdentityOidcAuthForm = ({
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => { <Controller
setTabValue( control={control}
["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0]) name="oidcDiscoveryUrl"
? IdentityFormTab.Advanced render={({ field, fieldState: { error } }) => (
: IdentityFormTab.Configuration <FormControl
); isRequired
})} label="OIDC Discovery URL"
> isError={Boolean(error)}
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}> errorText={error?.message}
<TabList> >
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab> <Input
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab> {...field}
</TabList> placeholder="https://token.actions.githubusercontent.com"
<TabPanel value={IdentityFormTab.Configuration}> type="text"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="boundIssuer"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Issuer"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
type="text"
placeholder="https://token.actions.githubusercontent.com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="boundSubject"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Subject"
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" />
</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 <Controller
control={control} control={control}
name="oidcDiscoveryUrl" name={`boundClaims.${index}.key`}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<FormControl return (
isRequired <FormControl
label="OIDC Discovery URL" className="mb-0 flex-grow"
isError={Boolean(error)} label={index === 0 ? "Claims" : undefined}
errorText={error?.message} icon={
> index === 0 ? (
<Input <Tooltip
{...field} className="text-center"
placeholder="https://token.actions.githubusercontent.com" content={<span>This field supports glob patterns</span>}
type="text" >
/> <FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</FormControl> </Tooltip>
)} ) : undefined
/>
<Controller
control={control}
name="boundIssuer"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Issuer"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
type="text"
placeholder="https://token.actions.githubusercontent.com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="boundSubject"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Subject"
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" />
</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>
)}
/>
<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>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</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
onClick={() => removeBoundClaimField(index)}
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={() =>
appendBoundClaimField({
key: "",
value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add Claims
</Button>
</div>
{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;
} }
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>
);
}}
/>
handlePopUpOpen("upgradePlan"); <IconButton
}} 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} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</div> </div>
))} ))}
<div className="my-4 ml-1"> <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>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button <Button
className="mr-4" variant="outline_bg"
size="sm" onClick={() =>
type="submit" appendBoundClaimField({
isLoading={isSubmitting} key: "",
isDisabled={isSubmitting} value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
{isUpdate ? "Update" : "Add"} Add Claims
</Button> </Button>
</div>
<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 <Button
colorSchema="secondary" variant="outline_bg"
variant="plain" onClick={() => {
onClick={() => handlePopUpToggle("identityAuthMethod", false)} if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
Cancel Add IP Address
</Button> </Button>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form> </form>
); );
}; };

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,27 +5,16 @@ 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 { import { Button, FormControl, IconButton, Input } from "@app/components/v2";
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityTokenAuth, useAddIdentityTokenAuth,
useGetIdentityTokenAuth, useGetIdentityTokenAuth,
useUpdateIdentityTokenAuth useUpdateIdentityTokenAuth
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z const schema = z
.object({ .object({
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, { accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
@ -51,18 +39,21 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityId?: string; identityAuthMethodData?: {
isUpdate?: boolean; identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
}; };
export const IdentityTokenAuthForm = ({ export const IdentityTokenAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityId, identityAuthMethodData
isUpdate
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@ -70,9 +61,12 @@ export const IdentityTokenAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityTokenAuth(identityId ?? "", { const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -97,30 +91,6 @@ export const IdentityTokenAuthForm = ({
remove: removeAccessTokenTrustedIp remove: removeAccessTokenTrustedIp
} = useFieldArray({ control, name: "accessTokenTrustedIps" }); } = useFieldArray({ control, name: "accessTokenTrustedIps" });
useEffect(() => {
if (data) {
reset({
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
({ ipAddress, prefix }: IdentityTrustedIp) => {
return {
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
};
}
)
});
} else {
reset({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
}
}, [data]);
const onFormSubmit = async ({ const onFormSubmit = async ({
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
@ -128,12 +98,12 @@ export const IdentityTokenAuthForm = ({
accessTokenTrustedIps accessTokenTrustedIps
}: FormData) => { }: FormData) => {
try { try {
if (!identityId) return; if (!identityAuthMethodData) return;
if (data) { if (data) {
await updateMutateAsync({ await updateMutateAsync({
organizationId: orgId, organizationId: orgId,
identityId, identityId: identityAuthMethodData.identityId,
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -142,7 +112,7 @@ export const IdentityTokenAuthForm = ({
} else { } else {
await addMutateAsync({ await addMutateAsync({
organizationId: orgId, organizationId: orgId,
identityId, identityId: identityAuthMethodData.identityId,
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -167,154 +137,149 @@ export const IdentityTokenAuthForm = ({
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => { <Controller
setTabValue( control={control}
["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) defaultValue="2592000"
? IdentityFormTab.Advanced name="accessTokenTTL"
: IdentityFormTab.Configuration render={({ field, fieldState: { error } }) => (
); <FormControl
})} label="Access Token TTL (seconds)"
> isError={Boolean(error)}
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}> errorText={error?.message}
<TabList> >
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab> </FormControl>
</TabList> )}
<TabPanel value={IdentityFormTab.Configuration}> />
<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}
defaultValue="2592000" name={`accessTokenTrustedIps.${index}.ipAddress`}
name="accessTokenTTL" defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<FormControl return (
label="Access Token TTL (seconds)" <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="2592000" type="number" min="1" step="1" /> errorText={error?.message}
</FormControl> >
)} <Input
/> value={field.value}
<Controller onChange={(e) => {
control={control} if (subscription?.ipAllowlisting) {
defaultValue="2592000" field.onChange(e);
name="accessTokenMaxTTL" return;
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>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{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"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <IconButton
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index); removeAccessTokenTrustedIp(index);
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
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} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</div> </div>
))} ))}
<div className="my-4 ml-1"> <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>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button <Button
className="mr-4" variant="outline_bg"
size="sm" onClick={() => {
type="submit" if (subscription?.ipAllowlisting) {
isLoading={isSubmitting} appendAccessTokenTrustedIp({
isDisabled={isSubmitting} ipAddress: "0.0.0.0/0"
> });
{isUpdate ? "Update" : "Add"} return;
</Button> }
<Button handlePopUpOpen("upgradePlan");
colorSchema="secondary" }}
variant="plain" leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpToggle("identityAuthMethod", false)} size="xs"
> >
Cancel Add IP Address
</Button> </Button>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form> </form>
); );
}; };

View File

@ -0,0 +1,344 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
// DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import {
useCreateIdentityUniversalAuthClientSecret,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
description: z.string(),
ttl: z
.string()
.refine((value) => Number(value) <= 315360000, "TTL cannot be greater than 315360000"),
numUsesLimit: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["revokeClientSecret"]>,
data?: {
clientSecretPrefix: string;
clientSecretId: string;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>,
state?: boolean
) => void;
};
export const IdentityUniversalAuthClientSecretModal = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: Props) => {
const { t } = useTranslation();
const [token, setToken] = useState("");
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
const popUpData = popUp?.universalAuthClientSecret?.data as {
identityId?: string;
name?: string;
};
const { data, isPending } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? "");
const { mutateAsync: createClientSecretMutateAsync } =
useCreateIdentityUniversalAuthClientSecret();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
description: "",
ttl: "",
numUsesLimit: ""
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientSecretCopied) {
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientSecretCopied]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientIdCopied) {
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientIdCopied]);
const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => {
try {
if (!popUpData?.identityId) return;
const { clientSecret } = await createClientSecretMutateAsync({
identityId: popUpData.identityId,
description,
ttl: Number(ttl),
numUsesLimit: Number(numUsesLimit)
});
setToken(clientSecret);
createNotification({
text: "Successfully created client secret",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create client secret",
type: "error"
});
}
};
const hasToken = Boolean(token);
return (
<Modal
isOpen={popUp?.universalAuthClientSecret?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("universalAuthClientSecret", isOpen);
reset();
setToken("");
}}
>
<ModalContent title={`Manage Client ID/Secrets for ${popUpData?.name ?? ""}`}>
<h2 className="mb-4">Client ID</h2>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{identityUniversalAuth?.clientId ?? ""}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? "");
setIsClientIdCopied.on();
}}
>
<FontAwesomeIcon icon={isClientIdCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
<h2 className="mb-4">New Client Secret</h2>
{hasToken ? (
<div>
<div className="mb-4 flex items-center justify-between">
<p>We will only show this secret once</p>
<Button
colorSchema="secondary"
type="submit"
onClick={() => {
reset();
setToken("");
}}
>
Got it
</Button>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{token}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(token);
setIsClientSecretCopied.on();
}}
>
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
</div>
) : (
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Description (optional)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Description" />
</FormControl>
)}
/>
<div className="flex">
<Controller
control={control}
defaultValue=""
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL (seconds - optional)"
isError={Boolean(error)}
errorText={error?.message}
>
<div className="flex">
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</div>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="numUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
className="ml-4"
>
<div className="flex">
<Input {...field} placeholder="0" type="number" min="0" step="1" />
<Button
className="ml-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</div>
</FormControl>
)}
/>
</div>
</form>
)}
<h2 className="mb-4">Client Secrets</h2>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Description</Th>
<Th>Num Uses</Th>
<Th>Expires At</Th>
<Th>Client Secret</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={5} innerKey="org-identities-client-secrets" />}
{!isPending &&
data &&
data.length > 0 &&
data.map(
({
id,
description,
clientSecretTTL,
clientSecretPrefix,
clientSecretNumUses,
clientSecretNumUsesLimit,
createdAt
}) => {
let expiresAt;
if (clientSecretTTL > 0) {
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
}
return (
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
<Td>{description === "" ? "-" : description}</Td>
<Td>{`${clientSecretNumUses}${
clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""
}`}</Td>
<Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}</Td>
<Td>{`${clientSecretPrefix}****`}</Td>
<Td>
<IconButton
onClick={() => {
handlePopUpOpen("revokeClientSecret", {
clientSecretPrefix,
clientSecretId: id
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
}
)}
{!isPending && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No client secrets have been created for this identity yet"
icon={faKey}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
);
};

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,27 +6,17 @@ 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 { import { Button, FormControl, IconButton, Input } from "@app/components/v2";
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityUniversalAuth, useAddIdentityUniversalAuth,
useGetIdentityUniversalAuth, useGetIdentityUniversalAuth,
useUpdateIdentityUniversalAuth useUpdateIdentityUniversalAuth
} from "@app/hooks/api"; } from "@app/hooks/api";
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 { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z const schema = z
.object({ .object({
accessTokenTTL: z accessTokenTTL: z
@ -62,27 +52,33 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityId?: string; identityAuthMethodData?: {
isUpdate?: boolean; identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
}; };
export const IdentityUniversalAuthForm = ({ export const IdentityUniversalAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityId, identityAuthMethodData
isUpdate
}: 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 [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data } = useGetIdentityUniversalAuth(identityId ?? "", { const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -153,13 +149,13 @@ export const IdentityUniversalAuthForm = ({
accessTokenTrustedIps accessTokenTrustedIps
}: FormData) => { }: FormData) => {
try { try {
if (!identityId) return; if (!identityAuthMethodData) return;
if (data) { if (data) {
// update universal auth configuration // update universal auth configuration
await updateMutateAsync({ await updateMutateAsync({
organizationId: orgId, organizationId: orgId,
identityId, identityId: identityAuthMethodData.identityId,
clientSecretTrustedIps, clientSecretTrustedIps,
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL),
@ -171,7 +167,7 @@ export const IdentityUniversalAuthForm = ({
await addMutateAsync({ await addMutateAsync({
organizationId: orgId, organizationId: orgId,
identityId, identityId: identityAuthMethodData.identityId,
clientSecretTrustedIps, clientSecretTrustedIps,
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL),
@ -199,221 +195,216 @@ export const IdentityUniversalAuthForm = ({
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => { <Controller
setTabValue( control={control}
["accessTokenTrustedIps", "clientSecretTrustedIps"].includes(Object.keys(fields)[0]) defaultValue="2592000"
? IdentityFormTab.Advanced name="accessTokenTTL"
: IdentityFormTab.Configuration render={({ field, fieldState: { error } }) => (
); <FormControl
})} label="Access Token TTL (seconds)"
> isError={Boolean(error)}
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}> errorText={error?.message}
<TabList> >
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab> </FormControl>
</TabList> )}
<TabPanel value={IdentityFormTab.Configuration}> />
<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="0" 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>
)}
/>
{clientSecretTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller <Controller
control={control} control={control}
defaultValue="2592000" name={`clientSecretTrustedIps.${index}.ipAddress`}
name="accessTokenTTL" defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => {
<FormControl return (
label="Access Token TTL (seconds)" <FormControl
isError={Boolean(error)} className="mb-0 flex-grow"
errorText={error?.message} label={index === 0 ? "Client Secret Trusted IPs" : undefined}
> isError={Boolean(error)}
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> 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>
);
}}
/> />
<Controller <IconButton
control={control} onClick={() => {
defaultValue="2592000" if (subscription?.ipAllowlisting) {
name="accessTokenMaxTTL" removeClientSecretTrustedIp(index);
render={({ field, fieldState: { error } }) => ( return;
<FormControl }
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" 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>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{clientSecretTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`clientSecretTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Client Secret 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} />
onClick={() => { </IconButton>
if (subscription?.ipAllowlisting) { </div>
removeClientSecretTrustedIp(index); ))}
return; <div className="my-4 ml-1">
}
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={() => {
if (subscription?.ipAllowlisting) {
appendClientSecretTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
{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={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button <Button
className="mr-4" variant="outline_bg"
size="sm" onClick={() => {
type="submit" if (subscription?.ipAllowlisting) {
isLoading={isSubmitting} appendClientSecretTrustedIp({
isDisabled={isSubmitting} ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
{isUpdate ? "Update" : "Add"} Add IP Address
</Button> </Button>
</div>
{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 <Button
colorSchema="secondary" variant="outline_bg"
variant="plain" onClick={() => {
onClick={() => handlePopUpToggle("identityAuthMethod", false)} if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
Cancel Add IP Address
</Button> </Button>
</div> </div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Edit" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Delete Auth Method
</Button>
)}
</div>
</form> </form>
); );
}; };

View File

@ -1,4 +0,0 @@
export enum IdentityFormTab {
Advanced = "advanced",
Configuration = "configuration"
}

View File

@ -1,24 +1,45 @@
import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, PageHeader } from "@app/components/v2"; import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api"; import {
IdentityAuthMethod,
useDeleteIdentity,
useGetIdentityById,
useRevokeIdentityTokenAuthToken,
useRevokeIdentityUniversalAuthClientSecret
} from "@app/hooks/api";
import { Identity } from "@app/hooks/api/identities/types";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal";
import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgAccessControlTabSections } from "@app/types/org";
import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
import { IdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal"; import { IdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
import { IdentityUniversalAuthClientSecretModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal";
import { import {
IdentityAuthenticationSection, IdentityAuthenticationSection,
IdentityClientSecretModal,
IdentityDetailsSection, IdentityDetailsSection,
IdentityProjectsSection IdentityProjectsSection,
IdentityTokenListModal,
IdentityTokenModal
} from "./components"; } from "./components";
const Page = () => { const Page = () => {
@ -31,13 +52,25 @@ const Page = () => {
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { data } = useGetIdentityById(identityId); const { data } = useGetIdentityById(identityId);
const { mutateAsync: deleteIdentity } = useDeleteIdentity(); const { mutateAsync: deleteIdentity } = useDeleteIdentity();
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
const [selectedAuthMethod, setSelectedAuthMethod] = useState<
Identity["authMethods"][number] | null
>(null);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"identity", "identity",
"deleteIdentity", "deleteIdentity",
"identityAuthMethod", "identityAuthMethod",
"upgradePlan", "revokeAuthMethod",
"viewAuthMethod" "token",
"tokenList",
"revokeToken",
"clientSecret",
"revokeClientSecret",
"universalAuthClientSecret", // list of client secrets
"upgradePlan"
] as const); ] as const);
const onDeleteIdentitySubmit = async (id: string) => { const onDeleteIdentitySubmit = async (id: string) => {
@ -71,15 +104,148 @@ const Page = () => {
} }
}; };
const onRevokeTokenSubmit = async ({
identityId: parentIdentityId,
tokenId,
name
}: {
identityId: string;
tokenId: string;
name: string;
}) => {
try {
await revokeToken({
identityId: parentIdentityId,
tokenId
});
handlePopUpClose("revokeToken");
createNotification({
text: `Successfully revoked token ${name ?? ""}`,
type: "success"
});
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete identity";
createNotification({
text,
type: "error"
});
}
};
const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => {
try {
if (!data?.identity.id || selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) return;
await revokeClientSecret({
identityId: data?.identity.id,
clientSecretId
});
handlePopUpToggle("revokeClientSecret", false);
createNotification({
text: "Successfully deleted client secret",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete client secret",
type: "error"
});
}
};
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.identity.name} /> <PageHeader title={data.identity.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("identity", {
identityId,
name: data.identity.name,
role: data.role,
customRole: data.customRole
});
}}
disabled={!isAllowed}
>
Edit Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
allAuthMethods: data.identity.authMethods
});
}}
disabled={!isAllowed}
>
Add new auth method
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("deleteIdentity", {
identityId,
name: data.identity.name
});
}}
disabled={!isAllowed}
>
Delete Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</PageHeader>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} /> <IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
<IdentityAuthenticationSection <IdentityAuthenticationSection
selectedAuthMethod={selectedAuthMethod}
setSelectedAuthMethod={setSelectedAuthMethod}
identityId={identityId} identityId={identityId}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
/> />
@ -94,6 +260,18 @@ const Page = () => {
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
<IdentityTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityTokenListModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityClientSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityUniversalAuthClientSecretModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
@ -112,11 +290,41 @@ const Page = () => {
) )
} }
/> />
<ViewIdentityAuthModal <DeleteActionModal
isOpen={popUp.viewAuthMethod.isOpen} isOpen={popUp.revokeToken.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)} title={`Are you sure want to revoke ${
authMethod={popUp.viewAuthMethod.data} (popUp?.revokeToken?.data as { name: string })?.name || ""
identityId={identityId} }?`}
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const revokeTokenData = popUp?.revokeToken?.data as {
identityId: string;
tokenId: string;
name: string;
};
return onRevokeTokenSubmit(revokeTokenData);
}}
/>
<DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
""
}************?`}
onChange={(isOpen) => handlePopUpToggle("revokeClientSecret", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const deleteClientSecretData = popUp?.revokeClientSecret?.data as {
clientSecretId: string;
clientSecretPrefix: string;
};
return onDeleteClientSecretSubmit({
clientSecretId: deleteClientSecretData.clientSecretId
});
}}
/> />
</div> </div>
); );

View File

@ -1,73 +1,155 @@
import { faCog, faPlus } from "@fortawesome/free-solid-svg-icons"; import { useEffect } from "react";
import { faPencil, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api"; import { useGetIdentityById } from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { Identity } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityClientSecrets } from "./IdentityClientSecrets";
import { IdentityTokens } from "./IdentityTokens";
type Props = { type Props = {
identityId: string; identityId: string;
setSelectedAuthMethod: (authMethod: Identity["authMethods"][number] | null) => void;
selectedAuthMethod: Identity["authMethods"][number] | null;
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "viewAuthMethod"]>, popUpName: keyof UsePopUpState<
data?: object | IdentityAuthMethod [
"clientSecret",
"identityAuthMethod",
"revokeClientSecret",
"token",
"revokeToken",
"universalAuthClientSecret",
"tokenList"
]
>,
data?: object
) => void; ) => void;
}; };
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => { export const IdentityAuthenticationSection = ({
identityId,
setSelectedAuthMethod,
selectedAuthMethod,
handlePopUpOpen
}: Props) => {
const { data } = useGetIdentityById(identityId); const { data } = useGetIdentityById(identityId);
useEffect(() => {
if (!data?.identity) return;
if (data.identity.authMethods?.length) {
setSelectedAuthMethod(data.identity.authMethods[0]);
}
// eslint-disable-next-line consistent-return
return () => setSelectedAuthMethod(null);
}, [data?.identity]);
return data ? ( return data ? (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3> <h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (
<Tooltip content="Add new auth method">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() =>
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
allAuthMethods: data.identity.authMethods
})
}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
</div> </div>
{data.identity.authMethods.length > 0 ? ( {data.identity.authMethods.length > 0 ? (
<div className="flex flex-col divide-y divide-mineshaft-400/50"> <>
{data.identity.authMethods.map((authMethod) => ( <div className="py-4">
<button <div className="flex justify-between">
key={authMethod} <p className="mb-0.5 ml-px text-sm font-semibold text-mineshaft-300">Auth Method</p>
onClick={() => handlePopUpOpen("viewAuthMethod", authMethod)} </div>
type="button" <div className="flex items-center gap-2">
className="flex w-full items-center justify-between bg-mineshaft-900 px-4 py-2 text-sm hover:bg-mineshaft-700 data-[state=open]:bg-mineshaft-600" <div className="w-full">
> <Select
<span>{identityAuthToNameMap[authMethod]}</span> className="w-full"
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" /> value={selectedAuthMethod as string}
</button> onValueChange={(value) => setSelectedAuthMethod(value as IdentityAuthMethod)}
))} >
</div> {(data.identity?.authMethods || []).map((authMethod) => (
<SelectItem key={authMethod || authMethod} value={authMethod}>
{identityAuthToNameMap[authMethod]}
</SelectItem>
))}
</Select>
</div>
<div>
<Tooltip content="Edit auth method">
<IconButton
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
authMethod: selectedAuthMethod,
allAuthMethods: data.identity.authMethods
});
}}
ariaLabel="copy icon"
variant="plain"
className="group relative"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>{" "}
</div>
</div>
</div>
{selectedAuthMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
{selectedAuthMethod === IdentityAuthMethod.TOKEN_AUTH && (
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
</>
) : ( ) : (
<div className="w-full space-y-2 pt-2"> <div className="w-full space-y-2 pt-2">
<p className="text-sm text-mineshaft-300"> <p className="text-sm text-mineshaft-300">
No authentication methods configured. Get started by creating a new auth method. No authentication methods configured. Get started by creating a new auth method.
</p> </p>
<Button
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
allAuthMethods: data.identity.authMethods
});
}}
variant="outline_bg"
className="w-full"
size="xs"
>
Create Auth Method
</Button>
</div> </div>
)} )}
{!Object.values(IdentityAuthMethod).every((method) =>
data.identity.authMethods.includes(method)
) && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
allAuthMethods: data.identity.authMethods
});
}}
variant="outline_bg"
className="mt-3 w-full"
size="xs"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
{data.identity.authMethods.length ? "Add" : "Create"} Auth Method
</Button>
)}
</OrgPermissionCan>
)}
</div> </div>
) : ( ) : (
<div /> <div />

View File

@ -1,4 +1,4 @@
import { faEllipsisVertical, faKey } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
@ -8,7 +8,6 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
IconButton,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api"; import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
@ -28,13 +27,10 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
return ( return (
<div> <div>
{tokens?.length ? ( {tokens?.length ? (
<div className="flex items-center justify-between border-b border-bunker-400 pb-1"> <div className="flex justify-between">
<p className="text-sm font-medium text-bunker-300">{`Access Tokens (${tokens.length})`}</p> <p className="text-sm font-semibold text-mineshaft-300">{`Access Tokens (${tokens.length})`}</p>
<Button <Button
size="xs" variant="link"
className="underline"
variant="plain"
colorSchema="secondary"
onClick={() => { onClick={() => {
handlePopUpOpen("tokenList", { handlePopUpOpen("tokenList", {
identityId, identityId,
@ -54,16 +50,16 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
); );
return ( return (
<div <div
className="group flex items-center justify-between border-b border-mineshaft-500 px-2 py-2 last:pb-0" className="group flex items-center justify-between py-2 last:pb-0"
key={`identity-token-${token.id}`} key={`identity-token-${token.id}`}
> >
<div className="flex items-center"> <div className="flex items-center">
<FontAwesomeIcon size="xs" className="text-mineshaft-400" icon={faKey} /> <FontAwesomeIcon size="1x" icon={faKey} />
<div className="ml-3"> <div className="ml-4">
<p className="text-sm font-medium text-mineshaft-300"> <p className="text-sm font-semibold text-mineshaft-300">
{token.name ? token.name : "-"} {token.name ? token.name : "-"}
</p> </p>
<p className="text-xs text-mineshaft-400"> <p className="text-sm text-mineshaft-300">
{token.isAccessTokenRevoked {token.isAccessTokenRevoked
? "Revoked" ? "Revoked"
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`} : `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
@ -71,19 +67,14 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
</div> </div>
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild className="rounded-lg">
<Tooltip side="right" content="More options"> <div className="opacity-0 transition-opacity duration-300 hover:text-primary-400 group-hover:opacity-100 data-[state=open]:text-primary-400">
<IconButton <Tooltip content="More options">
colorSchema="secondary" <FontAwesomeIcon size="sm" icon={faEllipsis} />
variant="plain" </Tooltip>
size="xs" </div>
ariaLabel="More options"
>
<FontAwesomeIcon icon={faEllipsisVertical} />
</IconButton>
</Tooltip>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[101] p-1"> <DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem <DropdownMenuItem
onClick={async () => { onClick={async () => {
handlePopUpOpen("token", { handlePopUpOpen("token", {
@ -115,9 +106,8 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
})} })}
<Button <Button
className="mr-4 mt-4 w-full" className="mr-4 mt-4 w-full"
colorSchema="secondary" colorSchema="primary"
type="submit" type="submit"
size="xs"
onClick={() => { onClick={() => {
handlePopUpOpen("token", { handlePopUpOpen("token", {
identityId identityId

View File

@ -1,25 +1,8 @@
import { import { faCheck, faCopy, faKey, faPencil } from "@fortawesome/free-solid-svg-icons";
faCheck,
faChevronDown,
faCopy,
faEdit,
faKey,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import { IconButton, Tag, Tooltip } from "@app/components/v2";
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Tag,
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { useGetIdentityById } from "@app/hooks/api"; import { useGetIdentityById } from "@app/hooks/api";
@ -28,7 +11,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
identityId: string; identityId: string;
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "deleteIdentity"]>, popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "token", "clientSecret"]>,
data?: object data?: object
) => void; ) => void;
}; };
@ -43,61 +26,31 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Identity Details</h3> <h3 className="text-lg font-semibold text-mineshaft-100">Identity Details</h3>
<DropdownMenu> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
<DropdownMenuTrigger asChild> {(isAllowed) => {
<Button return (
size="xs" <Tooltip content="Edit Identity">
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />} <IconButton
colorSchema="secondary" isDisabled={!isAllowed}
> ariaLabel="copy icon"
Options variant="plain"
</Button> className="group relative"
</DropdownMenuTrigger> onClick={() => {
<DropdownMenuContent className="min-w-[120px]" align="end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={async () => {
handlePopUpOpen("identity", { handlePopUpOpen("identity", {
identityId, identityId,
name: data.identity.name, name: data.identity.name,
role: data.role, role: data.role,
customRole: data.customRole customRole: data.customRole,
metadata: data.metadata
}); });
}} }}
disabled={!isAllowed}
> >
Edit Identity <FontAwesomeIcon icon={faPencil} />
</DropdownMenuItem> </IconButton>
)} </Tooltip>
</OrgPermissionCan> );
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Identity}> }}
{(isAllowed) => ( </OrgPermissionCan>
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("deleteIdentity", {
identityId,
name: data.identity.name
});
}}
icon={<FontAwesomeIcon icon={faTrash} />}
disabled={!isAllowed}
>
Delete Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
<div className="pt-4"> <div className="pt-4">
<div className="mb-4"> <div className="mb-4">

View File

@ -0,0 +1,270 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import {
useCreateTokenIdentityTokenAuth,
useGetIdentityTokensTokenAuth,
useGetIdentityUniversalAuthClientSecrets
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
name: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["tokenList", "revokeToken"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["revokeToken"]>, data?: object) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["tokenList", "revokeToken"]>,
state?: boolean
) => void;
};
export const IdentityTokenListModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { t } = useTranslation();
const [token, setToken] = useState("");
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
const popUpData = popUp?.tokenList?.data as {
identityId: string;
name: string;
};
const { data: tokens } = useGetIdentityTokensTokenAuth(popUpData?.identityId ?? "");
const { data, isPending } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: ""
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientSecretCopied) {
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientSecretCopied]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientIdCopied) {
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientIdCopied]);
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!popUpData?.identityId) return;
const newTokenData = await createToken({
identityId: popUpData.identityId,
name
});
setToken(newTokenData.accessToken);
createNotification({
text: "Successfully created token",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create token",
type: "error"
});
}
};
const hasToken = Boolean(token);
return (
<Modal
isOpen={popUp?.tokenList?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("tokenList", isOpen);
reset();
setToken("");
}}
>
<ModalContent title={`Manage Access Tokens for ${popUpData?.name ?? ""}`}>
<h2 className="mb-4">New Token</h2>
{hasToken ? (
<div>
<div className="mb-4 flex items-center justify-between">
<p>We will only show this token once</p>
<Button
colorSchema="secondary"
type="submit"
onClick={() => {
reset();
setToken("");
}}
>
Got it
</Button>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{token}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(token);
setIsClientSecretCopied.on();
}}
>
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
</div>
) : (
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
<div className="flex">
<Input {...field} placeholder="My Token" />
<Button
className="ml-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</div>
</FormControl>
)}
/>
</form>
)}
<h2 className="mb-4">Tokens</h2>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>name</Th>
<Th>Num Uses</Th>
<Th>Created At</Th>
<Th>Max Expires At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={5} innerKey="identities-tokens" />}
{!isPending &&
tokens?.map(
({
id,
createdAt,
name,
accessTokenNumUses,
accessTokenNumUsesLimit,
accessTokenMaxTTL,
isAccessTokenRevoked
}) => {
const expiresAt = new Date(
new Date(createdAt).getTime() + accessTokenMaxTTL * 1000
);
return (
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
<Td>{name === "" ? "-" : name}</Td>
<Td>{`${accessTokenNumUses}${
accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : ""
}`}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td>
{isAccessTokenRevoked ? "Revoked" : `${format(expiresAt, "yyyy-MM-dd")}`}
</Td>
<Td>
{!isAccessTokenRevoked && (
<IconButton
onClick={() => {
handlePopUpOpen("revokeToken", {
identityId: popUpData?.identityId,
tokenId: id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</Td>
</Tr>
);
}
)}
{!isPending && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No tokens have been created for this identity yet"
icon={faKey}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
);
};

View File

@ -1,20 +0,0 @@
import { ReactNode } from "react";
type Props = {
label: string;
children: ReactNode;
className?: string;
};
export const IdentityAuthFieldDisplay = ({ label, children, className }: Props) => {
return (
<div className={className}>
<span className="text-sm text-mineshaft-400">{label}</span>
{children ? (
<p className="break-words text-base leading-4">{children}</p>
) : (
<p className="text-base italic leading-4 text-bunker-400">Not set</p>
)}
</div>
);
};

View File

@ -1,239 +0,0 @@
import { useState } from "react";
import { faBan, faEdit, faKey, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Pagination,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useRevokeIdentityTokenAuthToken } from "@app/hooks/api";
import { IdentityAccessToken } from "@app/hooks/api/identities/types";
import { IdentityTokenModal } from "@app/pages/organization/IdentityDetailsByIDPage/components";
type Props = {
tokens: IdentityAccessToken[];
identityId: string;
};
export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"token",
"revokeToken"
] as const);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(5);
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
const onRevokeTokenSubmit = async ({
identityId: parentIdentityId,
tokenId,
name
}: {
identityId: string;
tokenId: string;
name: string;
}) => {
try {
await revokeToken({
identityId: parentIdentityId,
tokenId
});
handlePopUpClose("revokeToken");
createNotification({
text: `Successfully revoked token ${name ?? ""}`,
type: "success"
});
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to revoke token";
createNotification({
text,
type: "error"
});
}
};
return (
<div className="col-span-2 mt-3">
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Access Tokens</span>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<Button
size="xs"
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("token", {
identityId
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="secondary"
>
Add Token
</Button>
)}
</OrgPermissionCan>
</div>
<TableContainer className="mt-4 rounded-none border-none">
<Table>
{Boolean(tokens?.length) && (
<THead>
<Tr className="text-xs font-medium">
<Th className="py-1 font-normal">Name</Th>
<Th className="whitespace-nowrap py-1 font-normal">Number of Uses</Th>
<Th className="py-1 font-normal">Expires</Th>
<Th className="w-5 py-1 font-normal" />
</Tr>
</THead>
)}
<TBody>
{tokens
.slice((page - 1) * perPage, perPage * page)
.map(
({
createdAt,
isAccessTokenRevoked,
name,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenNumUses,
id
}) => {
let expiresAt;
if (accessTokenTTL > 0) {
expiresAt = new Date(new Date(createdAt).getTime() + accessTokenTTL * 1000);
}
return (
<Tr className="text-xs hover:bg-mineshaft-700" key={id}>
<Td>{name || "-"}</Td>
<Td>
{`${accessTokenNumUses}${accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : ""}`}
</Td>
<Td className="whitespace-nowrap">
{/* eslint-disable-next-line no-nested-ternary */}
{isAccessTokenRevoked
? "Revoked"
: expiresAt
? format(expiresAt, "yyyy-MM-dd")
: "-"}
</Td>
<Td>
<div className="flex items-center gap-2">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Tooltip content={isAllowed ? "Edit Token" : "Access Restricted"}>
<IconButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("token", {
identityId,
tokenId: id,
name
});
}}
size="xs"
variant="plain"
ariaLabel="Edit token"
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
{!isAccessTokenRevoked && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Tooltip content={isAllowed ? "Revoke Token" : "Access Restricted"}>
<IconButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("revokeToken", {
identityId,
tokenId: id,
name
});
}}
size="xs"
colorSchema="danger"
variant="plain"
ariaLabel="Revoke token"
>
<FontAwesomeIcon icon={faBan} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
)}
</div>
</Td>
</Tr>
);
}
)}
</TBody>
</Table>
{!tokens?.length && (
<EmptyState iconSize="1x" title="No access tokens have been generated" icon={faKey} />
)}
{tokens.length > 0 && (
<Pagination
count={tokens.length}
page={page}
perPage={perPage}
perPageList={[5]}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeToken.isOpen}
title={`Are you sure want to revoke ${
(popUp?.revokeToken?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const revokeTokenData = popUp?.revokeToken?.data as {
identityId: string;
tokenId: string;
name: string;
};
return onRevokeTokenSubmit(revokeTokenData);
}}
/>
<IdentityTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div>
);
};

View File

@ -1,196 +0,0 @@
import { useState } from "react";
import { faKey, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Pagination,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useRevokeIdentityUniversalAuthClientSecret } from "@app/hooks/api";
import { ClientSecretData } from "@app/hooks/api/identities/types";
import { IdentityClientSecretModal } from "@app/pages/organization/IdentityDetailsByIDPage/components";
type Props = {
clientSecrets: ClientSecretData[];
identityId: string;
};
export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identityId }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"revokeClientSecret",
"clientSecret"
] as const);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(5);
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
const onDeleteClientSecretSubmit = async (clientSecretId: string) => {
try {
await revokeClientSecret({
identityId,
clientSecretId
});
handlePopUpToggle("revokeClientSecret", false);
createNotification({
text: "Successfully deleted client secret",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete client secret",
type: "error"
});
}
};
return (
<div className="col-span-2">
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Client Secrets</span>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
size="xs"
onClick={() => {
handlePopUpOpen("clientSecret", {
identityId
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="secondary"
>
Add Client Secret
</Button>
)}
</OrgPermissionCan>
</div>
<TableContainer className="mt-4 rounded-none border-none">
<Table>
{Boolean(clientSecrets?.length) && (
<THead>
<Tr className="text-xs font-medium">
<Th className="py-1 font-normal">Secret</Th>
<Th className="py-1 font-normal">Description</Th>
<Th className="whitespace-nowrap py-1 font-normal">Number of Uses</Th>
<Th className="py-1 font-normal">Expires</Th>
<Th className="w-5 py-1 font-normal" />
</Tr>
</THead>
)}
<TBody>
{clientSecrets
.slice((page - 1) * perPage, perPage * page)
.map(
({
createdAt,
clientSecretTTL,
description,
clientSecretNumUses,
clientSecretPrefix,
clientSecretNumUsesLimit,
id
}) => {
let expiresAt;
if (clientSecretTTL > 0) {
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
}
return (
<Tr className="text-xs hover:bg-mineshaft-700" key={id}>
<Td>{clientSecretPrefix}***</Td>
<Td>{description || "-"}</Td>
<Td>
{`${clientSecretNumUses}${clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""}`}
</Td>
<Td className="whitespace-nowrap">
{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}
</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Tooltip content={isAllowed ? "Delete Secret" : "Access Restricted"}>
<IconButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("revokeClientSecret", {
clientSecretPrefix,
clientSecretId: id
});
}}
size="xs"
colorSchema="danger"
variant="plain"
ariaLabel="Delete secret"
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
</Td>
</Tr>
);
}
)}
</TBody>
</Table>
{!clientSecrets?.length && (
<EmptyState iconSize="1x" title="No client secrets have been generated" icon={faKey} />
)}
{clientSecrets.length > 0 && (
<Pagination
count={clientSecrets.length}
page={page}
perPage={perPage}
perPageList={[5]}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
""
}************?`}
onChange={(isOpen) => handlePopUpToggle("revokeClientSecret", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const deleteClientSecretData = popUp?.revokeClientSecret?.data as {
clientSecretId: string;
clientSecretPrefix: string;
};
return onDeleteClientSecretSubmit(deleteClientSecretData.clientSecretId);
}}
/>
<IdentityClientSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div>
);
};

View File

@ -1,174 +0,0 @@
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
IdentityAuthMethod,
identityAuthToNameMap,
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityJwtAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityAwsAuthContent } from "./ViewIdentityAwsAuthContent";
import { ViewIdentityAzureAuthContent } from "./ViewIdentityAzureAuthContent";
import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent";
import { ViewIdentityJwtAuthContent } from "./ViewIdentityJwtAuthContent";
import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthContent";
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
type Props = {
identityId: string;
authMethod?: IdentityAuthMethod;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onDeleteAuthMethod: () => void;
};
type TRevokeOptions = {
identityId: string;
organizationId: string;
};
export const Content = ({
identityId,
authMethod,
onDeleteAuthMethod
}: Pick<Props, "authMethod" | "identityId" | "onDeleteAuthMethod">) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"revokeAuthMethod",
"upgradePlan",
"identityAuthMethod"
] as const);
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
let Component: (props: ViewAuthMethodProps) => JSX.Element;
let revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
const handleDelete = () => handlePopUpOpen("revokeAuthMethod");
switch (authMethod) {
case IdentityAuthMethod.UNIVERSAL_AUTH:
revokeMethod = revokeUniversalAuth;
Component = ViewIdentityUniversalAuthContent;
break;
case IdentityAuthMethod.TOKEN_AUTH:
revokeMethod = revokeTokenAuth;
Component = ViewIdentityTokenAuthContent;
break;
case IdentityAuthMethod.KUBERNETES_AUTH:
revokeMethod = revokeKubernetesAuth;
Component = ViewIdentityKubernetesAuthContent;
break;
case IdentityAuthMethod.GCP_AUTH:
revokeMethod = revokeGcpAuth;
Component = ViewIdentityGcpAuthContent;
break;
case IdentityAuthMethod.AWS_AUTH:
revokeMethod = revokeAwsAuth;
Component = ViewIdentityAwsAuthContent;
break;
case IdentityAuthMethod.AZURE_AUTH:
revokeMethod = revokeAzureAuth;
Component = ViewIdentityAzureAuthContent;
break;
case IdentityAuthMethod.OIDC_AUTH:
revokeMethod = revokeOidcAuth;
Component = ViewIdentityOidcAuthContent;
break;
case IdentityAuthMethod.JWT_AUTH:
revokeMethod = revokeJwtAuth;
Component = ViewIdentityJwtAuthContent;
break;
default:
throw new Error(`Unhandled Auth Method: ${authMethod}`);
}
const handleDeleteAuthMethod = async () => {
try {
await revokeMethod({
identityId,
organizationId: orgId
});
createNotification({
text: "Successfully removed auth method",
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
onDeleteAuthMethod();
} catch {
createNotification({
text: "Failed to remove auth method",
type: "error"
});
}
};
return (
<>
<Component
identityId={identityId}
onDelete={handleDelete}
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={handleDeleteAuthMethod}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</>
);
};
export const ViewIdentityAuthModal = ({
isOpen,
onOpenChange,
authMethod,
identityId
}: Omit<Props, "onDeleteAuthMethod">) => {
if (!identityId || !authMethod) return null;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent className="max-w-2xl" title={identityAuthToNameMap[authMethod]}>
<Content
identityId={identityId}
authMethod={authMethod}
onDeleteAuthMethod={() => onOpenChange(false)}
/>
</ModalContent>
</Modal>
);
};

View File

@ -1,79 +0,0 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Spinner } from "@app/components/v2";
import { useGetIdentityAwsAuth } from "@app/hooks/api";
import { IdentityAwsAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityAwsAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityAwsAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find AWS Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityAwsAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Principal ARNs">
{data.allowedPrincipalArns
?.split(",")
.map((arn) => arn.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Account IDs">
{data.allowedAccountIds
?.split(",")
.map((id) => id.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="STS Endpoint">
{data.stsEndpoint}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -1,76 +0,0 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Spinner } from "@app/components/v2";
import { useGetIdentityAzureAuth } from "@app/hooks/api";
import { IdentityAzureAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityAzureAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityAzureAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find Azure Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityAzureAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Tenant ID">
{data.tenantId}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Resource / Audience">
{data.resource}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Principal IDs">
{data.allowedServicePrincipalIds
?.split(",")
.map((id) => id.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -1,69 +0,0 @@
import { ReactNode } from "react";
import { faChevronDown, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
type Props = {
children: ReactNode;
onEdit: VoidFunction;
onDelete: VoidFunction;
};
export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props) => {
return (
<div className="flex flex-col gap-4">
<div>
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Details</span>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="xs"
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
colorSchema="secondary"
>
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[120px]" align="end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
isDisabled={!isAllowed}
onClick={onEdit}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
isDisabled={!isAllowed}
onClick={onDelete}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-3">{children}</div>
</div>
</div>
);
};

View File

@ -1,89 +0,0 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Spinner } from "@app/components/v2";
import { useGetIdentityGcpAuth } from "@app/hooks/api";
import { IdentityGcpAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityGcpAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityGcpAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find GCP Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityGcpAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Type">
{data.type === "gce" ? "GCP ID Token Auth" : "GCP IAM Auth"}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Emails">
{data.allowedServiceAccounts
?.split(",")
.map((account) => account.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
{data.type === "gce" && (
<>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Projects">
{data.allowedProjects
?.split(",")
.map((project) => project.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Zones">
{data.allowedZones
?.split(",")
.map((zone) => zone.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
</>
)}
</ViewIdentityContentWrapper>
);
};

View File

@ -1,152 +0,0 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityJwtAuth } from "@app/hooks/api";
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
import { IdentityJwtAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm";
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
export const ViewIdentityJwtAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityJwtAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find JWT Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityJwtAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Configuration Type">
{data.configurationType === IdentityJwtConfigurationType.JWKS ? "JWKS" : "Static"}
</IdentityAuthFieldDisplay>
{data.configurationType === IdentityJwtConfigurationType.JWKS ? (
<>
<IdentityAuthFieldDisplay className="col-span-2" label="JWKS URL">
{data.jwksUrl}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="JWKS CA Certificate">
{data.jwksCaCert && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<p className="break-words rounded bg-mineshaft-600 p-2">{data.jwksCaCert}</p>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</>
) : (
<IdentityAuthFieldDisplay className="col-span-2" label="Public Keys">
{data.publicKeys.length && (
<div className="flex flex-wrap gap-1">
{data.publicKeys.map((key, index) => (
<Tooltip
side="right"
className="max-w-xl p-2"
key={key}
content={
<p className="whitespace-normal break-words rounded bg-mineshaft-600 p-2">
{key}
</p>
}
>
<div className="inline-block w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Key {index + 1}</span>
</Badge>
</div>
</Tooltip>
))}
</div>
)}
</IdentityAuthFieldDisplay>
)}
<IdentityAuthFieldDisplay className="col-span-2" label="Issuer">
{data.boundIssuer}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Subject">
{data.boundSubject}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Audiences">
{data.boundAudiences
?.split(",")
.map((name) => name.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Claims">
{Object.keys(data.boundClaims).length && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
{JSON.stringify(data.boundClaims, null, 2)}
</pre>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -1,121 +0,0 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityKubernetesAuth } from "@app/hooks/api";
import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityKubernetesAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityKubernetesAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState
icon={faBan}
title="Could not find Kubernetes Auth associated with this Identity."
/>
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityKubernetesAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay
className="col-span-2"
label="Kubernetes Host / Base Kubernetes API URL"
>
{data.kubernetesHost}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT">
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<p className="break-words rounded bg-mineshaft-600 p-2">{data.tokenReviewerJwt}</p>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Names">
{data.allowedNames
?.split(",")
.map((name) => name.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Namespaces">
{data.allowedNamespaces
?.split(",")
.map((namespace) => namespace.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Audience">
{data.allowedAudience}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
{data.caCert && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCert}</p>}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -1,116 +0,0 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityOidcAuth } from "@app/hooks/api";
import { IdentityOidcAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm";
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
export const ViewIdentityOidcAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityOidcAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find OIDC Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityOidcAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="OIDC Discovery URL">
{data.oidcDiscoveryUrl}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Issuer">
{data.boundIssuer}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
{data.caCert && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCert}</p>}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Subject">
{data.boundSubject}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Audiences">
{data.boundAudiences
?.split(",")
.map((name) => name.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Claims">
{Object.keys(data.boundClaims).length && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
{JSON.stringify(data.boundClaims, null, 2)}
</pre>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -1,68 +0,0 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Spinner } from "@app/components/v2";
import { useGetIdentityTokenAuth, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
import { IdentityTokenAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { IdentityTokenAuthTokensTable } from "./IdentityTokenAuthTokensTable";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityTokenAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityTokenAuth(identityId);
const { data: tokens = [], isPending: clientSecretsPending } =
useGetIdentityTokensTokenAuth(identityId);
if (isPending || clientSecretsPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find Token Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityTokenAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityTokenAuthTokensTable tokens={tokens} identityId={identityId} />
</ViewIdentityContentWrapper>
);
};

View File

@ -1,106 +0,0 @@
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import {
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets
} from "@app/hooks/api";
import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { IdentityUniversalAuthClientSecretsTable } from "./IdentityUniversalAuthClientSecretsTable";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityUniversalAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
const { data: clientSecrets = [], isPending: clientSecretsPending } =
useGetIdentityUniversalAuthClientSecrets(identityId);
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
initialState: "Copy Client ID to clipboard"
});
if (isPending || clientSecretsPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState
icon={faBan}
title="Could not find Universal Auth associated with this Identity."
/>
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityUniversalAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<div className="col-span-2 my-3">
<div className="mb-3 border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Client ID</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm">{data.clientId}</span>
<Tooltip content={copyTextClientId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
onClick={() => {
navigator.clipboard.writeText(data.clientId);
setCopyTextClientId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingClientId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
<IdentityUniversalAuthClientSecretsTable
clientSecrets={clientSecrets}
identityId={identityId}
/>
</ViewIdentityContentWrapper>
);
};

View File

@ -1 +0,0 @@
export * from "./ViewIdentityAuthModal";

View File

@ -1,12 +0,0 @@
import { UsePopUpState } from "@app/hooks/usePopUp";
export type ViewAuthMethodProps = {
identityId: string;
onDelete: () => void;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan", "identityAuthMethod"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>;
};

View File

@ -2,4 +2,5 @@ export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection/I
export { IdentityClientSecretModal } from "./IdentityClientSecretModal"; export { IdentityClientSecretModal } from "./IdentityClientSecretModal";
export { IdentityDetailsSection } from "./IdentityDetailsSection"; export { IdentityDetailsSection } from "./IdentityDetailsSection";
export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection"; export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection";
export { IdentityTokenListModal } from "./IdentityTokenListModal";
export { IdentityTokenModal } from "./IdentityTokenModal"; export { IdentityTokenModal } from "./IdentityTokenModal";