mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
6 Commits
daniel/upd
...
identity-a
Author | SHA1 | Date | |
---|---|---|---|
a534a4975c | |||
79a616dc1c | |||
598d14fc54 | |||
99c9b644df | |||
94aed485a5 | |||
e382941424 |
@ -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 [infisicalsdk](https://pypi.org/project/infisicalsdk/) Python client SDK to fetch secrets back to your Python application on demand.
|
- The [infisical-python](https://pypi.org/project/infisical-python/) Python client SDK to fetch secrets back to your Python application on demand.
|
||||||
|
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
@ -36,38 +36,40 @@ python3 -m venv env
|
|||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
Install Flask and [infisicalsdk](https://pypi.org/project/infisicalsdk/), the client Python SDK for Infisical.
|
Install Flask and [infisical-python](https://pypi.org/project/infisical-python/), the client Python SDK for Infisical.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
pip install flask infisicalsdk
|
pip install flask infisical-python
|
||||||
```
|
```
|
||||||
|
|
||||||
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_sdk import InfisicalSDKClient
|
from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions, AuthenticationOptions, UniversalAuthMethod
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
client = InfisicalSDKClient(host="https://app.infisical.com") # host is optional, defaults to https://app.infisical.com
|
client = InfisicalClient(ClientSettings(
|
||||||
|
auth=AuthenticationOptions(
|
||||||
client.auth.universal_auth.login(
|
universal_auth=UniversalAuthMethod(
|
||||||
"<machine-identity-client-id>",
|
client_id="CLIENT_ID",
|
||||||
"<machine-identity-client-secret>"
|
client_secret="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="/"
|
|
||||||
)
|
|
||||||
|
|
||||||
return f"Hello! My name is: {name.secretValue}"
|
name = client.getSecret(options=GetSecretOptions(
|
||||||
|
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
|
||||||
@ -87,6 +89,15 @@ 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
|
||||||
@ -103,6 +114,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/python-sdk-official)
|
- Explore the [Python SDK](https://github.com/Infisical/sdk/tree/main/crates/infisical-py)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.userprincipalname`
|
- `email -> user.userprinciplename`
|
||||||
- `firstName -> user.givenname`
|
- `firstName -> user.givenname`
|
||||||
- `lastName -> user.surname`
|
- `lastName -> user.surname`
|
||||||
|
|
||||||
|
@ -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.getIdentityUniversalAuth(identityId)
|
queryKey: identitiesKeys.getIdentityTokenAuth(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.getIdentityUniversalAuth(identityId)
|
queryKey: identitiesKeys.getIdentityTokenAuth(identityId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
@ -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", "revokeAuthMethod"]>;
|
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
|
||||||
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!] ?? ""}`
|
||||||
: `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
: `Add ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IdentityAuthMethodModalContent
|
<IdentityAuthMethodModalContent
|
||||||
|
@ -4,30 +4,8 @@ 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 { createNotification } from "@app/components/notifications";
|
import { Badge, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
import {
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
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";
|
||||||
@ -40,10 +18,10 @@ import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
|
|||||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
|
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
@ -56,13 +34,7 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,18 +68,6 @@ 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 () => {
|
||||||
@ -149,10 +109,9 @@ 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
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -160,10 +119,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.OIDC_AUTH]: {
|
[IdentityAuthMethod.OIDC_AUTH]: {
|
||||||
revokeMethod: revokeOidcAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityOidcAuthForm
|
<IdentityOidcAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -171,10 +129,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.TOKEN_AUTH]: {
|
[IdentityAuthMethod.TOKEN_AUTH]: {
|
||||||
revokeMethod: revokeTokenAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityTokenAuthForm
|
<IdentityTokenAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -182,10 +139,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.AZURE_AUTH]: {
|
[IdentityAuthMethod.AZURE_AUTH]: {
|
||||||
revokeMethod: revokeAzureAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityAzureAuthForm
|
<IdentityAzureAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -193,10 +149,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.GCP_AUTH]: {
|
[IdentityAuthMethod.GCP_AUTH]: {
|
||||||
revokeMethod: revokeGcpAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityGcpAuthForm
|
<IdentityGcpAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -204,10 +159,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.KUBERNETES_AUTH]: {
|
[IdentityAuthMethod.KUBERNETES_AUTH]: {
|
||||||
revokeMethod: revokeKubernetesAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityKubernetesAuthForm
|
<IdentityKubernetesAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -215,10 +169,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.AWS_AUTH]: {
|
[IdentityAuthMethod.AWS_AUTH]: {
|
||||||
revokeMethod: revokeAwsAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityAwsAuthForm
|
<IdentityAwsAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -226,10 +179,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.JWT_AUTH]: {
|
[IdentityAuthMethod.JWT_AUTH]: {
|
||||||
revokeMethod: revokeJwtAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityJwtAuthForm
|
<IdentityJwtAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -294,42 +246,6 @@ 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"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
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,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
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(),
|
||||||
@ -49,21 +59,18 @@ 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", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAwsAuthForm = ({
|
export const IdentityAwsAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -71,11 +78,9 @@ 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 isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityAwsAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -143,7 +148,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@ -151,7 +156,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
stsEndpoint,
|
stsEndpoint,
|
||||||
allowedPrincipalArns,
|
allowedPrincipalArns,
|
||||||
allowedAccountIds,
|
allowedAccountIds,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -160,7 +165,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
stsEndpoint: stsEndpoint || "",
|
stsEndpoint: stsEndpoint || "",
|
||||||
allowedPrincipalArns: allowedPrincipalArns || "",
|
allowedPrincipalArns: allowedPrincipalArns || "",
|
||||||
allowedAccountIds: allowedAccountIds || "",
|
allowedAccountIds: allowedAccountIds || "",
|
||||||
@ -188,189 +193,194 @@ export const IdentityAwsAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form
|
||||||
<Controller
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
control={control}
|
setTabValue(
|
||||||
defaultValue="2592000"
|
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
||||||
name="allowedPrincipalArns"
|
? IdentityFormTab.Advanced
|
||||||
render={({ field, fieldState: { error } }) => (
|
: IdentityFormTab.Configuration
|
||||||
<FormControl
|
);
|
||||||
label="Allowed Principal ARNs"
|
})}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
>
|
<TabList>
|
||||||
<Input
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
{...field}
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
</TabList>
|
||||||
type="text"
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
/>
|
|
||||||
</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}
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
defaultValue="2592000"
|
||||||
defaultValue="0.0.0.0/0"
|
name="allowedPrincipalArns"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
<FormControl
|
||||||
<FormControl
|
label="Allowed Principal ARNs"
|
||||||
className="mb-0 flex-grow"
|
isError={Boolean(error)}
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Input
|
||||||
>
|
{...field}
|
||||||
<Input
|
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
||||||
value={field.value}
|
type="text"
|
||||||
onChange={(e) => {
|
/>
|
||||||
if (subscription?.ipAllowlisting) {
|
</FormControl>
|
||||||
field.onChange(e);
|
)}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<Controller
|
||||||
onClick={() => {
|
control={control}
|
||||||
if (subscription?.ipAllowlisting) {
|
name="allowedAccountIds"
|
||||||
removeAccessTokenTrustedIp(index);
|
render={({ field, fieldState: { error } }) => (
|
||||||
return;
|
<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");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
placeholder="123.456.789.0"
|
||||||
colorSchema="danger"
|
/>
|
||||||
variant="plain"
|
</FormControl>
|
||||||
ariaLabel="update"
|
);
|
||||||
className="p-3"
|
}}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => {
|
||||||
</div>
|
if (subscription?.ipAllowlisting) {
|
||||||
))}
|
removeAccessTokenTrustedIp(index);
|
||||||
<div className="my-4 ml-1">
|
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
|
||||||
variant="outline_bg"
|
className="mr-4"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
if (subscription?.ipAllowlisting) {
|
type="submit"
|
||||||
appendAccessTokenTrustedIp({
|
isLoading={isSubmitting}
|
||||||
ipAddress: "0.0.0.0/0"
|
isDisabled={isSubmitting}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</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
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
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,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
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),
|
||||||
@ -44,21 +54,18 @@ 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", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAzureAuthForm = ({
|
export const IdentityAzureAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -66,11 +73,9 @@ 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 isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityAzureAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,12 +144,12 @@ export const IdentityAzureAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
tenantId,
|
tenantId,
|
||||||
resource,
|
resource,
|
||||||
allowedServicePrincipalIds,
|
allowedServicePrincipalIds,
|
||||||
@ -156,7 +161,7 @@ export const IdentityAzureAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
tenantId: tenantId || "",
|
tenantId: tenantId || "",
|
||||||
resource: resource || "",
|
resource: resource || "",
|
||||||
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
|
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
|
||||||
@ -184,189 +189,194 @@ export const IdentityAzureAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form
|
||||||
<Controller
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
control={control}
|
setTabValue(
|
||||||
defaultValue="2592000"
|
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
||||||
name="tenantId"
|
? IdentityFormTab.Advanced
|
||||||
render={({ field, fieldState: { error } }) => (
|
: IdentityFormTab.Configuration
|
||||||
<FormControl
|
);
|
||||||
label="Tenant ID"
|
})}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
isRequired
|
<TabList>
|
||||||
>
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
</FormControl>
|
</TabList>
|
||||||
)}
|
<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}
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
defaultValue="2592000"
|
||||||
defaultValue="0.0.0.0/0"
|
name="tenantId"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
<FormControl
|
||||||
<FormControl
|
label="Tenant ID"
|
||||||
className="mb-0 flex-grow"
|
isError={Boolean(error)}
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isRequired
|
||||||
errorText={error?.message}
|
>
|
||||||
>
|
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
||||||
<Input
|
</FormControl>
|
||||||
value={field.value}
|
)}
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<Controller
|
||||||
onClick={() => {
|
control={control}
|
||||||
if (subscription?.ipAllowlisting) {
|
name="resource"
|
||||||
removeAccessTokenTrustedIp(index);
|
render={({ field, fieldState: { error } }) => (
|
||||||
return;
|
<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");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
placeholder="123.456.789.0"
|
||||||
colorSchema="danger"
|
/>
|
||||||
variant="plain"
|
</FormControl>
|
||||||
ariaLabel="update"
|
);
|
||||||
className="p-3"
|
}}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => {
|
||||||
</div>
|
if (subscription?.ipAllowlisting) {
|
||||||
))}
|
removeAccessTokenTrustedIp(index);
|
||||||
<div className="my-4 ml-1">
|
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
|
||||||
variant="outline_bg"
|
className="mr-4"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
if (subscription?.ipAllowlisting) {
|
type="submit"
|
||||||
appendAccessTokenTrustedIp({
|
isLoading={isSubmitting}
|
||||||
ipAddress: "0.0.0.0/0"
|
isDisabled={isSubmitting}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</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
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
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,17 +6,29 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
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"]),
|
||||||
@ -45,21 +57,18 @@ 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", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityGcpAuthForm = ({
|
export const IdentityGcpAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -67,11 +76,9 @@ 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 isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityGcpAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -146,11 +153,11 @@ export const IdentityGcpAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
type,
|
type,
|
||||||
allowedServiceAccounts,
|
allowedServiceAccounts,
|
||||||
@ -163,7 +170,7 @@ export const IdentityGcpAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
type,
|
type,
|
||||||
allowedServiceAccounts: allowedServiceAccounts || "",
|
allowedServiceAccounts: allowedServiceAccounts || "",
|
||||||
@ -193,213 +200,222 @@ export const IdentityGcpAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form
|
||||||
<Controller
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
control={control}
|
setTabValue(
|
||||||
name="type"
|
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
? IdentityFormTab.Advanced
|
||||||
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
: IdentityFormTab.Configuration
|
||||||
<Select
|
);
|
||||||
defaultValue={field.value}
|
})}
|
||||||
{...field}
|
>
|
||||||
onValueChange={(e) => onChange(e)}
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
className="w-full"
|
<TabList>
|
||||||
>
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
<SelectItem value="gce" key="gce">
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
GCP ID Token Auth (Recommended)
|
</TabList>
|
||||||
</SelectItem>
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
name="type"
|
||||||
defaultValue="0.0.0.0/0"
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
render={({ field, fieldState: { error } }) => {
|
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
||||||
return (
|
<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
|
<FormControl
|
||||||
className="mb-0 flex-grow"
|
label="Allowed Projects"
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input {...field} placeholder="my-gcp-project, ..." />
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</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>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<Controller
|
||||||
onClick={() => {
|
control={control}
|
||||||
if (subscription?.ipAllowlisting) {
|
defaultValue="2592000"
|
||||||
removeAccessTokenTrustedIp(index);
|
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");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
placeholder="123.456.789.0"
|
||||||
colorSchema="danger"
|
/>
|
||||||
variant="plain"
|
</FormControl>
|
||||||
ariaLabel="update"
|
);
|
||||||
className="p-3"
|
}}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => {
|
||||||
</div>
|
if (subscription?.ipAllowlisting) {
|
||||||
))}
|
removeAccessTokenTrustedIp(index);
|
||||||
<div className="my-4 ml-1">
|
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
|
||||||
variant="outline_bg"
|
className="mr-4"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
if (subscription?.ipAllowlisting) {
|
type="submit"
|
||||||
appendAccessTokenTrustedIp({
|
isLoading={isSubmitting}
|
||||||
ipAddress: "0.0.0.0/0"
|
isDisabled={isSubmitting}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</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
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } 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,17 +14,22 @@ 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(
|
||||||
@ -85,21 +90,18 @@ 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", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityJwtAuthForm = ({
|
export const IdentityJwtAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -107,11 +109,9 @@ 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 isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityJwtAuth(identityId ?? "", {
|
||||||
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 (!identityAuthMethodData) {
|
if (!identityId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
configurationType,
|
configurationType,
|
||||||
jwksUrl,
|
jwksUrl,
|
||||||
@ -241,7 +241,7 @@ export const IdentityJwtAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
configurationType,
|
configurationType,
|
||||||
jwksUrl,
|
jwksUrl,
|
||||||
jwksCaCert,
|
jwksCaCert,
|
||||||
@ -275,56 +275,179 @@ export const IdentityJwtAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form
|
||||||
<Controller
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
control={control}
|
setTabValue(
|
||||||
name="configurationType"
|
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
? IdentityFormTab.Advanced
|
||||||
<FormControl
|
: IdentityFormTab.Configuration
|
||||||
label="Configuration Type"
|
);
|
||||||
isError={Boolean(error)}
|
})}
|
||||||
errorText={error?.message}
|
>
|
||||||
>
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
<Select
|
<TabList>
|
||||||
defaultValue={field.value}
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
{...field}
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
onValueChange={(e) => {
|
</TabList>
|
||||||
if (e === IdentityJwtConfigurationType.JWKS) {
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="jwksUrl"
|
name="configurationType"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
isRequired
|
label="Configuration Type"
|
||||||
label="JWKS URL"
|
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
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: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Add Public Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="boundIssuer"
|
||||||
|
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 } }) => (
|
||||||
|
<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" />
|
<Input {...field} type="text" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -332,58 +455,79 @@ export const IdentityJwtAuthForm = ({
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="jwksCaCert"
|
name="boundAudiences"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="JWKS CA Certificate"
|
label="Audiences"
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
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>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
<Input {...field} type="text" placeholder="service1, service2" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
{boundClaimsFields.map(({ id }, index) => (
|
||||||
)}
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
|
||||||
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
|
|
||||||
<>
|
|
||||||
{publicKeyFields.map(({ id }, index) => (
|
|
||||||
<div key={id} className="flex gap-2">
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`publicKeys.${index}.value`}
|
name={`boundClaims.${index}.key`}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
className="flex-grow"
|
<FormControl
|
||||||
label={`Public Key ${index + 1}`}
|
className="mb-0 flex-grow"
|
||||||
errorText={error?.message}
|
label={index === 0 ? "Claims" : undefined}
|
||||||
isError={Boolean(error)}
|
icon={
|
||||||
icon={
|
index === 0 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="text-center"
|
className="text-center"
|
||||||
content={<span>This field only accepts PEM-formatted public keys</span>}
|
content={<span>This field supports glob patterns</span>}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
) : undefined
|
||||||
>
|
}
|
||||||
<TextArea {...field} placeholder="-----BEGIN PUBLIC KEY----- ..." />
|
isError={Boolean(error)}
|
||||||
</FormControl>
|
errorText={error?.message}
|
||||||
)}
|
>
|
||||||
/>
|
<Input
|
||||||
<IconButton
|
value={field.value}
|
||||||
onClick={() => {
|
onChange={(e) => field.onChange(e)}
|
||||||
if (publicKeyFields.length === 1) {
|
placeholder="property"
|
||||||
createNotification({
|
/>
|
||||||
type: "error",
|
</FormControl>
|
||||||
text: "A public key is required for static configurations"
|
);
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
removePublicKeyFields(index);
|
|
||||||
}}
|
}}
|
||||||
|
/>
|
||||||
|
<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"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@ -398,291 +542,150 @@ export const IdentityJwtAuthForm = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
appendPublicKeyFields({
|
appendBoundClaimField({
|
||||||
|
key: "",
|
||||||
value: ""
|
value: ""
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
Add Public Key
|
Add Claims
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="boundIssuer"
|
|
||||||
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 } }) => (
|
|
||||||
<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={`boundClaims.${index}.key`}
|
defaultValue="2592000"
|
||||||
render={({ field, fieldState: { error } }) => {
|
name="accessTokenTTL"
|
||||||
return (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
className="mb-0 flex-grow"
|
label="Access Token TTL (seconds)"
|
||||||
label={index === 0 ? "Claims" : undefined}
|
isError={Boolean(error)}
|
||||||
icon={
|
errorText={error?.message}
|
||||||
index === 0 ? (
|
>
|
||||||
<Tooltip
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
className="text-center"
|
</FormControl>
|
||||||
content={<span>This field supports glob patterns</span>}
|
)}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
<Controller
|
||||||
</Tooltip>
|
control={control}
|
||||||
) : undefined
|
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;
|
||||||
}
|
}
|
||||||
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
|
handlePopUpOpen("upgradePlan");
|
||||||
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
|
||||||
variant="outline_bg"
|
className="mr-4"
|
||||||
onClick={() =>
|
size="sm"
|
||||||
appendBoundClaimField({
|
type="submit"
|
||||||
key: "",
|
isLoading={isSubmitting}
|
||||||
value: ""
|
isDisabled={isSubmitting}
|
||||||
})
|
|
||||||
}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add Claims
|
{isUpdate ? "Update" : "Create"}
|
||||||
</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
|
||||||
variant="outline_bg"
|
colorSchema="secondary"
|
||||||
onClick={() => {
|
variant="plain"
|
||||||
if (subscription?.ipAllowlisting) {
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
Cancel
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
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,17 +6,28 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
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),
|
||||||
@ -47,21 +58,18 @@ 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", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityKubernetesAuthForm = ({
|
export const IdentityKubernetesAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -69,11 +77,9 @@ 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 isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityKubernetesAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -154,7 +160,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@ -165,7 +171,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
allowedNamespaces,
|
allowedNamespaces,
|
||||||
allowedAudience,
|
allowedAudience,
|
||||||
caCert,
|
caCert,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -174,7 +180,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
kubernetesHost: kubernetesHost || "",
|
kubernetesHost: kubernetesHost || "",
|
||||||
tokenReviewerJwt,
|
tokenReviewerJwt,
|
||||||
allowedNames: allowedNames || "",
|
allowedNames: allowedNames || "",
|
||||||
@ -205,241 +211,254 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form
|
||||||
<Controller
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
control={control}
|
setTabValue(
|
||||||
defaultValue="2592000"
|
[
|
||||||
name="kubernetesHost"
|
"kubernetesHost",
|
||||||
render={({ field, fieldState: { error } }) => (
|
"tokenReviewerJwt",
|
||||||
<FormControl
|
"accessTokenTTL",
|
||||||
label="Kubernetes Host / Base Kubernetes API URL "
|
"accessTokenMaxTTL",
|
||||||
isError={Boolean(error)}
|
"accessTokenNumUsesLimit",
|
||||||
errorText={error?.message}
|
"allowedNames",
|
||||||
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'"
|
"allowedNamespaces"
|
||||||
isRequired
|
].includes(Object.keys(fields)[0])
|
||||||
>
|
? IdentityFormTab.Configuration
|
||||||
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
|
: IdentityFormTab.Advanced
|
||||||
</FormControl>
|
);
|
||||||
)}
|
})}
|
||||||
/>
|
>
|
||||||
<Controller
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
control={control}
|
<TabList>
|
||||||
name="tokenReviewerJwt"
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
render={({ field, fieldState: { error } }) => (
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
<FormControl
|
</TabList>
|
||||||
label="Token Reviewer JWT"
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
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}
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
defaultValue="2592000"
|
||||||
defaultValue="0.0.0.0/0"
|
name="kubernetesHost"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
<FormControl
|
||||||
<FormControl
|
label="Kubernetes Host / Base Kubernetes API URL "
|
||||||
className="mb-0 flex-grow"
|
isError={Boolean(error)}
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'"
|
||||||
errorText={error?.message}
|
isRequired
|
||||||
tooltipText="The 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" />
|
||||||
<Input
|
</FormControl>
|
||||||
value={field.value}
|
)}
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<Controller
|
||||||
onClick={() => {
|
control={control}
|
||||||
if (subscription?.ipAllowlisting) {
|
name="tokenReviewerJwt"
|
||||||
removeAccessTokenTrustedIp(index);
|
render={({ field, fieldState: { error } }) => (
|
||||||
return;
|
<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");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
placeholder="123.456.789.0"
|
||||||
colorSchema="danger"
|
/>
|
||||||
variant="plain"
|
</FormControl>
|
||||||
ariaLabel="update"
|
);
|
||||||
className="p-3"
|
}}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => {
|
||||||
</div>
|
if (subscription?.ipAllowlisting) {
|
||||||
))}
|
removeAccessTokenTrustedIp(index);
|
||||||
<div className="my-4 ml-1">
|
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
|
||||||
variant="outline_bg"
|
className="mr-4"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
if (subscription?.ipAllowlisting) {
|
type="submit"
|
||||||
appendAccessTokenTrustedIp({
|
isLoading={isSubmitting}
|
||||||
ipAddress: "0.0.0.0/0"
|
isDisabled={isSubmitting}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</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
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } 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,14 +7,26 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
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(
|
||||||
@ -48,21 +60,18 @@ 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", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityOidcAuthForm = ({
|
export const IdentityOidcAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -70,11 +79,9 @@ 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 isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityOidcAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -160,13 +167,13 @@ export const IdentityOidcAuthForm = ({
|
|||||||
boundSubject
|
boundSubject
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) {
|
if (!identityId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
oidcDiscoveryUrl,
|
oidcDiscoveryUrl,
|
||||||
caCert,
|
caCert,
|
||||||
@ -181,7 +188,7 @@ export const IdentityOidcAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
oidcDiscoveryUrl,
|
oidcDiscoveryUrl,
|
||||||
caCert,
|
caCert,
|
||||||
boundIssuer,
|
boundIssuer,
|
||||||
@ -213,314 +220,323 @@ export const IdentityOidcAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form
|
||||||
<Controller
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
control={control}
|
setTabValue(
|
||||||
name="oidcDiscoveryUrl"
|
["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0])
|
||||||
render={({ field, fieldState: { error } }) => (
|
? IdentityFormTab.Advanced
|
||||||
<FormControl
|
: IdentityFormTab.Configuration
|
||||||
isRequired
|
);
|
||||||
label="OIDC Discovery URL"
|
})}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
>
|
<TabList>
|
||||||
<Input
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
{...field}
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
placeholder="https://token.actions.githubusercontent.com"
|
</TabList>
|
||||||
type="text"
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
/>
|
|
||||||
</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={`boundClaims.${index}.key`}
|
name="oidcDiscoveryUrl"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
<FormControl
|
||||||
<FormControl
|
isRequired
|
||||||
className="mb-0 flex-grow"
|
label="OIDC Discovery URL"
|
||||||
label={index === 0 ? "Claims" : undefined}
|
isError={Boolean(error)}
|
||||||
icon={
|
errorText={error?.message}
|
||||||
index === 0 ? (
|
>
|
||||||
<Tooltip
|
<Input
|
||||||
className="text-center"
|
{...field}
|
||||||
content={<span>This field supports glob patterns</span>}
|
placeholder="https://token.actions.githubusercontent.com"
|
||||||
>
|
type="text"
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
/>
|
||||||
</Tooltip>
|
</FormControl>
|
||||||
) : undefined
|
)}
|
||||||
}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => field.onChange(e)}
|
|
||||||
placeholder="property"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`boundClaims.${index}.value`}
|
name="boundIssuer"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
<FormControl
|
||||||
<FormControl
|
isRequired
|
||||||
className="mb-0 flex-grow"
|
label="Issuer"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
value={field.value}
|
{...field}
|
||||||
onChange={(e) => field.onChange(e)}
|
type="text"
|
||||||
placeholder="value1, value2"
|
placeholder="https://token.actions.githubusercontent.com"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</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>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenMaxTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
name="boundSubject"
|
||||||
defaultValue="0.0.0.0/0"
|
render={({ field, fieldState: { error } }) => (
|
||||||
render={({ field, fieldState: { error } }) => {
|
<FormControl
|
||||||
return (
|
label="Subject"
|
||||||
<FormControl
|
isError={Boolean(error)}
|
||||||
className="mb-0 flex-grow"
|
errorText={error?.message}
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
icon={
|
||||||
isError={Boolean(error)}
|
<Tooltip
|
||||||
errorText={error?.message}
|
className="text-center"
|
||||||
>
|
content={<span>This field supports glob patterns</span>}
|
||||||
<Input
|
>
|
||||||
value={field.value}
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
onChange={(e) => {
|
</Tooltip>
|
||||||
if (subscription?.ipAllowlisting) {
|
}
|
||||||
field.onChange(e);
|
>
|
||||||
return;
|
<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>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
<IconButton
|
||||||
}}
|
onClick={() => removeBoundClaimField(index)}
|
||||||
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>
|
||||||
removeAccessTokenTrustedIp(index);
|
))}
|
||||||
return;
|
<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");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
placeholder="123.456.789.0"
|
||||||
colorSchema="danger"
|
/>
|
||||||
variant="plain"
|
</FormControl>
|
||||||
ariaLabel="update"
|
);
|
||||||
className="p-3"
|
}}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => {
|
||||||
</div>
|
if (subscription?.ipAllowlisting) {
|
||||||
))}
|
removeAccessTokenTrustedIp(index);
|
||||||
<div className="my-4 ml-1">
|
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
|
||||||
variant="outline_bg"
|
className="mr-4"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
if (subscription?.ipAllowlisting) {
|
type="submit"
|
||||||
appendAccessTokenTrustedIp({
|
isLoading={isSubmitting}
|
||||||
ipAddress: "0.0.0.0/0"
|
isDisabled={isSubmitting}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</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
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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";
|
||||||
@ -5,16 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
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 { IdentityAuthMethod } from "@app/hooks/api/identities";
|
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.string().refine((val) => Number(val) <= 315360000, {
|
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||||
@ -39,21 +51,18 @@ 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", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData?: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityTokenAuthForm = ({
|
export const IdentityTokenAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -61,12 +70,9 @@ 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 isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityTokenAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,6 +97,30 @@ 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,
|
||||||
@ -98,12 +128,12 @@ export const IdentityTokenAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -112,7 +142,7 @@ export const IdentityTokenAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -137,148 +167,153 @@ export const IdentityTokenAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form
|
||||||
<Controller
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
control={control}
|
setTabValue(
|
||||||
defaultValue="2592000"
|
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
||||||
name="accessTokenTTL"
|
? IdentityFormTab.Advanced
|
||||||
render={({ field, fieldState: { error } }) => (
|
: IdentityFormTab.Configuration
|
||||||
<FormControl
|
);
|
||||||
label="Access Token TTL (seconds)"
|
})}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
>
|
<TabList>
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
</FormControl>
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
)}
|
</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}
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
defaultValue="2592000"
|
||||||
defaultValue="0.0.0.0/0"
|
name="accessTokenTTL"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
<FormControl
|
||||||
<FormControl
|
label="Access Token TTL (seconds)"
|
||||||
className="mb-0 flex-grow"
|
isError={Boolean(error)}
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
>
|
</FormControl>
|
||||||
<Input
|
)}
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<Controller
|
||||||
onClick={() => {
|
control={control}
|
||||||
if (subscription?.ipAllowlisting) {
|
defaultValue="2592000"
|
||||||
removeAccessTokenTrustedIp(index);
|
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");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
placeholder="123.456.789.0"
|
||||||
colorSchema="danger"
|
/>
|
||||||
variant="plain"
|
</FormControl>
|
||||||
ariaLabel="update"
|
);
|
||||||
className="p-3"
|
}}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => {
|
||||||
</div>
|
if (subscription?.ipAllowlisting) {
|
||||||
))}
|
removeAccessTokenTrustedIp(index);
|
||||||
<div className="my-4 ml-1">
|
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
|
||||||
variant="outline_bg"
|
className="mr-4"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
if (subscription?.ipAllowlisting) {
|
type="submit"
|
||||||
appendAccessTokenTrustedIp({
|
isLoading={isSubmitting}
|
||||||
ipAddress: "0.0.0.0/0"
|
isDisabled={isSubmitting}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</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
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -1,344 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
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,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
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
|
||||||
@ -52,33 +62,27 @@ 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", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData?: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityUniversalAuthForm = ({
|
export const IdentityUniversalAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
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 isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityUniversalAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -149,13 +153,13 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// update universal auth configuration
|
// update universal auth configuration
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
clientSecretTrustedIps,
|
clientSecretTrustedIps,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
@ -167,7 +171,7 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
|
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
clientSecretTrustedIps,
|
clientSecretTrustedIps,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
@ -195,215 +199,220 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form
|
||||||
<Controller
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
control={control}
|
setTabValue(
|
||||||
defaultValue="2592000"
|
["accessTokenTrustedIps", "clientSecretTrustedIps"].includes(Object.keys(fields)[0])
|
||||||
name="accessTokenTTL"
|
? IdentityFormTab.Advanced
|
||||||
render={({ field, fieldState: { error } }) => (
|
: IdentityFormTab.Configuration
|
||||||
<FormControl
|
);
|
||||||
label="Access Token TTL (seconds)"
|
})}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
>
|
<TabList>
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
</FormControl>
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
)}
|
</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}
|
||||||
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
defaultValue="2592000"
|
||||||
defaultValue="0.0.0.0/0"
|
name="accessTokenTTL"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
<FormControl
|
||||||
<FormControl
|
label="Access Token TTL (seconds)"
|
||||||
className="mb-0 flex-grow"
|
isError={Boolean(error)}
|
||||||
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
>
|
</FormControl>
|
||||||
<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) {
|
|
||||||
removeClientSecretTrustedIp(index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="my-4 ml-1">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
defaultValue="2592000"
|
||||||
defaultValue="0.0.0.0/0"
|
name="accessTokenMaxTTL"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
<FormControl
|
||||||
<FormControl
|
label="Access Token Max TTL (seconds)"
|
||||||
className="mb-0 flex-grow"
|
isError={Boolean(error)}
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
>
|
||||||
errorText={error?.message}
|
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||||
>
|
</FormControl>
|
||||||
<Input
|
)}
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<Controller
|
||||||
onClick={() => {
|
control={control}
|
||||||
if (subscription?.ipAllowlisting) {
|
defaultValue="0"
|
||||||
removeAccessTokenTrustedIp(index);
|
name="accessTokenNumUsesLimit"
|
||||||
return;
|
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");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
placeholder="123.456.789.0"
|
||||||
colorSchema="danger"
|
/>
|
||||||
variant="plain"
|
</FormControl>
|
||||||
ariaLabel="update"
|
);
|
||||||
className="p-3"
|
}}
|
||||||
>
|
/>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => {
|
||||||
</div>
|
if (subscription?.ipAllowlisting) {
|
||||||
))}
|
removeClientSecretTrustedIp(index);
|
||||||
<div className="my-4 ml-1">
|
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) {
|
||||||
|
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
|
||||||
variant="outline_bg"
|
className="mr-4"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
if (subscription?.ipAllowlisting) {
|
type="submit"
|
||||||
appendAccessTokenTrustedIp({
|
isLoading={isSubmitting}
|
||||||
ipAddress: "0.0.0.0/0"
|
isDisabled={isSubmitting}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
{isUpdate ? "Update" : "Add"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum IdentityFormTab {
|
||||||
|
Advanced = "advanced",
|
||||||
|
Configuration = "configuration"
|
||||||
|
}
|
@ -1,45 +1,24 @@
|
|||||||
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 {
|
import { DeleteActionModal, PageHeader } from "@app/components/v2";
|
||||||
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 {
|
import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api";
|
||||||
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 = () => {
|
||||||
@ -52,25 +31,13 @@ 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",
|
||||||
"revokeAuthMethod",
|
"upgradePlan",
|
||||||
"token",
|
"viewAuthMethod"
|
||||||
"tokenList",
|
|
||||||
"revokeToken",
|
|
||||||
"clientSecret",
|
|
||||||
"revokeClientSecret",
|
|
||||||
"universalAuthClientSecret", // list of client secrets
|
|
||||||
"upgradePlan"
|
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const onDeleteIdentitySubmit = async (id: string) => {
|
const onDeleteIdentitySubmit = async (id: string) => {
|
||||||
@ -104,148 +71,15 @@ 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}
|
||||||
/>
|
/>
|
||||||
@ -260,18 +94,6 @@ 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)}
|
||||||
@ -290,41 +112,11 @@ const Page = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<ViewIdentityAuthModal
|
||||||
isOpen={popUp.revokeToken.isOpen}
|
isOpen={popUp.viewAuthMethod.isOpen}
|
||||||
title={`Are you sure want to revoke ${
|
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
|
||||||
(popUp?.revokeToken?.data as { name: string })?.name || ""
|
authMethod={popUp.viewAuthMethod.data}
|
||||||
}?`}
|
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>
|
||||||
);
|
);
|
||||||
|
@ -1,155 +1,73 @@
|
|||||||
import { useEffect } from "react";
|
import { faCog, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
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, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { useGetIdentityById } from "@app/hooks/api";
|
import { IdentityAuthMethod, identityAuthToNameMap, 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<
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "viewAuthMethod"]>,
|
||||||
[
|
data?: object | IdentityAuthMethod
|
||||||
"clientSecret",
|
|
||||||
"identityAuthMethod",
|
|
||||||
"revokeClientSecret",
|
|
||||||
"token",
|
|
||||||
"revokeToken",
|
|
||||||
"universalAuthClientSecret",
|
|
||||||
"tokenList"
|
|
||||||
]
|
|
||||||
>,
|
|
||||||
data?: object
|
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAuthenticationSection = ({
|
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
|
||||||
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">
|
||||||
<div className="py-4">
|
{data.identity.authMethods.map((authMethod) => (
|
||||||
<div className="flex justify-between">
|
<button
|
||||||
<p className="mb-0.5 ml-px text-sm font-semibold text-mineshaft-300">Auth Method</p>
|
key={authMethod}
|
||||||
</div>
|
onClick={() => handlePopUpOpen("viewAuthMethod", authMethod)}
|
||||||
<div className="flex items-center gap-2">
|
type="button"
|
||||||
<div className="w-full">
|
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"
|
||||||
<Select
|
>
|
||||||
className="w-full"
|
<span>{identityAuthToNameMap[authMethod]}</span>
|
||||||
value={selectedAuthMethod as string}
|
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
|
||||||
onValueChange={(value) => setSelectedAuthMethod(value as IdentityAuthMethod)}
|
</button>
|
||||||
>
|
))}
|
||||||
{(data.identity?.authMethods || []).map((authMethod) => (
|
</div>
|
||||||
<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 />
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { faEllipsis, faKey } from "@fortawesome/free-solid-svg-icons";
|
import { faEllipsisVertical, 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,6 +8,7 @@ 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";
|
||||||
@ -27,10 +28,13 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{tokens?.length ? (
|
{tokens?.length ? (
|
||||||
<div className="flex justify-between">
|
<div className="flex items-center justify-between border-b border-bunker-400 pb-1">
|
||||||
<p className="text-sm font-semibold text-mineshaft-300">{`Access Tokens (${tokens.length})`}</p>
|
<p className="text-sm font-medium text-bunker-300">{`Access Tokens (${tokens.length})`}</p>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
size="xs"
|
||||||
|
className="underline"
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("tokenList", {
|
handlePopUpOpen("tokenList", {
|
||||||
identityId,
|
identityId,
|
||||||
@ -50,16 +54,16 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group flex items-center justify-between py-2 last:pb-0"
|
className="group flex items-center justify-between border-b border-mineshaft-500 px-2 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="1x" icon={faKey} />
|
<FontAwesomeIcon size="xs" className="text-mineshaft-400" icon={faKey} />
|
||||||
<div className="ml-4">
|
<div className="ml-3">
|
||||||
<p className="text-sm font-semibold text-mineshaft-300">
|
<p className="text-sm font-medium text-mineshaft-300">
|
||||||
{token.name ? token.name : "-"}
|
{token.name ? token.name : "-"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-mineshaft-300">
|
<p className="text-xs text-mineshaft-400">
|
||||||
{token.isAccessTokenRevoked
|
{token.isAccessTokenRevoked
|
||||||
? "Revoked"
|
? "Revoked"
|
||||||
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
|
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
|
||||||
@ -67,14 +71,19 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="opacity-0 transition-opacity duration-300 hover:text-primary-400 group-hover:opacity-100 data-[state=open]:text-primary-400">
|
<Tooltip side="right" content="More options">
|
||||||
<Tooltip content="More options">
|
<IconButton
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
colorSchema="secondary"
|
||||||
</Tooltip>
|
variant="plain"
|
||||||
</div>
|
size="xs"
|
||||||
|
ariaLabel="More options"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent align="start" className="z-[101] p-1">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handlePopUpOpen("token", {
|
handlePopUpOpen("token", {
|
||||||
@ -106,8 +115,9 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
})}
|
})}
|
||||||
<Button
|
<Button
|
||||||
className="mr-4 mt-4 w-full"
|
className="mr-4 mt-4 w-full"
|
||||||
colorSchema="primary"
|
colorSchema="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("token", {
|
handlePopUpOpen("token", {
|
||||||
identityId
|
identityId
|
||||||
|
@ -1,8 +1,25 @@
|
|||||||
import { faCheck, faCopy, faKey, faPencil } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
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 { IconButton, Tag, Tooltip } from "@app/components/v2";
|
import {
|
||||||
|
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";
|
||||||
@ -11,7 +28,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
type Props = {
|
type Props = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "token", "clientSecret"]>,
|
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "deleteIdentity"]>,
|
||||||
data?: object
|
data?: object
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
@ -26,31 +43,61 @@ 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>
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
<DropdownMenu>
|
||||||
{(isAllowed) => {
|
<DropdownMenuTrigger asChild>
|
||||||
return (
|
<Button
|
||||||
<Tooltip content="Edit Identity">
|
size="xs"
|
||||||
<IconButton
|
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
|
||||||
isDisabled={!isAllowed}
|
colorSchema="secondary"
|
||||||
ariaLabel="copy icon"
|
>
|
||||||
variant="plain"
|
Options
|
||||||
className="group relative"
|
</Button>
|
||||||
onClick={() => {
|
</DropdownMenuTrigger>
|
||||||
|
<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}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
Edit Identity
|
||||||
</IconButton>
|
</DropdownMenuItem>
|
||||||
</Tooltip>
|
)}
|
||||||
);
|
</OrgPermissionCan>
|
||||||
}}
|
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Identity}>
|
||||||
</OrgPermissionCan>
|
{(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
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
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">
|
||||||
|
@ -1,270 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,20 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,239 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,196 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,174 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,79 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,76 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,69 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,89 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,152 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,121 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,116 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,106 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./ViewIdentityAuthModal";
|
@ -0,0 +1,12 @@
|
|||||||
|
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"]>;
|
||||||
|
};
|
@ -2,5 +2,4 @@ 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";
|
||||||
|
Reference in New Issue
Block a user