mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
2 Commits
identity-a
...
daniel/upd
Author | SHA1 | Date | |
---|---|---|---|
b949708f45 | |||
2a6b6b03b9 |
@ -5,7 +5,7 @@ title: "Python"
|
|||||||
This guide demonstrates how to use Infisical to manage secrets for your Python stack from local development to production. It uses:
|
This guide demonstrates how to use Infisical to manage secrets for your Python stack from local development to production. It uses:
|
||||||
|
|
||||||
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets.
|
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets.
|
||||||
- The [infisical-python](https://pypi.org/project/infisical-python/) Python client SDK to fetch secrets back to your Python application on demand.
|
- The [infisicalsdk](https://pypi.org/project/infisicalsdk/) Python client SDK to fetch secrets back to your Python application on demand.
|
||||||
|
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
@ -36,40 +36,38 @@ python3 -m venv env
|
|||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
Install Flask and [infisical-python](https://pypi.org/project/infisical-python/), the client Python SDK for Infisical.
|
Install Flask and [infisicalsdk](https://pypi.org/project/infisicalsdk/), the client Python SDK for Infisical.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
pip install flask infisical-python
|
pip install flask infisicalsdk
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, create an `app.py` file containing the application code.
|
Finally, create an `app.py` file containing the application code.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions, AuthenticationOptions, UniversalAuthMethod
|
from infisical_sdk import InfisicalSDKClient
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
client = InfisicalClient(ClientSettings(
|
client = InfisicalSDKClient(host="https://app.infisical.com") # host is optional, defaults to https://app.infisical.com
|
||||||
auth=AuthenticationOptions(
|
|
||||||
universal_auth=UniversalAuthMethod(
|
client.auth.universal_auth.login(
|
||||||
client_id="CLIENT_ID",
|
"<machine-identity-client-id>",
|
||||||
client_secret="CLIENT_SECRET",
|
"<machine-identity-client-secret>"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
))
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def hello_world():
|
def hello_world():
|
||||||
# access value
|
# access value
|
||||||
|
name = client.secrets.get_secret_by_name(
|
||||||
|
secret_name="NAME",
|
||||||
|
project_id="<project-id>",
|
||||||
|
environment_slug="dev",
|
||||||
|
secret_path="/"
|
||||||
|
)
|
||||||
|
|
||||||
name = client.getSecret(options=GetSecretOptions(
|
return f"Hello! My name is: {name.secretValue}"
|
||||||
environment="dev",
|
|
||||||
project_id="PROJECT_ID",
|
|
||||||
secret_name="NAME"
|
|
||||||
))
|
|
||||||
|
|
||||||
return f"Hello! My name is: {name.secret_value}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Here, we initialized a `client` instance of the Infisical Python SDK with the Infisical Token
|
Here, we initialized a `client` instance of the Infisical Python SDK with the Infisical Token
|
||||||
@ -89,15 +87,6 @@ At this stage, you know how to fetch secrets from Infisical back to your Python
|
|||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="Isn't it inefficient if my app makes a request every time it needs a secret?">
|
|
||||||
The client SDK caches every secret and implements a 5-minute waiting period before
|
|
||||||
re-requesting it. The waiting period can be controlled by setting the `cacheTTL` parameter at
|
|
||||||
the time of initializing the client.
|
|
||||||
</Accordion>
|
|
||||||
<Accordion title="What if a request for a secret fails?">
|
|
||||||
The SDK caches every secret and falls back to the cached value if a request fails. If no cached
|
|
||||||
value ever-existed, the SDK falls back to whatever value is on `process.env`.
|
|
||||||
</Accordion>
|
|
||||||
<Accordion title="What's the point if I still have to manage a token for the SDK?">
|
<Accordion title="What's the point if I still have to manage a token for the SDK?">
|
||||||
The token enables the SDK to authenticate with Infisical to fetch back your secrets.
|
The token enables the SDK to authenticate with Infisical to fetch back your secrets.
|
||||||
Although the SDK requires you to pass in a token, it enables greater efficiency and security
|
Although the SDK requires you to pass in a token, it enables greater efficiency and security
|
||||||
@ -114,6 +103,6 @@ At this stage, you know how to fetch secrets from Infisical back to your Python
|
|||||||
|
|
||||||
See also:
|
See also:
|
||||||
|
|
||||||
- Explore the [Python SDK](https://github.com/Infisical/sdk/tree/main/crates/infisical-py)
|
- Explore the [Python SDK](https://github.com/Infisical/python-sdk-official)
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO."
|
|||||||
|
|
||||||
Back in the **Set up Single Sign-On with SAML** screen, select **Edit** in the **Attributes & Claims** section and configure the following map:
|
Back in the **Set up Single Sign-On with SAML** screen, select **Edit** in the **Attributes & Claims** section and configure the following map:
|
||||||
|
|
||||||
- `email -> user.userprinciplename`
|
- `email -> user.userprincipalname`
|
||||||
- `firstName -> user.givenname`
|
- `firstName -> user.givenname`
|
||||||
- `lastName -> user.surname`
|
- `lastName -> user.surname`
|
||||||
|
|
||||||
|
@ -923,7 +923,7 @@ export const useAddIdentityTokenAuth = () => {
|
|||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: identitiesKeys.getIdentityTokenAuth(identityId)
|
queryKey: identitiesKeys.getIdentityUniversalAuth(identityId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -959,7 +959,7 @@ export const useUpdateIdentityTokenAuth = () => {
|
|||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: identitiesKeys.getIdentityTokenAuth(identityId)
|
queryKey: identitiesKeys.getIdentityUniversalAuth(identityId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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"]>;
|
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
@ -34,7 +34,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
|||||||
title={
|
title={
|
||||||
isSelectedAuthAlreadyConfigured
|
isSelectedAuthAlreadyConfigured
|
||||||
? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
||||||
: `Add ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
: `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IdentityAuthMethodModalContent
|
<IdentityAuthMethodModalContent
|
||||||
|
@ -4,8 +4,30 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { Badge, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
import {
|
||||||
|
Badge,
|
||||||
|
DeleteActionModal,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import {
|
||||||
|
useDeleteIdentityAwsAuth,
|
||||||
|
useDeleteIdentityAzureAuth,
|
||||||
|
useDeleteIdentityGcpAuth,
|
||||||
|
useDeleteIdentityKubernetesAuth,
|
||||||
|
useDeleteIdentityOidcAuth,
|
||||||
|
useDeleteIdentityTokenAuth,
|
||||||
|
useDeleteIdentityUniversalAuth
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
IdentityAuthMethod,
|
||||||
|
identityAuthToNameMap,
|
||||||
|
useDeleteIdentityJwtAuth
|
||||||
|
} from "@app/hooks/api/identities";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
||||||
@ -18,10 +40,10 @@ import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
|
|||||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>;
|
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
@ -34,7 +56,13 @@ type Props = {
|
|||||||
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
|
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TRevokeOptions = {
|
||||||
|
identityId: string;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
type TRevokeMethods = {
|
type TRevokeMethods = {
|
||||||
|
revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
|
||||||
render: () => JSX.Element;
|
render: () => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,6 +96,18 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
initialAuthMethod,
|
initialAuthMethod,
|
||||||
setSelectedAuthMethod
|
setSelectedAuthMethod
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const orgId = currentOrg?.id || "";
|
||||||
|
|
||||||
|
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
|
||||||
|
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
|
||||||
|
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
|
||||||
|
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
|
||||||
|
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
|
||||||
|
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
|
||||||
|
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
|
||||||
|
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
|
||||||
|
|
||||||
const { control, watch } = useForm<FormData>({
|
const { control, watch } = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: async () => {
|
defaultValues: async () => {
|
||||||
@ -109,9 +149,10 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
|
|
||||||
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
|
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
|
||||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: {
|
[IdentityAuthMethod.UNIVERSAL_AUTH]: {
|
||||||
|
revokeMethod: revokeUniversalAuth,
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityUniversalAuthForm
|
<IdentityUniversalAuthForm
|
||||||
identityId={identityAuthMethodData.identityId}
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -119,9 +160,10 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.OIDC_AUTH]: {
|
[IdentityAuthMethod.OIDC_AUTH]: {
|
||||||
|
revokeMethod: revokeOidcAuth,
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityOidcAuthForm
|
<IdentityOidcAuthForm
|
||||||
identityId={identityAuthMethodData.identityId}
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -129,9 +171,10 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.TOKEN_AUTH]: {
|
[IdentityAuthMethod.TOKEN_AUTH]: {
|
||||||
|
revokeMethod: revokeTokenAuth,
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityTokenAuthForm
|
<IdentityTokenAuthForm
|
||||||
identityId={identityAuthMethodData.identityId}
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -139,9 +182,10 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.AZURE_AUTH]: {
|
[IdentityAuthMethod.AZURE_AUTH]: {
|
||||||
|
revokeMethod: revokeAzureAuth,
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityAzureAuthForm
|
<IdentityAzureAuthForm
|
||||||
identityId={identityAuthMethodData.identityId}
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -149,9 +193,10 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.GCP_AUTH]: {
|
[IdentityAuthMethod.GCP_AUTH]: {
|
||||||
|
revokeMethod: revokeGcpAuth,
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityGcpAuthForm
|
<IdentityGcpAuthForm
|
||||||
identityId={identityAuthMethodData.identityId}
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -159,9 +204,10 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.KUBERNETES_AUTH]: {
|
[IdentityAuthMethod.KUBERNETES_AUTH]: {
|
||||||
|
revokeMethod: revokeKubernetesAuth,
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityKubernetesAuthForm
|
<IdentityKubernetesAuthForm
|
||||||
identityId={identityAuthMethodData.identityId}
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -169,9 +215,10 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.AWS_AUTH]: {
|
[IdentityAuthMethod.AWS_AUTH]: {
|
||||||
|
revokeMethod: revokeAwsAuth,
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityAwsAuthForm
|
<IdentityAwsAuthForm
|
||||||
identityId={identityAuthMethodData.identityId}
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -179,9 +226,10 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.JWT_AUTH]: {
|
[IdentityAuthMethod.JWT_AUTH]: {
|
||||||
|
revokeMethod: revokeJwtAuth,
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityJwtAuthForm
|
<IdentityJwtAuthForm
|
||||||
identityId={identityAuthMethodData.identityId}
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -246,6 +294,42 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
||||||
/>
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
||||||
|
title={`Are you sure want to remove ${
|
||||||
|
identityAuthMethodData?.authMethod
|
||||||
|
? identityAuthToNameMap[identityAuthMethodData.authMethod]
|
||||||
|
: "the auth method"
|
||||||
|
} on ${identityAuthMethodData?.name ?? ""}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
buttonText="Remove"
|
||||||
|
onDeleteApproved={async () => {
|
||||||
|
if (!identityAuthMethodData.authMethod || !orgId || !selectedMethodItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await selectedMethodItem.revokeMethod({
|
||||||
|
identityId: identityAuthMethodData.identityId,
|
||||||
|
organizationId: orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully removed auth method",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpToggle("revokeAuthMethod", false);
|
||||||
|
handlePopUpToggle("identityAuthMethod", false);
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove auth method",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,27 +6,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityAwsAuth,
|
useAddIdentityAwsAuth,
|
||||||
useGetIdentityAwsAuth,
|
useGetIdentityAwsAuth,
|
||||||
useUpdateIdentityAwsAuth
|
useUpdateIdentityAwsAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
stsEndpoint: z.string(),
|
stsEndpoint: z.string(),
|
||||||
@ -59,18 +49,21 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityId?: string;
|
identityAuthMethodData: {
|
||||||
isUpdate?: boolean;
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
configuredAuthMethods?: IdentityAuthMethod[];
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAwsAuthForm = ({
|
export const IdentityAwsAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityId,
|
identityAuthMethodData
|
||||||
isUpdate
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -78,9 +71,11 @@ export const IdentityAwsAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityAwsAuth(identityId ?? "", {
|
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||||
|
identityAuthMethodData.authMethod! || ""
|
||||||
|
);
|
||||||
|
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -148,7 +143,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) return;
|
if (!identityAuthMethodData) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@ -156,7 +151,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
stsEndpoint,
|
stsEndpoint,
|
||||||
allowedPrincipalArns,
|
allowedPrincipalArns,
|
||||||
allowedAccountIds,
|
allowedAccountIds,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -165,7 +160,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
stsEndpoint: stsEndpoint || "",
|
stsEndpoint: stsEndpoint || "",
|
||||||
allowedPrincipalArns: allowedPrincipalArns || "",
|
allowedPrincipalArns: allowedPrincipalArns || "",
|
||||||
allowedAccountIds: allowedAccountIds || "",
|
allowedAccountIds: allowedAccountIds || "",
|
||||||
@ -193,195 +188,190 @@ export const IdentityAwsAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
<Controller
|
||||||
setTabValue(
|
control={control}
|
||||||
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
defaultValue="2592000"
|
||||||
? IdentityFormTab.Advanced
|
name="allowedPrincipalArns"
|
||||||
: IdentityFormTab.Configuration
|
render={({ field, fieldState: { error } }) => (
|
||||||
);
|
<FormControl
|
||||||
})}
|
label="Allowed Principal ARNs"
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
errorText={error?.message}
|
||||||
<TabList>
|
>
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
<Input
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
{...field}
|
||||||
</TabList>
|
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
type="text"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedAccountIds"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Account IDs"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="123456789012, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="https://sts.amazonaws.com/"
|
||||||
|
name="stsEndpoint"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
name="allowedPrincipalArns"
|
defaultValue="0.0.0.0/0"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
label="Allowed Principal ARNs"
|
<FormControl
|
||||||
isError={Boolean(error)}
|
className="mb-0 flex-grow"
|
||||||
errorText={error?.message}
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Input
|
errorText={error?.message}
|
||||||
{...field}
|
>
|
||||||
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
<Input
|
||||||
type="text"
|
value={field.value}
|
||||||
/>
|
onChange={(e) => {
|
||||||
</FormControl>
|
if (subscription?.ipAllowlisting) {
|
||||||
)}
|
field.onChange(e);
|
||||||
/>
|
return;
|
||||||
<Controller
|
}
|
||||||
control={control}
|
|
||||||
name="allowedAccountIds"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Allowed Account IDs"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="123456789012, ..." />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="https://sts.amazonaws.com/"
|
|
||||||
name="stsEndpoint"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenMaxTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
placeholder="123.456.789.0"
|
placeholder="123.456.789.0"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (subscription?.ipAllowlisting) {
|
if (subscription?.ipAllowlisting) {
|
||||||
removeAccessTokenTrustedIp(index);
|
removeAccessTokenTrustedIp(index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
className="p-3"
|
className="p-3"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="my-4 ml-1">
|
<div className="my-4 ml-1">
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add IP Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
variant="outline_bg"
|
||||||
size="sm"
|
onClick={() => {
|
||||||
type="submit"
|
if (subscription?.ipAllowlisting) {
|
||||||
isLoading={isSubmitting}
|
appendAccessTokenTrustedIp({
|
||||||
isDisabled={isSubmitting}
|
ipAddress: "0.0.0.0/0"
|
||||||
>
|
});
|
||||||
{isUpdate ? "Update" : "Add"}
|
return;
|
||||||
</Button>
|
}
|
||||||
|
|
||||||
<Button
|
handlePopUpOpen("upgradePlan");
|
||||||
colorSchema="secondary"
|
}}
|
||||||
variant="plain"
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
size="xs"
|
||||||
>
|
>
|
||||||
Cancel
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{!isUpdate ? "Create" : "Edit"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isUpdate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||||
|
>
|
||||||
|
Remove Auth Method
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,27 +6,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityAzureAuth,
|
useAddIdentityAzureAuth,
|
||||||
useGetIdentityAzureAuth,
|
useGetIdentityAzureAuth,
|
||||||
useUpdateIdentityAzureAuth
|
useUpdateIdentityAzureAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
tenantId: z.string().min(1),
|
tenantId: z.string().min(1),
|
||||||
@ -54,18 +44,21 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityId?: string;
|
identityAuthMethodData: {
|
||||||
isUpdate?: boolean;
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
configuredAuthMethods?: IdentityAuthMethod[];
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAzureAuthForm = ({
|
export const IdentityAzureAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityId,
|
identityAuthMethodData
|
||||||
isUpdate
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -73,9 +66,11 @@ export const IdentityAzureAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityAzureAuth(identityId ?? "", {
|
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||||
|
identityAuthMethodData.authMethod! || ""
|
||||||
|
);
|
||||||
|
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -144,12 +139,12 @@ export const IdentityAzureAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) return;
|
if (!identityAuthMethodData) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
tenantId,
|
tenantId,
|
||||||
resource,
|
resource,
|
||||||
allowedServicePrincipalIds,
|
allowedServicePrincipalIds,
|
||||||
@ -161,7 +156,7 @@ export const IdentityAzureAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
tenantId: tenantId || "",
|
tenantId: tenantId || "",
|
||||||
resource: resource || "",
|
resource: resource || "",
|
||||||
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
|
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
|
||||||
@ -189,195 +184,190 @@ export const IdentityAzureAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
<Controller
|
||||||
setTabValue(
|
control={control}
|
||||||
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
defaultValue="2592000"
|
||||||
? IdentityFormTab.Advanced
|
name="tenantId"
|
||||||
: IdentityFormTab.Configuration
|
render={({ field, fieldState: { error } }) => (
|
||||||
);
|
<FormControl
|
||||||
})}
|
label="Tenant ID"
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
errorText={error?.message}
|
||||||
<TabList>
|
isRequired
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
>
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
||||||
</TabList>
|
</FormControl>
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="resource"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Resource / Audience"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="https://management.azure.com/" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedServicePrincipalIds"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Service Principal IDs"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
name="tenantId"
|
defaultValue="0.0.0.0/0"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
label="Tenant ID"
|
<FormControl
|
||||||
isError={Boolean(error)}
|
className="mb-0 flex-grow"
|
||||||
errorText={error?.message}
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
isRequired
|
isError={Boolean(error)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
|
>
|
||||||
</FormControl>
|
<Input
|
||||||
)}
|
value={field.value}
|
||||||
/>
|
onChange={(e) => {
|
||||||
<Controller
|
if (subscription?.ipAllowlisting) {
|
||||||
control={control}
|
field.onChange(e);
|
||||||
name="resource"
|
return;
|
||||||
render={({ field, fieldState: { error } }) => (
|
}
|
||||||
<FormControl
|
|
||||||
label="Resource / Audience"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="https://management.azure.com/" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="allowedServicePrincipalIds"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Allowed Service Principal IDs"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenMaxTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
placeholder="123.456.789.0"
|
placeholder="123.456.789.0"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (subscription?.ipAllowlisting) {
|
if (subscription?.ipAllowlisting) {
|
||||||
removeAccessTokenTrustedIp(index);
|
removeAccessTokenTrustedIp(index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
className="p-3"
|
className="p-3"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="my-4 ml-1">
|
<div className="my-4 ml-1">
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add IP Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
variant="outline_bg"
|
||||||
size="sm"
|
onClick={() => {
|
||||||
type="submit"
|
if (subscription?.ipAllowlisting) {
|
||||||
isLoading={isSubmitting}
|
appendAccessTokenTrustedIp({
|
||||||
isDisabled={isSubmitting}
|
ipAddress: "0.0.0.0/0"
|
||||||
>
|
});
|
||||||
{isUpdate ? "Update" : "Add"}
|
return;
|
||||||
</Button>
|
}
|
||||||
|
|
||||||
<Button
|
handlePopUpOpen("upgradePlan");
|
||||||
colorSchema="secondary"
|
}}
|
||||||
variant="plain"
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
size="xs"
|
||||||
>
|
>
|
||||||
Cancel
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{!isUpdate ? "Create" : "Edit"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isUpdate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||||
|
>
|
||||||
|
Remove Auth Method
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,29 +6,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityGcpAuth,
|
useAddIdentityGcpAuth,
|
||||||
useGetIdentityGcpAuth,
|
useGetIdentityGcpAuth,
|
||||||
useUpdateIdentityGcpAuth
|
useUpdateIdentityGcpAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.enum(["iam", "gce"]),
|
type: z.enum(["iam", "gce"]),
|
||||||
@ -57,18 +45,21 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityId?: string;
|
identityAuthMethodData: {
|
||||||
isUpdate?: boolean;
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
configuredAuthMethods?: IdentityAuthMethod[];
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityGcpAuthForm = ({
|
export const IdentityGcpAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityId,
|
identityAuthMethodData
|
||||||
isUpdate
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -76,9 +67,11 @@ export const IdentityGcpAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityGcpAuth(identityId ?? "", {
|
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||||
|
identityAuthMethodData.authMethod! || ""
|
||||||
|
);
|
||||||
|
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -153,11 +146,11 @@ export const IdentityGcpAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) return;
|
if (!identityAuthMethodData) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
type,
|
type,
|
||||||
allowedServiceAccounts,
|
allowedServiceAccounts,
|
||||||
@ -170,7 +163,7 @@ export const IdentityGcpAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
type,
|
type,
|
||||||
allowedServiceAccounts: allowedServiceAccounts || "",
|
allowedServiceAccounts: allowedServiceAccounts || "",
|
||||||
@ -200,223 +193,214 @@ export const IdentityGcpAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
<Controller
|
||||||
setTabValue(
|
control={control}
|
||||||
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
name="type"
|
||||||
? IdentityFormTab.Advanced
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
: IdentityFormTab.Configuration
|
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
||||||
);
|
<Select
|
||||||
})}
|
defaultValue={field.value}
|
||||||
>
|
{...field}
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
onValueChange={(e) => onChange(e)}
|
||||||
<TabList>
|
className="w-full"
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
|
||||||
</TabList>
|
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="type"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<SelectItem value="gce" key="gce">
|
|
||||||
GCP ID Token Auth (Recommended)
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="iam" key="iam">
|
|
||||||
GCP IAM Auth
|
|
||||||
</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="allowedServiceAccounts"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Allowed Service Account Emails"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{watchedType === "gce" && (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="allowedProjects"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Allowed Projects"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="my-gcp-project, ..." />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{watchedType === "gce" && (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="allowedZones"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Allowed Zones"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenMaxTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
removeAccessTokenTrustedIp(index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="my-4 ml-1">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
<SelectItem value="gce" key="gce">
|
||||||
</Button>
|
GCP ID Token Auth (Recommended)
|
||||||
</div>
|
</SelectItem>
|
||||||
</TabPanel>
|
<SelectItem value="iam" key="iam">
|
||||||
</Tabs>
|
GCP IAM Auth
|
||||||
<div className="flex items-center">
|
</SelectItem>
|
||||||
<Button
|
</Select>
|
||||||
className="mr-4"
|
</FormControl>
|
||||||
size="sm"
|
)}
|
||||||
type="submit"
|
/>
|
||||||
isLoading={isSubmitting}
|
<Controller
|
||||||
isDisabled={isSubmitting}
|
control={control}
|
||||||
>
|
defaultValue="2592000"
|
||||||
{isUpdate ? "Update" : "Add"}
|
name="allowedServiceAccounts"
|
||||||
</Button>
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Service Account Emails"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{watchedType === "gce" && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedProjects"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Projects"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="my-gcp-project, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{watchedType === "gce" && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedZones"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
|
defaultValue="0.0.0.0/0"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
removeAccessTokenTrustedIp(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
variant="plain"
|
onClick={() => {
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
if (subscription?.ipAllowlisting) {
|
||||||
|
appendAccessTokenTrustedIp({
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
Cancel
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{!isUpdate ? "Create" : "Edit"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isUpdate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||||
|
>
|
||||||
|
Remove Auth Method
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
@ -14,22 +14,17 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs,
|
|
||||||
TextArea,
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api";
|
import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
|
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
|
||||||
import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries";
|
import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
|
||||||
|
|
||||||
const commonSchema = z.object({
|
const commonSchema = z.object({
|
||||||
accessTokenTrustedIps: z
|
accessTokenTrustedIps: z
|
||||||
.array(
|
.array(
|
||||||
@ -90,18 +85,21 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityId?: string;
|
identityAuthMethodData: {
|
||||||
isUpdate?: boolean;
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
configuredAuthMethods?: IdentityAuthMethod[];
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityJwtAuthForm = ({
|
export const IdentityJwtAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityId,
|
identityAuthMethodData
|
||||||
isUpdate
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -109,9 +107,11 @@ export const IdentityJwtAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityJwtAuth(identityId ?? "", {
|
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||||
|
identityAuthMethodData.authMethod! || ""
|
||||||
|
);
|
||||||
|
const { data } = useGetIdentityJwtAuth(identityAuthMethodData?.identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -218,13 +218,13 @@ export const IdentityJwtAuthForm = ({
|
|||||||
boundSubject
|
boundSubject
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) {
|
if (!identityAuthMethodData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
configurationType,
|
configurationType,
|
||||||
jwksUrl,
|
jwksUrl,
|
||||||
@ -241,7 +241,7 @@ export const IdentityJwtAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
configurationType,
|
configurationType,
|
||||||
jwksUrl,
|
jwksUrl,
|
||||||
jwksCaCert,
|
jwksCaCert,
|
||||||
@ -275,179 +275,56 @@ export const IdentityJwtAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
<Controller
|
||||||
setTabValue(
|
control={control}
|
||||||
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
name="configurationType"
|
||||||
? IdentityFormTab.Advanced
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
: IdentityFormTab.Configuration
|
<FormControl
|
||||||
);
|
label="Configuration Type"
|
||||||
})}
|
isError={Boolean(error)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
>
|
||||||
<TabList>
|
<Select
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
defaultValue={field.value}
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
{...field}
|
||||||
</TabList>
|
onValueChange={(e) => {
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
if (e === IdentityJwtConfigurationType.JWKS) {
|
||||||
<Controller
|
setValue("publicKeys", []);
|
||||||
control={control}
|
} else {
|
||||||
name="configurationType"
|
setValue("publicKeys", [
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
{
|
||||||
<FormControl
|
|
||||||
label="Configuration Type"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => {
|
|
||||||
if (e === IdentityJwtConfigurationType.JWKS) {
|
|
||||||
setValue("publicKeys", []);
|
|
||||||
} else {
|
|
||||||
setValue("publicKeys", [
|
|
||||||
{
|
|
||||||
value: ""
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
setValue("jwksUrl", "");
|
|
||||||
setValue("jwksCaCert", "");
|
|
||||||
}
|
|
||||||
onChange(e);
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<SelectItem value={IdentityJwtConfigurationType.JWKS} key="jwks">
|
|
||||||
JWKS
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={IdentityJwtConfigurationType.STATIC} key="static">
|
|
||||||
Static
|
|
||||||
</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{selectedConfigurationType === IdentityJwtConfigurationType.JWKS && (
|
|
||||||
<>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="jwksUrl"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isRequired
|
|
||||||
label="JWKS URL"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} type="text" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="jwksCaCert"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="JWKS CA Certificate"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
|
|
||||||
<>
|
|
||||||
{publicKeyFields.map(({ id }, index) => (
|
|
||||||
<div key={id} className="flex gap-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`publicKeys.${index}.value`}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
className="flex-grow"
|
|
||||||
label={`Public Key ${index + 1}`}
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
icon={
|
|
||||||
<Tooltip
|
|
||||||
className="text-center"
|
|
||||||
content={<span>This field only accepts PEM-formatted public keys</span>}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TextArea {...field} placeholder="-----BEGIN PUBLIC KEY----- ..." />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
if (publicKeyFields.length === 1) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "A public key is required for static configurations"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
removePublicKeyFields(index);
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="my-4 ml-1">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() =>
|
|
||||||
appendPublicKeyFields({
|
|
||||||
value: ""
|
value: ""
|
||||||
})
|
}
|
||||||
}
|
]);
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
setValue("jwksUrl", "");
|
||||||
size="xs"
|
setValue("jwksCaCert", "");
|
||||||
>
|
}
|
||||||
Add Public Key
|
onChange(e);
|
||||||
</Button>
|
}}
|
||||||
</div>
|
className="w-full"
|
||||||
</>
|
>
|
||||||
)}
|
<SelectItem value={IdentityJwtConfigurationType.JWKS} key="jwks">
|
||||||
|
JWKS
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={IdentityJwtConfigurationType.STATIC} key="static">
|
||||||
|
Static
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedConfigurationType === IdentityJwtConfigurationType.JWKS && (
|
||||||
|
<>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="boundIssuer"
|
name="jwksUrl"
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Issuer" isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Input {...field} type="text" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="boundSubject"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Subject"
|
isRequired
|
||||||
|
label="JWKS URL"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
icon={
|
|
||||||
<Tooltip
|
|
||||||
className="text-center"
|
|
||||||
content={<span>This field supports glob patterns</span>}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Input {...field} type="text" />
|
<Input {...field} type="text" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -455,79 +332,58 @@ export const IdentityJwtAuthForm = ({
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="boundAudiences"
|
name="jwksCaCert"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Audiences"
|
label="JWKS CA Certificate"
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
icon={
|
isError={Boolean(error)}
|
||||||
<Tooltip
|
|
||||||
className="text-center"
|
|
||||||
content={<span>This field supports glob patterns</span>}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Input {...field} type="text" placeholder="service1, service2" />
|
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{boundClaimsFields.map(({ id }, index) => (
|
</>
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
)}
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`boundClaims.${index}.key`}
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Claims" : undefined}
|
|
||||||
icon={
|
|
||||||
index === 0 ? (
|
|
||||||
<Tooltip
|
|
||||||
className="text-center"
|
|
||||||
content={<span>This field supports glob patterns</span>}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => field.onChange(e)}
|
|
||||||
placeholder="property"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`boundClaims.${index}.value`}
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => field.onChange(e)}
|
|
||||||
placeholder="value1, value2"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
|
||||||
|
<>
|
||||||
|
{publicKeyFields.map(({ id }, index) => (
|
||||||
|
<div key={id} className="flex gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`publicKeys.${index}.value`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="flex-grow"
|
||||||
|
label={`Public Key ${index + 1}`}
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
icon={
|
||||||
|
<Tooltip
|
||||||
|
className="text-center"
|
||||||
|
content={<span>This field only accepts PEM-formatted public keys</span>}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextArea {...field} placeholder="-----BEGIN PUBLIC KEY----- ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => removeBoundClaimField(index)}
|
onClick={() => {
|
||||||
|
if (publicKeyFields.length === 1) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "A public key is required for static configurations"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removePublicKeyFields(index);
|
||||||
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@ -542,150 +398,291 @@ export const IdentityJwtAuthForm = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
appendBoundClaimField({
|
appendPublicKeyFields({
|
||||||
key: "",
|
|
||||||
value: ""
|
value: ""
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
Add Claims
|
Add Public Key
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
</>
|
||||||
control={control}
|
)}
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenMaxTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
<Controller
|
||||||
}}
|
control={control}
|
||||||
placeholder="123.456.789.0"
|
name="boundIssuer"
|
||||||
/>
|
render={({ field, fieldState: { error } }) => (
|
||||||
</FormControl>
|
<FormControl label="Issuer" isError={Boolean(error)} errorText={error?.message}>
|
||||||
);
|
<Input {...field} type="text" />
|
||||||
}}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
<IconButton
|
/>
|
||||||
onClick={() => {
|
<Controller
|
||||||
if (subscription?.ipAllowlisting) {
|
control={control}
|
||||||
removeAccessTokenTrustedIp(index);
|
name="boundSubject"
|
||||||
return;
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Subject"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
icon={
|
||||||
|
<Tooltip
|
||||||
|
className="text-center"
|
||||||
|
content={<span>This field supports glob patterns</span>}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input {...field} type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="boundAudiences"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Audiences"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
icon={
|
||||||
|
<Tooltip
|
||||||
|
className="text-center"
|
||||||
|
content={<span>This field supports glob patterns</span>}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input {...field} type="text" placeholder="service1, service2" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{boundClaimsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`boundClaims.${index}.key`}
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Claims" : undefined}
|
||||||
|
icon={
|
||||||
|
index === 0 ? (
|
||||||
|
<Tooltip
|
||||||
|
className="text-center"
|
||||||
|
content={<span>This field supports glob patterns</span>}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => field.onChange(e)}
|
||||||
|
placeholder="property"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`boundClaims.${index}.value`}
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => field.onChange(e)}
|
||||||
|
placeholder="value1, value2"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
<IconButton
|
||||||
}}
|
onClick={() => removeBoundClaimField(index)}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
className="p-3"
|
className="p-3"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="my-4 ml-1">
|
<div className="my-4 ml-1">
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add IP Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
variant="outline_bg"
|
||||||
size="sm"
|
onClick={() =>
|
||||||
type="submit"
|
appendBoundClaimField({
|
||||||
isLoading={isSubmitting}
|
key: "",
|
||||||
isDisabled={isSubmitting}
|
value: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
{isUpdate ? "Update" : "Create"}
|
Add Claims
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
|
defaultValue="0.0.0.0/0"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
removeAccessTokenTrustedIp(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
variant="plain"
|
onClick={() => {
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
if (subscription?.ipAllowlisting) {
|
||||||
|
appendAccessTokenTrustedIp({
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
Cancel
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isUpdate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||||
|
>
|
||||||
|
Remove Auth Method
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,28 +6,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs,
|
|
||||||
TextArea
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityKubernetesAuth,
|
useAddIdentityKubernetesAuth,
|
||||||
useGetIdentityKubernetesAuth,
|
useGetIdentityKubernetesAuth,
|
||||||
useUpdateIdentityKubernetesAuth
|
useUpdateIdentityKubernetesAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
kubernetesHost: z.string().min(1),
|
kubernetesHost: z.string().min(1),
|
||||||
@ -58,18 +47,21 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityId?: string;
|
identityAuthMethodData: {
|
||||||
isUpdate?: boolean;
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
configuredAuthMethods?: IdentityAuthMethod[];
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityKubernetesAuthForm = ({
|
export const IdentityKubernetesAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityId,
|
identityAuthMethodData
|
||||||
isUpdate
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -77,9 +69,11 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityKubernetesAuth(identityId ?? "", {
|
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||||
|
identityAuthMethodData.authMethod! || ""
|
||||||
|
);
|
||||||
|
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -160,7 +154,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) return;
|
if (!identityAuthMethodData) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@ -171,7 +165,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
allowedNamespaces,
|
allowedNamespaces,
|
||||||
allowedAudience,
|
allowedAudience,
|
||||||
caCert,
|
caCert,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -180,7 +174,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
kubernetesHost: kubernetesHost || "",
|
kubernetesHost: kubernetesHost || "",
|
||||||
tokenReviewerJwt,
|
tokenReviewerJwt,
|
||||||
allowedNames: allowedNames || "",
|
allowedNames: allowedNames || "",
|
||||||
@ -211,255 +205,242 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
<Controller
|
||||||
setTabValue(
|
control={control}
|
||||||
[
|
defaultValue="2592000"
|
||||||
"kubernetesHost",
|
name="kubernetesHost"
|
||||||
"tokenReviewerJwt",
|
render={({ field, fieldState: { error } }) => (
|
||||||
"accessTokenTTL",
|
<FormControl
|
||||||
"accessTokenMaxTTL",
|
label="Kubernetes Host / Base Kubernetes API URL "
|
||||||
"accessTokenNumUsesLimit",
|
isError={Boolean(error)}
|
||||||
"allowedNames",
|
errorText={error?.message}
|
||||||
"allowedNamespaces"
|
tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'"
|
||||||
].includes(Object.keys(fields)[0])
|
isRequired
|
||||||
? IdentityFormTab.Configuration
|
>
|
||||||
: IdentityFormTab.Advanced
|
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
|
||||||
);
|
</FormControl>
|
||||||
})}
|
)}
|
||||||
>
|
/>
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
<Controller
|
||||||
<TabList>
|
control={control}
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
name="tokenReviewerJwt"
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
render={({ field, fieldState: { error } }) => (
|
||||||
</TabList>
|
<FormControl
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
label="Token Reviewer JWT"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="" type="password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedNames"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Service Account Names"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="allowedNamespaces"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Namespaces"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="allowedAudience"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Audience"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="" type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="caCert"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="CA Certificate"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
|
||||||
|
>
|
||||||
|
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
name="kubernetesHost"
|
defaultValue="0.0.0.0/0"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
label="Kubernetes Host / Base Kubernetes API URL "
|
<FormControl
|
||||||
isError={Boolean(error)}
|
className="mb-0 flex-grow"
|
||||||
errorText={error?.message}
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'"
|
isError={Boolean(error)}
|
||||||
isRequired
|
errorText={error?.message}
|
||||||
>
|
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||||
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
|
>
|
||||||
</FormControl>
|
<Input
|
||||||
)}
|
value={field.value}
|
||||||
/>
|
onChange={(e) => {
|
||||||
<Controller
|
if (subscription?.ipAllowlisting) {
|
||||||
control={control}
|
field.onChange(e);
|
||||||
name="tokenReviewerJwt"
|
return;
|
||||||
render={({ field, fieldState: { error } }) => (
|
}
|
||||||
<FormControl
|
|
||||||
label="Token Reviewer JWT"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
|
|
||||||
isRequired
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="" type="password" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="allowedNamespaces"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Allowed Namespaces"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="allowedNames"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Allowed Service Account Names"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token TTL (seconds)"
|
|
||||||
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenMaxTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="allowedAudience"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Allowed Audience"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="" type="text" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="caCert"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="CA Certificate"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
|
|
||||||
>
|
|
||||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
placeholder="123.456.789.0"
|
placeholder="123.456.789.0"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (subscription?.ipAllowlisting) {
|
if (subscription?.ipAllowlisting) {
|
||||||
removeAccessTokenTrustedIp(index);
|
removeAccessTokenTrustedIp(index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
className="p-3"
|
className="p-3"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="my-4 ml-1">
|
<div className="my-4 ml-1">
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add IP Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
variant="outline_bg"
|
||||||
size="sm"
|
onClick={() => {
|
||||||
type="submit"
|
if (subscription?.ipAllowlisting) {
|
||||||
isLoading={isSubmitting}
|
appendAccessTokenTrustedIp({
|
||||||
isDisabled={isSubmitting}
|
ipAddress: "0.0.0.0/0"
|
||||||
>
|
});
|
||||||
{isUpdate ? "Update" : "Add"}
|
return;
|
||||||
</Button>
|
}
|
||||||
|
|
||||||
<Button
|
handlePopUpOpen("upgradePlan");
|
||||||
colorSchema="secondary"
|
}}
|
||||||
variant="plain"
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
size="xs"
|
||||||
>
|
>
|
||||||
Cancel
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isUpdate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||||
|
>
|
||||||
|
Remove Auth Method
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
@ -7,26 +7,14 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs,
|
|
||||||
TextArea,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
|
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
|
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
accessTokenTrustedIps: z
|
accessTokenTrustedIps: z
|
||||||
.array(
|
.array(
|
||||||
@ -60,18 +48,21 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityId?: string;
|
identityAuthMethodData: {
|
||||||
isUpdate?: boolean;
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
configuredAuthMethods?: IdentityAuthMethod[];
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityOidcAuthForm = ({
|
export const IdentityOidcAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityId,
|
identityAuthMethodData
|
||||||
isUpdate
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -79,9 +70,11 @@ export const IdentityOidcAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityOidcAuth(identityId ?? "", {
|
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||||
|
identityAuthMethodData.authMethod! || ""
|
||||||
|
);
|
||||||
|
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -167,13 +160,13 @@ export const IdentityOidcAuthForm = ({
|
|||||||
boundSubject
|
boundSubject
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) {
|
if (!identityAuthMethodData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
oidcDiscoveryUrl,
|
oidcDiscoveryUrl,
|
||||||
caCert,
|
caCert,
|
||||||
@ -188,7 +181,7 @@ export const IdentityOidcAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
oidcDiscoveryUrl,
|
oidcDiscoveryUrl,
|
||||||
caCert,
|
caCert,
|
||||||
boundIssuer,
|
boundIssuer,
|
||||||
@ -220,324 +213,315 @@ export const IdentityOidcAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
<Controller
|
||||||
setTabValue(
|
control={control}
|
||||||
["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0])
|
name="oidcDiscoveryUrl"
|
||||||
? IdentityFormTab.Advanced
|
render={({ field, fieldState: { error } }) => (
|
||||||
: IdentityFormTab.Configuration
|
<FormControl
|
||||||
);
|
isRequired
|
||||||
})}
|
label="OIDC Discovery URL"
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
errorText={error?.message}
|
||||||
<TabList>
|
>
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
<Input
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
{...field}
|
||||||
</TabList>
|
placeholder="https://token.actions.githubusercontent.com"
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
type="text"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="boundIssuer"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isRequired
|
||||||
|
label="Issuer"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
placeholder="https://token.actions.githubusercontent.com"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="caCert"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="boundSubject"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Subject"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
icon={
|
||||||
|
<Tooltip
|
||||||
|
className="text-center"
|
||||||
|
content={<span>This field supports glob patterns</span>}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input {...field} type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="boundAudiences"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Audiences"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
icon={
|
||||||
|
<Tooltip
|
||||||
|
className="text-center"
|
||||||
|
content={<span>This field supports glob patterns</span>}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input {...field} type="text" placeholder="service1, service2" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{boundClaimsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="oidcDiscoveryUrl"
|
name={`boundClaims.${index}.key`}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
isRequired
|
<FormControl
|
||||||
label="OIDC Discovery URL"
|
className="mb-0 flex-grow"
|
||||||
isError={Boolean(error)}
|
label={index === 0 ? "Claims" : undefined}
|
||||||
errorText={error?.message}
|
icon={
|
||||||
>
|
index === 0 ? (
|
||||||
<Input
|
<Tooltip
|
||||||
{...field}
|
className="text-center"
|
||||||
placeholder="https://token.actions.githubusercontent.com"
|
content={<span>This field supports glob patterns</span>}
|
||||||
type="text"
|
>
|
||||||
/>
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||||
</FormControl>
|
</Tooltip>
|
||||||
)}
|
) : undefined
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="boundIssuer"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isRequired
|
|
||||||
label="Issuer"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type="text"
|
|
||||||
placeholder="https://token.actions.githubusercontent.com"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="boundSubject"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Subject"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
icon={
|
|
||||||
<Tooltip
|
|
||||||
className="text-center"
|
|
||||||
content={<span>This field supports glob patterns</span>}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Input {...field} type="text" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="boundAudiences"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Audiences"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
icon={
|
|
||||||
<Tooltip
|
|
||||||
className="text-center"
|
|
||||||
content={<span>This field supports glob patterns</span>}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Input {...field} type="text" placeholder="service1, service2" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenMaxTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="caCert"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="CA Certificate"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{boundClaimsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`boundClaims.${index}.key`}
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Claims" : undefined}
|
|
||||||
icon={
|
|
||||||
index === 0 ? (
|
|
||||||
<Tooltip
|
|
||||||
className="text-center"
|
|
||||||
content={<span>This field supports glob patterns</span>}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => field.onChange(e)}
|
|
||||||
placeholder="property"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`boundClaims.${index}.value`}
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => field.onChange(e)}
|
|
||||||
placeholder="value1, value2"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
onClick={() => removeBoundClaimField(index)}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="my-4 ml-1">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() =>
|
|
||||||
appendBoundClaimField({
|
|
||||||
key: "",
|
|
||||||
value: ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add Claims
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
removeAccessTokenTrustedIp(index);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => field.onChange(e)}
|
||||||
|
placeholder="property"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`boundClaims.${index}.value`}
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => field.onChange(e)}
|
||||||
|
placeholder="value1, value2"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
<IconButton
|
||||||
}}
|
onClick={() => removeBoundClaimField(index)}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
className="p-3"
|
className="p-3"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="my-4 ml-1">
|
<div className="my-4 ml-1">
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add IP Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
variant="outline_bg"
|
||||||
size="sm"
|
onClick={() =>
|
||||||
type="submit"
|
appendBoundClaimField({
|
||||||
isLoading={isSubmitting}
|
key: "",
|
||||||
isDisabled={isSubmitting}
|
value: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
{isUpdate ? "Update" : "Add"}
|
Add Claims
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
|
defaultValue="0.0.0.0/0"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
removeAccessTokenTrustedIp(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
variant="plain"
|
onClick={() => {
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
if (subscription?.ipAllowlisting) {
|
||||||
|
appendAccessTokenTrustedIp({
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
Cancel
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isUpdate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||||
|
>
|
||||||
|
Remove Auth Method
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,27 +5,16 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityTokenAuth,
|
useAddIdentityTokenAuth,
|
||||||
useGetIdentityTokenAuth,
|
useGetIdentityTokenAuth,
|
||||||
useUpdateIdentityTokenAuth
|
useUpdateIdentityTokenAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||||
@ -51,18 +39,21 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityId?: string;
|
identityAuthMethodData?: {
|
||||||
isUpdate?: boolean;
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
configuredAuthMethods?: IdentityAuthMethod[];
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityTokenAuthForm = ({
|
export const IdentityTokenAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityId,
|
identityAuthMethodData
|
||||||
isUpdate
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -70,9 +61,12 @@ export const IdentityTokenAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityTokenAuth(identityId ?? "", {
|
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||||
|
identityAuthMethodData.authMethod! || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,30 +91,6 @@ export const IdentityTokenAuthForm = ({
|
|||||||
remove: removeAccessTokenTrustedIp
|
remove: removeAccessTokenTrustedIp
|
||||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
reset({
|
|
||||||
accessTokenTTL: String(data.accessTokenTTL),
|
|
||||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
|
||||||
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
|
||||||
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
|
|
||||||
({ ipAddress, prefix }: IdentityTrustedIp) => {
|
|
||||||
return {
|
|
||||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reset({
|
|
||||||
accessTokenTTL: "2592000",
|
|
||||||
accessTokenMaxTTL: "2592000",
|
|
||||||
accessTokenNumUsesLimit: "0",
|
|
||||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
@ -128,12 +98,12 @@ export const IdentityTokenAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) return;
|
if (!identityAuthMethodData) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -142,7 +112,7 @@ export const IdentityTokenAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -167,154 +137,149 @@ export const IdentityTokenAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
<Controller
|
||||||
setTabValue(
|
control={control}
|
||||||
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
|
defaultValue="2592000"
|
||||||
? IdentityFormTab.Advanced
|
name="accessTokenTTL"
|
||||||
: IdentityFormTab.Configuration
|
render={({ field, fieldState: { error } }) => (
|
||||||
);
|
<FormControl
|
||||||
})}
|
label="Access Token TTL (seconds)"
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
errorText={error?.message}
|
||||||
<TabList>
|
>
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
</FormControl>
|
||||||
</TabList>
|
)}
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
name="accessTokenTTL"
|
defaultValue="0.0.0.0/0"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
label="Access Token TTL (seconds)"
|
<FormControl
|
||||||
isError={Boolean(error)}
|
className="mb-0 flex-grow"
|
||||||
errorText={error?.message}
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
errorText={error?.message}
|
||||||
</FormControl>
|
>
|
||||||
)}
|
<Input
|
||||||
/>
|
value={field.value}
|
||||||
<Controller
|
onChange={(e) => {
|
||||||
control={control}
|
if (subscription?.ipAllowlisting) {
|
||||||
defaultValue="2592000"
|
field.onChange(e);
|
||||||
name="accessTokenMaxTTL"
|
return;
|
||||||
render={({ field, fieldState: { error } }) => (
|
}
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
placeholder="123.456.789.0"
|
placeholder="123.456.789.0"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (subscription?.ipAllowlisting) {
|
if (subscription?.ipAllowlisting) {
|
||||||
removeAccessTokenTrustedIp(index);
|
removeAccessTokenTrustedIp(index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
className="p-3"
|
className="p-3"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="my-4 ml-1">
|
<div className="my-4 ml-1">
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add IP Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
variant="outline_bg"
|
||||||
size="sm"
|
onClick={() => {
|
||||||
type="submit"
|
if (subscription?.ipAllowlisting) {
|
||||||
isLoading={isSubmitting}
|
appendAccessTokenTrustedIp({
|
||||||
isDisabled={isSubmitting}
|
ipAddress: "0.0.0.0/0"
|
||||||
>
|
});
|
||||||
{isUpdate ? "Update" : "Add"}
|
return;
|
||||||
</Button>
|
}
|
||||||
|
|
||||||
<Button
|
handlePopUpOpen("upgradePlan");
|
||||||
colorSchema="secondary"
|
}}
|
||||||
variant="plain"
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
size="xs"
|
||||||
>
|
>
|
||||||
Cancel
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isUpdate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||||
|
>
|
||||||
|
Remove Auth Method
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,344 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
// DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useCreateIdentityUniversalAuthClientSecret,
|
||||||
|
useGetIdentityUniversalAuth,
|
||||||
|
useGetIdentityUniversalAuthClientSecrets
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
description: z.string(),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((value) => Number(value) <= 315360000, "TTL cannot be greater than 315360000"),
|
||||||
|
numUsesLimit: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>;
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["revokeClientSecret"]>,
|
||||||
|
data?: {
|
||||||
|
clientSecretPrefix: string;
|
||||||
|
clientSecretId: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityUniversalAuthClientSecretModal = ({
|
||||||
|
popUp,
|
||||||
|
handlePopUpOpen,
|
||||||
|
handlePopUpToggle
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
|
||||||
|
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
|
||||||
|
|
||||||
|
const popUpData = popUp?.universalAuthClientSecret?.data as {
|
||||||
|
identityId?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isPending } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
|
||||||
|
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? "");
|
||||||
|
|
||||||
|
const { mutateAsync: createClientSecretMutateAsync } =
|
||||||
|
useCreateIdentityUniversalAuthClientSecret();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
description: "",
|
||||||
|
ttl: "",
|
||||||
|
numUsesLimit: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isClientSecretCopied) {
|
||||||
|
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isClientSecretCopied]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isClientIdCopied) {
|
||||||
|
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isClientIdCopied]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!popUpData?.identityId) return;
|
||||||
|
|
||||||
|
const { clientSecret } = await createClientSecretMutateAsync({
|
||||||
|
identityId: popUpData.identityId,
|
||||||
|
description,
|
||||||
|
ttl: Number(ttl),
|
||||||
|
numUsesLimit: Number(numUsesLimit)
|
||||||
|
});
|
||||||
|
|
||||||
|
setToken(clientSecret);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created client secret",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create client secret",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasToken = Boolean(token);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.universalAuthClientSecret?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("universalAuthClientSecret", isOpen);
|
||||||
|
reset();
|
||||||
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`Manage Client ID/Secrets for ${popUpData?.name ?? ""}`}>
|
||||||
|
<h2 className="mb-4">Client ID</h2>
|
||||||
|
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{identityUniversalAuth?.clientId ?? ""}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? "");
|
||||||
|
setIsClientIdCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isClientIdCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
{t("common.click-to-copy")}
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<h2 className="mb-4">New Client Secret</h2>
|
||||||
|
{hasToken ? (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<p>We will only show this secret once</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Got it
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{token}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(token);
|
||||||
|
setIsClientSecretCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
{t("common.click-to-copy")}
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Description (optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Description" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="ttl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="TTL (seconds - optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="numUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="ml-4"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
<Button
|
||||||
|
className="ml-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<h2 className="mb-4">Client Secrets</h2>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Description</Th>
|
||||||
|
<Th>Num Uses</Th>
|
||||||
|
<Th>Expires At</Th>
|
||||||
|
<Th>Client Secret</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={5} innerKey="org-identities-client-secrets" />}
|
||||||
|
{!isPending &&
|
||||||
|
data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
description,
|
||||||
|
clientSecretTTL,
|
||||||
|
clientSecretPrefix,
|
||||||
|
clientSecretNumUses,
|
||||||
|
clientSecretNumUsesLimit,
|
||||||
|
createdAt
|
||||||
|
}) => {
|
||||||
|
let expiresAt;
|
||||||
|
if (clientSecretTTL > 0) {
|
||||||
|
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
|
||||||
|
<Td>{description === "" ? "-" : description}</Td>
|
||||||
|
<Td>{`${clientSecretNumUses}${
|
||||||
|
clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""
|
||||||
|
}`}</Td>
|
||||||
|
<Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}</Td>
|
||||||
|
<Td>{`${clientSecretPrefix}****`}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("revokeClientSecret", {
|
||||||
|
clientSecretPrefix,
|
||||||
|
clientSecretId: id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{!isPending && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
title="No client secrets have been created for this identity yet"
|
||||||
|
icon={faKey}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,27 +6,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityUniversalAuth,
|
useAddIdentityUniversalAuth,
|
||||||
useGetIdentityUniversalAuth,
|
useGetIdentityUniversalAuth,
|
||||||
useUpdateIdentityUniversalAuth
|
useUpdateIdentityUniversalAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
accessTokenTTL: z
|
accessTokenTTL: z
|
||||||
@ -62,27 +52,33 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityId?: string;
|
identityAuthMethodData?: {
|
||||||
isUpdate?: boolean;
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
configuredAuthMethods?: IdentityAuthMethod[];
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityUniversalAuthForm = ({
|
export const IdentityUniversalAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityId,
|
identityAuthMethodData
|
||||||
isUpdate
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityUniversalAuth(identityId ?? "", {
|
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
||||||
|
identityAuthMethodData.authMethod! || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -153,13 +149,13 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) return;
|
if (!identityAuthMethodData) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// update universal auth configuration
|
// update universal auth configuration
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
clientSecretTrustedIps,
|
clientSecretTrustedIps,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
@ -171,7 +167,7 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
|
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId,
|
identityId: identityAuthMethodData.identityId,
|
||||||
clientSecretTrustedIps,
|
clientSecretTrustedIps,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
@ -199,221 +195,216 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
<Controller
|
||||||
setTabValue(
|
control={control}
|
||||||
["accessTokenTrustedIps", "clientSecretTrustedIps"].includes(Object.keys(fields)[0])
|
defaultValue="2592000"
|
||||||
? IdentityFormTab.Advanced
|
name="accessTokenTTL"
|
||||||
: IdentityFormTab.Configuration
|
render={({ field, fieldState: { error } }) => (
|
||||||
);
|
<FormControl
|
||||||
})}
|
label="Access Token TTL (seconds)"
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
errorText={error?.message}
|
||||||
<TabList>
|
>
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
</FormControl>
|
||||||
</TabList>
|
)}
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
||||||
name="accessTokenTTL"
|
defaultValue="0.0.0.0/0"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<FormControl
|
return (
|
||||||
label="Access Token TTL (seconds)"
|
<FormControl
|
||||||
isError={Boolean(error)}
|
className="mb-0 flex-grow"
|
||||||
errorText={error?.message}
|
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
||||||
>
|
isError={Boolean(error)}
|
||||||
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
errorText={error?.message}
|
||||||
</FormControl>
|
>
|
||||||
)}
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<IconButton
|
||||||
control={control}
|
onClick={() => {
|
||||||
defaultValue="2592000"
|
if (subscription?.ipAllowlisting) {
|
||||||
name="accessTokenMaxTTL"
|
removeClientSecretTrustedIp(index);
|
||||||
render={({ field, fieldState: { error } }) => (
|
return;
|
||||||
<FormControl
|
}
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
|
||||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
placeholder="123.456.789.0"
|
size="lg"
|
||||||
/>
|
colorSchema="danger"
|
||||||
</FormControl>
|
variant="plain"
|
||||||
);
|
ariaLabel="update"
|
||||||
}}
|
className="p-3"
|
||||||
/>
|
>
|
||||||
<IconButton
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
onClick={() => {
|
</IconButton>
|
||||||
if (subscription?.ipAllowlisting) {
|
</div>
|
||||||
removeClientSecretTrustedIp(index);
|
))}
|
||||||
return;
|
<div className="my-4 ml-1">
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="my-4 ml-1">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendClientSecretTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add IP Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
removeAccessTokenTrustedIp(index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="my-4 ml-1">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendAccessTokenTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
Add IP Address
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
variant="outline_bg"
|
||||||
size="sm"
|
onClick={() => {
|
||||||
type="submit"
|
if (subscription?.ipAllowlisting) {
|
||||||
isLoading={isSubmitting}
|
appendClientSecretTrustedIp({
|
||||||
isDisabled={isSubmitting}
|
ipAddress: "0.0.0.0/0"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
{isUpdate ? "Update" : "Add"}
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
|
defaultValue="0.0.0.0/0"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
removeAccessTokenTrustedIp(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
variant="plain"
|
onClick={() => {
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
if (subscription?.ipAllowlisting) {
|
||||||
|
appendAccessTokenTrustedIp({
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
Cancel
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Edit" : "Create"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isUpdate && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
||||||
|
>
|
||||||
|
Delete Auth Method
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
export enum IdentityFormTab {
|
|
||||||
Advanced = "advanced",
|
|
||||||
Configuration = "configuration"
|
|
||||||
}
|
|
@ -1,24 +1,45 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { DeleteActionModal, PageHeader } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
PageHeader,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api";
|
import {
|
||||||
|
IdentityAuthMethod,
|
||||||
|
useDeleteIdentity,
|
||||||
|
useGetIdentityById,
|
||||||
|
useRevokeIdentityTokenAuthToken,
|
||||||
|
useRevokeIdentityUniversalAuthClientSecret
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { Identity } from "@app/hooks/api/identities/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal";
|
|
||||||
import { OrgAccessControlTabSections } from "@app/types/org";
|
import { OrgAccessControlTabSections } from "@app/types/org";
|
||||||
|
|
||||||
import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
|
import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
|
||||||
import { IdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
|
import { IdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
|
||||||
|
import { IdentityUniversalAuthClientSecretModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal";
|
||||||
import {
|
import {
|
||||||
IdentityAuthenticationSection,
|
IdentityAuthenticationSection,
|
||||||
|
IdentityClientSecretModal,
|
||||||
IdentityDetailsSection,
|
IdentityDetailsSection,
|
||||||
IdentityProjectsSection
|
IdentityProjectsSection,
|
||||||
|
IdentityTokenListModal,
|
||||||
|
IdentityTokenModal
|
||||||
} from "./components";
|
} from "./components";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
@ -31,13 +52,25 @@ const Page = () => {
|
|||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const { data } = useGetIdentityById(identityId);
|
const { data } = useGetIdentityById(identityId);
|
||||||
const { mutateAsync: deleteIdentity } = useDeleteIdentity();
|
const { mutateAsync: deleteIdentity } = useDeleteIdentity();
|
||||||
|
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
|
||||||
|
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
|
||||||
|
|
||||||
|
const [selectedAuthMethod, setSelectedAuthMethod] = useState<
|
||||||
|
Identity["authMethods"][number] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"identity",
|
"identity",
|
||||||
"deleteIdentity",
|
"deleteIdentity",
|
||||||
"identityAuthMethod",
|
"identityAuthMethod",
|
||||||
"upgradePlan",
|
"revokeAuthMethod",
|
||||||
"viewAuthMethod"
|
"token",
|
||||||
|
"tokenList",
|
||||||
|
"revokeToken",
|
||||||
|
"clientSecret",
|
||||||
|
"revokeClientSecret",
|
||||||
|
"universalAuthClientSecret", // list of client secrets
|
||||||
|
"upgradePlan"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const onDeleteIdentitySubmit = async (id: string) => {
|
const onDeleteIdentitySubmit = async (id: string) => {
|
||||||
@ -71,15 +104,148 @@ const Page = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRevokeTokenSubmit = async ({
|
||||||
|
identityId: parentIdentityId,
|
||||||
|
tokenId,
|
||||||
|
name
|
||||||
|
}: {
|
||||||
|
identityId: string;
|
||||||
|
tokenId: string;
|
||||||
|
name: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await revokeToken({
|
||||||
|
identityId: parentIdentityId,
|
||||||
|
tokenId
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("revokeToken");
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully revoked token ${name ?? ""}`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to delete identity";
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => {
|
||||||
|
try {
|
||||||
|
if (!data?.identity.id || selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) return;
|
||||||
|
|
||||||
|
await revokeClientSecret({
|
||||||
|
identityId: data?.identity.id,
|
||||||
|
clientSecretId
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpToggle("revokeClientSecret", false);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted client secret",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete client secret",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
{data && (
|
{data && (
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
<PageHeader title={data.identity.name} />
|
<PageHeader title={data.identity.name}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<Button variant="outline_bg">More</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={async () => {
|
||||||
|
handlePopUpOpen("identity", {
|
||||||
|
identityId,
|
||||||
|
name: data.identity.name,
|
||||||
|
role: data.role,
|
||||||
|
customRole: data.customRole
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Edit Identity
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={async () => {
|
||||||
|
handlePopUpOpen("identityAuthMethod", {
|
||||||
|
identityId,
|
||||||
|
name: data.identity.name,
|
||||||
|
allAuthMethods: data.identity.authMethods
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Add new auth method
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Identity}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
isAllowed
|
||||||
|
? "hover:!bg-red-500 hover:!text-white"
|
||||||
|
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={async () => {
|
||||||
|
handlePopUpOpen("deleteIdentity", {
|
||||||
|
identityId,
|
||||||
|
name: data.identity.name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Delete Identity
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</PageHeader>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="mr-4 w-96">
|
<div className="mr-4 w-96">
|
||||||
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||||
<IdentityAuthenticationSection
|
<IdentityAuthenticationSection
|
||||||
|
selectedAuthMethod={selectedAuthMethod}
|
||||||
|
setSelectedAuthMethod={setSelectedAuthMethod}
|
||||||
identityId={identityId}
|
identityId={identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
/>
|
/>
|
||||||
@ -94,6 +260,18 @@ const Page = () => {
|
|||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
|
<IdentityTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<IdentityTokenListModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
<IdentityClientSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<IdentityUniversalAuthClientSecretModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
@ -112,11 +290,41 @@ const Page = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ViewIdentityAuthModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.viewAuthMethod.isOpen}
|
isOpen={popUp.revokeToken.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
|
title={`Are you sure want to revoke ${
|
||||||
authMethod={popUp.viewAuthMethod.data}
|
(popUp?.revokeToken?.data as { name: string })?.name || ""
|
||||||
identityId={identityId}
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() => {
|
||||||
|
const revokeTokenData = popUp?.revokeToken?.data as {
|
||||||
|
identityId: string;
|
||||||
|
tokenId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return onRevokeTokenSubmit(revokeTokenData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.revokeClientSecret.isOpen}
|
||||||
|
title={`Are you sure want to delete the client secret ${
|
||||||
|
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
|
||||||
|
""
|
||||||
|
}************?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("revokeClientSecret", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() => {
|
||||||
|
const deleteClientSecretData = popUp?.revokeClientSecret?.data as {
|
||||||
|
clientSecretId: string;
|
||||||
|
clientSecretPrefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return onDeleteClientSecretSubmit({
|
||||||
|
clientSecretId: deleteClientSecretData.clientSecretId
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,73 +1,155 @@
|
|||||||
import { faCog, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { useEffect } from "react";
|
||||||
|
import { faPencil, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
|
import { useGetIdentityById } from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
||||||
|
import { Identity } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityClientSecrets } from "./IdentityClientSecrets";
|
||||||
|
import { IdentityTokens } from "./IdentityTokens";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
|
setSelectedAuthMethod: (authMethod: Identity["authMethods"][number] | null) => void;
|
||||||
|
selectedAuthMethod: Identity["authMethods"][number] | null;
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "viewAuthMethod"]>,
|
popUpName: keyof UsePopUpState<
|
||||||
data?: object | IdentityAuthMethod
|
[
|
||||||
|
"clientSecret",
|
||||||
|
"identityAuthMethod",
|
||||||
|
"revokeClientSecret",
|
||||||
|
"token",
|
||||||
|
"revokeToken",
|
||||||
|
"universalAuthClientSecret",
|
||||||
|
"tokenList"
|
||||||
|
]
|
||||||
|
>,
|
||||||
|
data?: object
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
|
export const IdentityAuthenticationSection = ({
|
||||||
|
identityId,
|
||||||
|
setSelectedAuthMethod,
|
||||||
|
selectedAuthMethod,
|
||||||
|
handlePopUpOpen
|
||||||
|
}: Props) => {
|
||||||
const { data } = useGetIdentityById(identityId);
|
const { data } = useGetIdentityById(identityId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data?.identity) return;
|
||||||
|
|
||||||
|
if (data.identity.authMethods?.length) {
|
||||||
|
setSelectedAuthMethod(data.identity.authMethods[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return () => setSelectedAuthMethod(null);
|
||||||
|
}, [data?.identity]);
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3>
|
<h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3>
|
||||||
|
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||||
|
{(isAllowed) => {
|
||||||
|
return (
|
||||||
|
<Tooltip content="Add new auth method">
|
||||||
|
<IconButton
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("identityAuthMethod", {
|
||||||
|
identityId,
|
||||||
|
name: data.identity.name,
|
||||||
|
allAuthMethods: data.identity.authMethods
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
{data.identity.authMethods.length > 0 ? (
|
{data.identity.authMethods.length > 0 ? (
|
||||||
<div className="flex flex-col divide-y divide-mineshaft-400/50">
|
<>
|
||||||
{data.identity.authMethods.map((authMethod) => (
|
<div className="py-4">
|
||||||
<button
|
<div className="flex justify-between">
|
||||||
key={authMethod}
|
<p className="mb-0.5 ml-px text-sm font-semibold text-mineshaft-300">Auth Method</p>
|
||||||
onClick={() => handlePopUpOpen("viewAuthMethod", authMethod)}
|
</div>
|
||||||
type="button"
|
<div className="flex items-center gap-2">
|
||||||
className="flex w-full items-center justify-between bg-mineshaft-900 px-4 py-2 text-sm hover:bg-mineshaft-700 data-[state=open]:bg-mineshaft-600"
|
<div className="w-full">
|
||||||
>
|
<Select
|
||||||
<span>{identityAuthToNameMap[authMethod]}</span>
|
className="w-full"
|
||||||
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
|
value={selectedAuthMethod as string}
|
||||||
</button>
|
onValueChange={(value) => setSelectedAuthMethod(value as IdentityAuthMethod)}
|
||||||
))}
|
>
|
||||||
</div>
|
{(data.identity?.authMethods || []).map((authMethod) => (
|
||||||
|
<SelectItem key={authMethod || authMethod} value={authMethod}>
|
||||||
|
{identityAuthToNameMap[authMethod]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip content="Edit auth method">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("identityAuthMethod", {
|
||||||
|
identityId,
|
||||||
|
name: data.identity.name,
|
||||||
|
authMethod: selectedAuthMethod,
|
||||||
|
allAuthMethods: data.identity.authMethods
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>{" "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedAuthMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
|
||||||
|
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
)}
|
||||||
|
{selectedAuthMethod === IdentityAuthMethod.TOKEN_AUTH && (
|
||||||
|
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full space-y-2 pt-2">
|
<div className="w-full space-y-2 pt-2">
|
||||||
<p className="text-sm text-mineshaft-300">
|
<p className="text-sm text-mineshaft-300">
|
||||||
No authentication methods configured. Get started by creating a new auth method.
|
No authentication methods configured. Get started by creating a new auth method.
|
||||||
</p>
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("identityAuthMethod", {
|
||||||
|
identityId,
|
||||||
|
name: data.identity.name,
|
||||||
|
allAuthMethods: data.identity.authMethods
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="outline_bg"
|
||||||
|
className="w-full"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Create Auth Method
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!Object.values(IdentityAuthMethod).every((method) =>
|
|
||||||
data.identity.authMethods.includes(method)
|
|
||||||
) && (
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("identityAuthMethod", {
|
|
||||||
identityId,
|
|
||||||
name: data.identity.name,
|
|
||||||
allAuthMethods: data.identity.authMethods
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
variant="outline_bg"
|
|
||||||
className="mt-3 w-full"
|
|
||||||
size="xs"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
>
|
|
||||||
{data.identity.authMethods.length ? "Add" : "Create"} Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { faEllipsisVertical, faKey } from "@fortawesome/free-solid-svg-icons";
|
import { faEllipsis, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
@ -8,7 +8,6 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
IconButton,
|
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
|
import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
|
||||||
@ -28,13 +27,10 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{tokens?.length ? (
|
{tokens?.length ? (
|
||||||
<div className="flex items-center justify-between border-b border-bunker-400 pb-1">
|
<div className="flex justify-between">
|
||||||
<p className="text-sm font-medium text-bunker-300">{`Access Tokens (${tokens.length})`}</p>
|
<p className="text-sm font-semibold text-mineshaft-300">{`Access Tokens (${tokens.length})`}</p>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
variant="link"
|
||||||
className="underline"
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="secondary"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("tokenList", {
|
handlePopUpOpen("tokenList", {
|
||||||
identityId,
|
identityId,
|
||||||
@ -54,16 +50,16 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group flex items-center justify-between border-b border-mineshaft-500 px-2 py-2 last:pb-0"
|
className="group flex items-center justify-between py-2 last:pb-0"
|
||||||
key={`identity-token-${token.id}`}
|
key={`identity-token-${token.id}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FontAwesomeIcon size="xs" className="text-mineshaft-400" icon={faKey} />
|
<FontAwesomeIcon size="1x" icon={faKey} />
|
||||||
<div className="ml-3">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-mineshaft-300">
|
<p className="text-sm font-semibold text-mineshaft-300">
|
||||||
{token.name ? token.name : "-"}
|
{token.name ? token.name : "-"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-mineshaft-400">
|
<p className="text-sm text-mineshaft-300">
|
||||||
{token.isAccessTokenRevoked
|
{token.isAccessTokenRevoked
|
||||||
? "Revoked"
|
? "Revoked"
|
||||||
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
|
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
|
||||||
@ -71,19 +67,14 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
<Tooltip side="right" content="More options">
|
<div className="opacity-0 transition-opacity duration-300 hover:text-primary-400 group-hover:opacity-100 data-[state=open]:text-primary-400">
|
||||||
<IconButton
|
<Tooltip content="More options">
|
||||||
colorSchema="secondary"
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
variant="plain"
|
</Tooltip>
|
||||||
size="xs"
|
</div>
|
||||||
ariaLabel="More options"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEllipsisVertical} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="z-[101] p-1">
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handlePopUpOpen("token", {
|
handlePopUpOpen("token", {
|
||||||
@ -115,9 +106,8 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
})}
|
})}
|
||||||
<Button
|
<Button
|
||||||
className="mr-4 mt-4 w-full"
|
className="mr-4 mt-4 w-full"
|
||||||
colorSchema="secondary"
|
colorSchema="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("token", {
|
handlePopUpOpen("token", {
|
||||||
identityId
|
identityId
|
||||||
|
@ -1,25 +1,8 @@
|
|||||||
import {
|
import { faCheck, faCopy, faKey, faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||||
faCheck,
|
|
||||||
faChevronDown,
|
|
||||||
faCopy,
|
|
||||||
faEdit,
|
|
||||||
faKey,
|
|
||||||
faTrash
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import { IconButton, Tag, Tooltip } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
IconButton,
|
|
||||||
Tag,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import { useGetIdentityById } from "@app/hooks/api";
|
import { useGetIdentityById } from "@app/hooks/api";
|
||||||
@ -28,7 +11,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
type Props = {
|
type Props = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "deleteIdentity"]>,
|
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "token", "clientSecret"]>,
|
||||||
data?: object
|
data?: object
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
@ -43,61 +26,31 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
|||||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Identity Details</h3>
|
<h3 className="text-lg font-semibold text-mineshaft-100">Identity Details</h3>
|
||||||
<DropdownMenu>
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||||
<DropdownMenuTrigger asChild>
|
{(isAllowed) => {
|
||||||
<Button
|
return (
|
||||||
size="xs"
|
<Tooltip content="Edit Identity">
|
||||||
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
|
<IconButton
|
||||||
colorSchema="secondary"
|
isDisabled={!isAllowed}
|
||||||
>
|
ariaLabel="copy icon"
|
||||||
Options
|
variant="plain"
|
||||||
</Button>
|
className="group relative"
|
||||||
</DropdownMenuTrigger>
|
onClick={() => {
|
||||||
<DropdownMenuContent className="min-w-[120px]" align="end">
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className={twMerge(
|
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
|
||||||
onClick={async () => {
|
|
||||||
handlePopUpOpen("identity", {
|
handlePopUpOpen("identity", {
|
||||||
identityId,
|
identityId,
|
||||||
name: data.identity.name,
|
name: data.identity.name,
|
||||||
role: data.role,
|
role: data.role,
|
||||||
customRole: data.customRole
|
customRole: data.customRole,
|
||||||
|
metadata: data.metadata
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
|
||||||
>
|
>
|
||||||
Edit Identity
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
</DropdownMenuItem>
|
</IconButton>
|
||||||
)}
|
</Tooltip>
|
||||||
</OrgPermissionCan>
|
);
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Identity}>
|
}}
|
||||||
{(isAllowed) => (
|
</OrgPermissionCan>
|
||||||
<DropdownMenuItem
|
|
||||||
className={twMerge(
|
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={async () => {
|
|
||||||
handlePopUpOpen("deleteIdentity", {
|
|
||||||
identityId,
|
|
||||||
name: data.identity.name
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
|
||||||
disabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Delete Identity
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
@ -0,0 +1,270 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useCreateTokenIdentityTokenAuth,
|
||||||
|
useGetIdentityTokensTokenAuth,
|
||||||
|
useGetIdentityUniversalAuthClientSecrets
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["tokenList", "revokeToken"]>;
|
||||||
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["revokeToken"]>, data?: object) => void;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["tokenList", "revokeToken"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityTokenListModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
|
||||||
|
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
|
||||||
|
|
||||||
|
const popUpData = popUp?.tokenList?.data as {
|
||||||
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: tokens } = useGetIdentityTokensTokenAuth(popUpData?.identityId ?? "");
|
||||||
|
const { data, isPending } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
|
||||||
|
|
||||||
|
const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
name: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isClientSecretCopied) {
|
||||||
|
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isClientSecretCopied]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isClientIdCopied) {
|
||||||
|
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isClientIdCopied]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!popUpData?.identityId) return;
|
||||||
|
|
||||||
|
const newTokenData = await createToken({
|
||||||
|
identityId: popUpData.identityId,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
|
||||||
|
setToken(newTokenData.accessToken);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created token",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create token",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasToken = Boolean(token);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.tokenList?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("tokenList", isOpen);
|
||||||
|
reset();
|
||||||
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`Manage Access Tokens for ${popUpData?.name ?? ""}`}>
|
||||||
|
<h2 className="mb-4">New Token</h2>
|
||||||
|
{hasToken ? (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<p>We will only show this token once</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Got it
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{token}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(token);
|
||||||
|
setIsClientSecretCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
{t("common.click-to-copy")}
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<div className="flex">
|
||||||
|
<Input {...field} placeholder="My Token" />
|
||||||
|
<Button
|
||||||
|
className="ml-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<h2 className="mb-4">Tokens</h2>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>name</Th>
|
||||||
|
<Th>Num Uses</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th>Max Expires At</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={5} innerKey="identities-tokens" />}
|
||||||
|
{!isPending &&
|
||||||
|
tokens?.map(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
name,
|
||||||
|
accessTokenNumUses,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
isAccessTokenRevoked
|
||||||
|
}) => {
|
||||||
|
const expiresAt = new Date(
|
||||||
|
new Date(createdAt).getTime() + accessTokenMaxTTL * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
|
||||||
|
<Td>{name === "" ? "-" : name}</Td>
|
||||||
|
<Td>{`${accessTokenNumUses}${
|
||||||
|
accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : ""
|
||||||
|
}`}</Td>
|
||||||
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||||
|
<Td>
|
||||||
|
{isAccessTokenRevoked ? "Revoked" : `${format(expiresAt, "yyyy-MM-dd")}`}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{!isAccessTokenRevoked && (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("revokeToken", {
|
||||||
|
identityId: popUpData?.identityId,
|
||||||
|
tokenId: id,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{!isPending && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
title="No tokens have been created for this identity yet"
|
||||||
|
icon={faKey}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -1,20 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
label: string;
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IdentityAuthFieldDisplay = ({ label, children, className }: Props) => {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<span className="text-sm text-mineshaft-400">{label}</span>
|
|
||||||
{children ? (
|
|
||||||
<p className="break-words text-base leading-4">{children}</p>
|
|
||||||
) : (
|
|
||||||
<p className="text-base italic leading-4 text-bunker-400">Not set</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,239 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { faBan, faEdit, faKey, faPlus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DeleteActionModal,
|
|
||||||
EmptyState,
|
|
||||||
IconButton,
|
|
||||||
Pagination,
|
|
||||||
Table,
|
|
||||||
TableContainer,
|
|
||||||
TBody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
Tooltip,
|
|
||||||
Tr
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
import { useRevokeIdentityTokenAuthToken } from "@app/hooks/api";
|
|
||||||
import { IdentityAccessToken } from "@app/hooks/api/identities/types";
|
|
||||||
import { IdentityTokenModal } from "@app/pages/organization/IdentityDetailsByIDPage/components";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
tokens: IdentityAccessToken[];
|
|
||||||
identityId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
|
||||||
"token",
|
|
||||||
"revokeToken"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [perPage, setPerPage] = useState(5);
|
|
||||||
|
|
||||||
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
|
|
||||||
|
|
||||||
const onRevokeTokenSubmit = async ({
|
|
||||||
identityId: parentIdentityId,
|
|
||||||
tokenId,
|
|
||||||
name
|
|
||||||
}: {
|
|
||||||
identityId: string;
|
|
||||||
tokenId: string;
|
|
||||||
name: string;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
await revokeToken({
|
|
||||||
identityId: parentIdentityId,
|
|
||||||
tokenId
|
|
||||||
});
|
|
||||||
|
|
||||||
handlePopUpClose("revokeToken");
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: `Successfully revoked token ${name ?? ""}`,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
const error = err as any;
|
|
||||||
const text = error?.response?.data?.message ?? "Failed to revoke token";
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-span-2 mt-3">
|
|
||||||
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
|
|
||||||
<span className="text-bunker-300">Access Tokens</span>
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("token", {
|
|
||||||
identityId
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
Add Token
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</div>
|
|
||||||
<TableContainer className="mt-4 rounded-none border-none">
|
|
||||||
<Table>
|
|
||||||
{Boolean(tokens?.length) && (
|
|
||||||
<THead>
|
|
||||||
<Tr className="text-xs font-medium">
|
|
||||||
<Th className="py-1 font-normal">Name</Th>
|
|
||||||
<Th className="whitespace-nowrap py-1 font-normal">Number of Uses</Th>
|
|
||||||
<Th className="py-1 font-normal">Expires</Th>
|
|
||||||
<Th className="w-5 py-1 font-normal" />
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
)}
|
|
||||||
<TBody>
|
|
||||||
{tokens
|
|
||||||
.slice((page - 1) * perPage, perPage * page)
|
|
||||||
.map(
|
|
||||||
({
|
|
||||||
createdAt,
|
|
||||||
isAccessTokenRevoked,
|
|
||||||
name,
|
|
||||||
accessTokenTTL,
|
|
||||||
accessTokenNumUsesLimit,
|
|
||||||
accessTokenNumUses,
|
|
||||||
id
|
|
||||||
}) => {
|
|
||||||
let expiresAt;
|
|
||||||
if (accessTokenTTL > 0) {
|
|
||||||
expiresAt = new Date(new Date(createdAt).getTime() + accessTokenTTL * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr className="text-xs hover:bg-mineshaft-700" key={id}>
|
|
||||||
<Td>{name || "-"}</Td>
|
|
||||||
<Td>
|
|
||||||
{`${accessTokenNumUses}${accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : ""}`}
|
|
||||||
</Td>
|
|
||||||
<Td className="whitespace-nowrap">
|
|
||||||
{/* eslint-disable-next-line no-nested-ternary */}
|
|
||||||
{isAccessTokenRevoked
|
|
||||||
? "Revoked"
|
|
||||||
: expiresAt
|
|
||||||
? format(expiresAt, "yyyy-MM-dd")
|
|
||||||
: "-"}
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Edit}
|
|
||||||
a={OrgPermissionSubjects.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Tooltip content={isAllowed ? "Edit Token" : "Access Restricted"}>
|
|
||||||
<IconButton
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("token", {
|
|
||||||
identityId,
|
|
||||||
tokenId: id,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="Edit token"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
{!isAccessTokenRevoked && (
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Edit}
|
|
||||||
a={OrgPermissionSubjects.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Tooltip content={isAllowed ? "Revoke Token" : "Access Restricted"}>
|
|
||||||
<IconButton
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("revokeToken", {
|
|
||||||
identityId,
|
|
||||||
tokenId: id,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="Revoke token"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faBan} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{!tokens?.length && (
|
|
||||||
<EmptyState iconSize="1x" title="No access tokens have been generated" icon={faKey} />
|
|
||||||
)}
|
|
||||||
{tokens.length > 0 && (
|
|
||||||
<Pagination
|
|
||||||
count={tokens.length}
|
|
||||||
page={page}
|
|
||||||
perPage={perPage}
|
|
||||||
perPageList={[5]}
|
|
||||||
onChangePage={(newPage) => setPage(newPage)}
|
|
||||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.revokeToken.isOpen}
|
|
||||||
title={`Are you sure want to revoke ${
|
|
||||||
(popUp?.revokeToken?.data as { name: string })?.name || ""
|
|
||||||
}?`}
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
|
|
||||||
deleteKey="confirm"
|
|
||||||
onDeleteApproved={() => {
|
|
||||||
const revokeTokenData = popUp?.revokeToken?.data as {
|
|
||||||
identityId: string;
|
|
||||||
tokenId: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return onRevokeTokenSubmit(revokeTokenData);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IdentityTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,196 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { faKey, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DeleteActionModal,
|
|
||||||
EmptyState,
|
|
||||||
IconButton,
|
|
||||||
Pagination,
|
|
||||||
Table,
|
|
||||||
TableContainer,
|
|
||||||
TBody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
Tooltip,
|
|
||||||
Tr
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
import { useRevokeIdentityUniversalAuthClientSecret } from "@app/hooks/api";
|
|
||||||
import { ClientSecretData } from "@app/hooks/api/identities/types";
|
|
||||||
import { IdentityClientSecretModal } from "@app/pages/organization/IdentityDetailsByIDPage/components";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
clientSecrets: ClientSecretData[];
|
|
||||||
identityId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identityId }: Props) => {
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
|
||||||
"revokeClientSecret",
|
|
||||||
"clientSecret"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [perPage, setPerPage] = useState(5);
|
|
||||||
|
|
||||||
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
|
|
||||||
|
|
||||||
const onDeleteClientSecretSubmit = async (clientSecretId: string) => {
|
|
||||||
try {
|
|
||||||
await revokeClientSecret({
|
|
||||||
identityId,
|
|
||||||
clientSecretId
|
|
||||||
});
|
|
||||||
|
|
||||||
handlePopUpToggle("revokeClientSecret", false);
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully deleted client secret",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to delete client secret",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-span-2">
|
|
||||||
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
|
|
||||||
<span className="text-bunker-300">Client Secrets</span>
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("clientSecret", {
|
|
||||||
identityId
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
Add Client Secret
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</div>
|
|
||||||
<TableContainer className="mt-4 rounded-none border-none">
|
|
||||||
<Table>
|
|
||||||
{Boolean(clientSecrets?.length) && (
|
|
||||||
<THead>
|
|
||||||
<Tr className="text-xs font-medium">
|
|
||||||
<Th className="py-1 font-normal">Secret</Th>
|
|
||||||
<Th className="py-1 font-normal">Description</Th>
|
|
||||||
<Th className="whitespace-nowrap py-1 font-normal">Number of Uses</Th>
|
|
||||||
<Th className="py-1 font-normal">Expires</Th>
|
|
||||||
<Th className="w-5 py-1 font-normal" />
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
)}
|
|
||||||
<TBody>
|
|
||||||
{clientSecrets
|
|
||||||
.slice((page - 1) * perPage, perPage * page)
|
|
||||||
.map(
|
|
||||||
({
|
|
||||||
createdAt,
|
|
||||||
clientSecretTTL,
|
|
||||||
description,
|
|
||||||
clientSecretNumUses,
|
|
||||||
clientSecretPrefix,
|
|
||||||
clientSecretNumUsesLimit,
|
|
||||||
id
|
|
||||||
}) => {
|
|
||||||
let expiresAt;
|
|
||||||
if (clientSecretTTL > 0) {
|
|
||||||
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr className="text-xs hover:bg-mineshaft-700" key={id}>
|
|
||||||
<Td>{clientSecretPrefix}***</Td>
|
|
||||||
<Td>{description || "-"}</Td>
|
|
||||||
<Td>
|
|
||||||
{`${clientSecretNumUses}${clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""}`}
|
|
||||||
</Td>
|
|
||||||
<Td className="whitespace-nowrap">
|
|
||||||
{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Edit}
|
|
||||||
a={OrgPermissionSubjects.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Tooltip content={isAllowed ? "Delete Secret" : "Access Restricted"}>
|
|
||||||
<IconButton
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("revokeClientSecret", {
|
|
||||||
clientSecretPrefix,
|
|
||||||
clientSecretId: id
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="Delete secret"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{!clientSecrets?.length && (
|
|
||||||
<EmptyState iconSize="1x" title="No client secrets have been generated" icon={faKey} />
|
|
||||||
)}
|
|
||||||
{clientSecrets.length > 0 && (
|
|
||||||
<Pagination
|
|
||||||
count={clientSecrets.length}
|
|
||||||
page={page}
|
|
||||||
perPage={perPage}
|
|
||||||
perPageList={[5]}
|
|
||||||
onChangePage={(newPage) => setPage(newPage)}
|
|
||||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.revokeClientSecret.isOpen}
|
|
||||||
title={`Are you sure want to delete the client secret ${
|
|
||||||
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
|
|
||||||
""
|
|
||||||
}************?`}
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("revokeClientSecret", isOpen)}
|
|
||||||
deleteKey="confirm"
|
|
||||||
onDeleteApproved={() => {
|
|
||||||
const deleteClientSecretData = popUp?.revokeClientSecret?.data as {
|
|
||||||
clientSecretId: string;
|
|
||||||
clientSecretPrefix: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return onDeleteClientSecretSubmit(deleteClientSecretData.clientSecretId);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IdentityClientSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,174 +0,0 @@
|
|||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
|
|
||||||
import { useOrganization } from "@app/context";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
import {
|
|
||||||
IdentityAuthMethod,
|
|
||||||
identityAuthToNameMap,
|
|
||||||
useDeleteIdentityAwsAuth,
|
|
||||||
useDeleteIdentityAzureAuth,
|
|
||||||
useDeleteIdentityGcpAuth,
|
|
||||||
useDeleteIdentityJwtAuth,
|
|
||||||
useDeleteIdentityKubernetesAuth,
|
|
||||||
useDeleteIdentityOidcAuth,
|
|
||||||
useDeleteIdentityTokenAuth,
|
|
||||||
useDeleteIdentityUniversalAuth
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
import { ViewIdentityAwsAuthContent } from "./ViewIdentityAwsAuthContent";
|
|
||||||
import { ViewIdentityAzureAuthContent } from "./ViewIdentityAzureAuthContent";
|
|
||||||
import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent";
|
|
||||||
import { ViewIdentityJwtAuthContent } from "./ViewIdentityJwtAuthContent";
|
|
||||||
import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthContent";
|
|
||||||
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
|
|
||||||
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
|
|
||||||
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
identityId: string;
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
isOpen: boolean;
|
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
|
||||||
onDeleteAuthMethod: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TRevokeOptions = {
|
|
||||||
identityId: string;
|
|
||||||
organizationId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Content = ({
|
|
||||||
identityId,
|
|
||||||
authMethod,
|
|
||||||
onDeleteAuthMethod
|
|
||||||
}: Pick<Props, "authMethod" | "identityId" | "onDeleteAuthMethod">) => {
|
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const orgId = currentOrg?.id || "";
|
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
|
||||||
"revokeAuthMethod",
|
|
||||||
"upgradePlan",
|
|
||||||
"identityAuthMethod"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
|
|
||||||
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
|
|
||||||
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
|
|
||||||
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
|
|
||||||
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
|
|
||||||
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
|
|
||||||
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
|
|
||||||
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
|
|
||||||
|
|
||||||
let Component: (props: ViewAuthMethodProps) => JSX.Element;
|
|
||||||
let revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
|
|
||||||
|
|
||||||
const handleDelete = () => handlePopUpOpen("revokeAuthMethod");
|
|
||||||
|
|
||||||
switch (authMethod) {
|
|
||||||
case IdentityAuthMethod.UNIVERSAL_AUTH:
|
|
||||||
revokeMethod = revokeUniversalAuth;
|
|
||||||
Component = ViewIdentityUniversalAuthContent;
|
|
||||||
break;
|
|
||||||
case IdentityAuthMethod.TOKEN_AUTH:
|
|
||||||
revokeMethod = revokeTokenAuth;
|
|
||||||
Component = ViewIdentityTokenAuthContent;
|
|
||||||
break;
|
|
||||||
case IdentityAuthMethod.KUBERNETES_AUTH:
|
|
||||||
revokeMethod = revokeKubernetesAuth;
|
|
||||||
Component = ViewIdentityKubernetesAuthContent;
|
|
||||||
break;
|
|
||||||
case IdentityAuthMethod.GCP_AUTH:
|
|
||||||
revokeMethod = revokeGcpAuth;
|
|
||||||
Component = ViewIdentityGcpAuthContent;
|
|
||||||
break;
|
|
||||||
case IdentityAuthMethod.AWS_AUTH:
|
|
||||||
revokeMethod = revokeAwsAuth;
|
|
||||||
Component = ViewIdentityAwsAuthContent;
|
|
||||||
break;
|
|
||||||
case IdentityAuthMethod.AZURE_AUTH:
|
|
||||||
revokeMethod = revokeAzureAuth;
|
|
||||||
Component = ViewIdentityAzureAuthContent;
|
|
||||||
break;
|
|
||||||
case IdentityAuthMethod.OIDC_AUTH:
|
|
||||||
revokeMethod = revokeOidcAuth;
|
|
||||||
Component = ViewIdentityOidcAuthContent;
|
|
||||||
break;
|
|
||||||
case IdentityAuthMethod.JWT_AUTH:
|
|
||||||
revokeMethod = revokeJwtAuth;
|
|
||||||
Component = ViewIdentityJwtAuthContent;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unhandled Auth Method: ${authMethod}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteAuthMethod = async () => {
|
|
||||||
try {
|
|
||||||
await revokeMethod({
|
|
||||||
identityId,
|
|
||||||
organizationId: orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully removed auth method",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
handlePopUpToggle("revokeAuthMethod", false);
|
|
||||||
onDeleteAuthMethod();
|
|
||||||
} catch {
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to remove auth method",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Component
|
|
||||||
identityId={identityId}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
|
||||||
title={`Are you sure want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
|
|
||||||
deleteKey="confirm"
|
|
||||||
buttonText="Remove"
|
|
||||||
onDeleteApproved={handleDeleteAuthMethod}
|
|
||||||
/>
|
|
||||||
<UpgradePlanModal
|
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
|
||||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ViewIdentityAuthModal = ({
|
|
||||||
isOpen,
|
|
||||||
onOpenChange,
|
|
||||||
authMethod,
|
|
||||||
identityId
|
|
||||||
}: Omit<Props, "onDeleteAuthMethod">) => {
|
|
||||||
if (!identityId || !authMethod) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
|
||||||
<ModalContent className="max-w-2xl" title={identityAuthToNameMap[authMethod]}>
|
|
||||||
<Content
|
|
||||||
identityId={identityId}
|
|
||||||
authMethod={authMethod}
|
|
||||||
onDeleteAuthMethod={() => onOpenChange(false)}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,79 +0,0 @@
|
|||||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
import { EmptyState, Spinner } from "@app/components/v2";
|
|
||||||
import { useGetIdentityAwsAuth } from "@app/hooks/api";
|
|
||||||
import { IdentityAwsAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm";
|
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
|
||||||
|
|
||||||
export const ViewIdentityAwsAuthContent = ({
|
|
||||||
identityId,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpOpen,
|
|
||||||
onDelete,
|
|
||||||
popUp
|
|
||||||
}: ViewAuthMethodProps) => {
|
|
||||||
const { data, isPending } = useGetIdentityAwsAuth(identityId);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<Spinner className="text-mineshaft-400" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<EmptyState icon={faBan} title="Could not find AWS Auth associated with this Identity." />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popUp.identityAuthMethod.isOpen) {
|
|
||||||
return (
|
|
||||||
<IdentityAwsAuthForm
|
|
||||||
identityId={identityId}
|
|
||||||
isUpdate
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewIdentityContentWrapper
|
|
||||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
|
||||||
onDelete={onDelete}
|
|
||||||
>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
|
||||||
{data.accessTokenTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
|
||||||
{data.accessTokenMaxTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
|
||||||
{data.accessTokenNumUsesLimit}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
|
||||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Principal ARNs">
|
|
||||||
{data.allowedPrincipalArns
|
|
||||||
?.split(",")
|
|
||||||
.map((arn) => arn.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Account IDs">
|
|
||||||
{data.allowedAccountIds
|
|
||||||
?.split(",")
|
|
||||||
.map((id) => id.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="STS Endpoint">
|
|
||||||
{data.stsEndpoint}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
</ViewIdentityContentWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,76 +0,0 @@
|
|||||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
import { EmptyState, Spinner } from "@app/components/v2";
|
|
||||||
import { useGetIdentityAzureAuth } from "@app/hooks/api";
|
|
||||||
import { IdentityAzureAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm";
|
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
|
||||||
|
|
||||||
export const ViewIdentityAzureAuthContent = ({
|
|
||||||
identityId,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpOpen,
|
|
||||||
onDelete,
|
|
||||||
popUp
|
|
||||||
}: ViewAuthMethodProps) => {
|
|
||||||
const { data, isPending } = useGetIdentityAzureAuth(identityId);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<Spinner className="text-mineshaft-400" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<EmptyState icon={faBan} title="Could not find Azure Auth associated with this Identity." />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popUp.identityAuthMethod.isOpen) {
|
|
||||||
return (
|
|
||||||
<IdentityAzureAuthForm
|
|
||||||
identityId={identityId}
|
|
||||||
isUpdate
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewIdentityContentWrapper
|
|
||||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
|
||||||
onDelete={onDelete}
|
|
||||||
>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
|
||||||
{data.accessTokenTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
|
||||||
{data.accessTokenMaxTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
|
||||||
{data.accessTokenNumUsesLimit}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
|
||||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Tenant ID">
|
|
||||||
{data.tenantId}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Resource / Audience">
|
|
||||||
{data.resource}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Principal IDs">
|
|
||||||
{data.allowedServicePrincipalIds
|
|
||||||
?.split(",")
|
|
||||||
.map((id) => id.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
</ViewIdentityContentWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,69 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
import { faChevronDown, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: ReactNode;
|
|
||||||
onEdit: VoidFunction;
|
|
||||||
onDelete: VoidFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
|
|
||||||
<span className="text-bunker-300">Details</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
Options
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="min-w-[120px]" align="end">
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={onEdit}
|
|
||||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={onDelete}
|
|
||||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 grid grid-cols-2 gap-3">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,89 +0,0 @@
|
|||||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
import { EmptyState, Spinner } from "@app/components/v2";
|
|
||||||
import { useGetIdentityGcpAuth } from "@app/hooks/api";
|
|
||||||
import { IdentityGcpAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm";
|
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
|
||||||
|
|
||||||
export const ViewIdentityGcpAuthContent = ({
|
|
||||||
identityId,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpOpen,
|
|
||||||
onDelete,
|
|
||||||
popUp
|
|
||||||
}: ViewAuthMethodProps) => {
|
|
||||||
const { data, isPending } = useGetIdentityGcpAuth(identityId);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<Spinner className="text-mineshaft-400" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<EmptyState icon={faBan} title="Could not find GCP Auth associated with this Identity." />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popUp.identityAuthMethod.isOpen) {
|
|
||||||
return (
|
|
||||||
<IdentityGcpAuthForm
|
|
||||||
identityId={identityId}
|
|
||||||
isUpdate
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewIdentityContentWrapper
|
|
||||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
|
||||||
onDelete={onDelete}
|
|
||||||
>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
|
||||||
{data.accessTokenTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
|
||||||
{data.accessTokenMaxTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
|
||||||
{data.accessTokenNumUsesLimit}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
|
||||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Type">
|
|
||||||
{data.type === "gce" ? "GCP ID Token Auth" : "GCP IAM Auth"}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Emails">
|
|
||||||
{data.allowedServiceAccounts
|
|
||||||
?.split(",")
|
|
||||||
.map((account) => account.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
{data.type === "gce" && (
|
|
||||||
<>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Projects">
|
|
||||||
{data.allowedProjects
|
|
||||||
?.split(",")
|
|
||||||
.map((project) => project.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Zones">
|
|
||||||
{data.allowedZones
|
|
||||||
?.split(",")
|
|
||||||
.map((zone) => zone.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ViewIdentityContentWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,152 +0,0 @@
|
|||||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
|
||||||
import { useGetIdentityJwtAuth } from "@app/hooks/api";
|
|
||||||
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
|
|
||||||
import { IdentityJwtAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm";
|
|
||||||
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
|
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
|
|
||||||
export const ViewIdentityJwtAuthContent = ({
|
|
||||||
identityId,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpOpen,
|
|
||||||
onDelete,
|
|
||||||
popUp
|
|
||||||
}: ViewAuthMethodProps) => {
|
|
||||||
const { data, isPending } = useGetIdentityJwtAuth(identityId);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<Spinner className="text-mineshaft-400" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<EmptyState icon={faBan} title="Could not find JWT Auth associated with this Identity." />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popUp.identityAuthMethod.isOpen) {
|
|
||||||
return (
|
|
||||||
<IdentityJwtAuthForm
|
|
||||||
identityId={identityId}
|
|
||||||
isUpdate
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewIdentityContentWrapper
|
|
||||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
|
||||||
onDelete={onDelete}
|
|
||||||
>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
|
||||||
{data.accessTokenTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
|
||||||
{data.accessTokenMaxTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
|
||||||
{data.accessTokenNumUsesLimit}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
|
||||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Configuration Type">
|
|
||||||
{data.configurationType === IdentityJwtConfigurationType.JWKS ? "JWKS" : "Static"}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
{data.configurationType === IdentityJwtConfigurationType.JWKS ? (
|
|
||||||
<>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="JWKS URL">
|
|
||||||
{data.jwksUrl}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="JWKS CA Certificate">
|
|
||||||
{data.jwksCaCert && (
|
|
||||||
<Tooltip
|
|
||||||
side="right"
|
|
||||||
className="max-w-xl p-2"
|
|
||||||
content={
|
|
||||||
<p className="break-words rounded bg-mineshaft-600 p-2">{data.jwksCaCert}</p>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="w-min">
|
|
||||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faEye} />
|
|
||||||
<span>Reveal</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Public Keys">
|
|
||||||
{data.publicKeys.length && (
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{data.publicKeys.map((key, index) => (
|
|
||||||
<Tooltip
|
|
||||||
side="right"
|
|
||||||
className="max-w-xl p-2"
|
|
||||||
key={key}
|
|
||||||
content={
|
|
||||||
<p className="whitespace-normal break-words rounded bg-mineshaft-600 p-2">
|
|
||||||
{key}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="inline-block w-min">
|
|
||||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faEye} />
|
|
||||||
<span>Key {index + 1}</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
)}
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Issuer">
|
|
||||||
{data.boundIssuer}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Subject">
|
|
||||||
{data.boundSubject}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Audiences">
|
|
||||||
{data.boundAudiences
|
|
||||||
?.split(",")
|
|
||||||
.map((name) => name.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Claims">
|
|
||||||
{Object.keys(data.boundClaims).length && (
|
|
||||||
<Tooltip
|
|
||||||
side="right"
|
|
||||||
className="max-w-xl p-2"
|
|
||||||
content={
|
|
||||||
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
|
|
||||||
{JSON.stringify(data.boundClaims, null, 2)}
|
|
||||||
</pre>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="w-min">
|
|
||||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faEye} />
|
|
||||||
<span>Reveal</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
</ViewIdentityContentWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,121 +0,0 @@
|
|||||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
|
||||||
import { useGetIdentityKubernetesAuth } from "@app/hooks/api";
|
|
||||||
import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm";
|
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
|
||||||
|
|
||||||
export const ViewIdentityKubernetesAuthContent = ({
|
|
||||||
identityId,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpOpen,
|
|
||||||
onDelete,
|
|
||||||
popUp
|
|
||||||
}: ViewAuthMethodProps) => {
|
|
||||||
const { data, isPending } = useGetIdentityKubernetesAuth(identityId);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<Spinner className="text-mineshaft-400" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
icon={faBan}
|
|
||||||
title="Could not find Kubernetes Auth associated with this Identity."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popUp.identityAuthMethod.isOpen) {
|
|
||||||
return (
|
|
||||||
<IdentityKubernetesAuthForm
|
|
||||||
identityId={identityId}
|
|
||||||
isUpdate
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewIdentityContentWrapper
|
|
||||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
|
||||||
onDelete={onDelete}
|
|
||||||
>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
|
||||||
{data.accessTokenTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
|
||||||
{data.accessTokenMaxTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
|
||||||
{data.accessTokenNumUsesLimit}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
|
||||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay
|
|
||||||
className="col-span-2"
|
|
||||||
label="Kubernetes Host / Base Kubernetes API URL"
|
|
||||||
>
|
|
||||||
{data.kubernetesHost}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT">
|
|
||||||
<Tooltip
|
|
||||||
side="right"
|
|
||||||
className="max-w-xl p-2"
|
|
||||||
content={
|
|
||||||
<p className="break-words rounded bg-mineshaft-600 p-2">{data.tokenReviewerJwt}</p>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="w-min">
|
|
||||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faEye} />
|
|
||||||
<span>Reveal</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Names">
|
|
||||||
{data.allowedNames
|
|
||||||
?.split(",")
|
|
||||||
.map((name) => name.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Namespaces">
|
|
||||||
{data.allowedNamespaces
|
|
||||||
?.split(",")
|
|
||||||
.map((namespace) => namespace.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Audience">
|
|
||||||
{data.allowedAudience}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
|
|
||||||
{data.caCert && (
|
|
||||||
<Tooltip
|
|
||||||
side="right"
|
|
||||||
className="max-w-xl p-2"
|
|
||||||
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCert}</p>}
|
|
||||||
>
|
|
||||||
<div className="w-min">
|
|
||||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faEye} />
|
|
||||||
<span>Reveal</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
</ViewIdentityContentWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,116 +0,0 @@
|
|||||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
|
||||||
import { useGetIdentityOidcAuth } from "@app/hooks/api";
|
|
||||||
import { IdentityOidcAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm";
|
|
||||||
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
|
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
|
|
||||||
export const ViewIdentityOidcAuthContent = ({
|
|
||||||
identityId,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpOpen,
|
|
||||||
onDelete,
|
|
||||||
popUp
|
|
||||||
}: ViewAuthMethodProps) => {
|
|
||||||
const { data, isPending } = useGetIdentityOidcAuth(identityId);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<Spinner className="text-mineshaft-400" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<EmptyState icon={faBan} title="Could not find OIDC Auth associated with this Identity." />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popUp.identityAuthMethod.isOpen) {
|
|
||||||
return (
|
|
||||||
<IdentityOidcAuthForm
|
|
||||||
identityId={identityId}
|
|
||||||
isUpdate
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewIdentityContentWrapper
|
|
||||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
|
||||||
onDelete={onDelete}
|
|
||||||
>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
|
||||||
{data.accessTokenTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
|
||||||
{data.accessTokenMaxTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
|
||||||
{data.accessTokenNumUsesLimit}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
|
||||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="OIDC Discovery URL">
|
|
||||||
{data.oidcDiscoveryUrl}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Issuer">
|
|
||||||
{data.boundIssuer}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
|
|
||||||
{data.caCert && (
|
|
||||||
<Tooltip
|
|
||||||
side="right"
|
|
||||||
className="max-w-xl p-2"
|
|
||||||
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCert}</p>}
|
|
||||||
>
|
|
||||||
<div className="w-min">
|
|
||||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faEye} />
|
|
||||||
<span>Reveal</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Subject">
|
|
||||||
{data.boundSubject}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Audiences">
|
|
||||||
{data.boundAudiences
|
|
||||||
?.split(",")
|
|
||||||
.map((name) => name.trim())
|
|
||||||
.join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay className="col-span-2" label="Claims">
|
|
||||||
{Object.keys(data.boundClaims).length && (
|
|
||||||
<Tooltip
|
|
||||||
side="right"
|
|
||||||
className="max-w-xl p-2"
|
|
||||||
content={
|
|
||||||
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
|
|
||||||
{JSON.stringify(data.boundClaims, null, 2)}
|
|
||||||
</pre>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="w-min">
|
|
||||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faEye} />
|
|
||||||
<span>Reveal</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
</ViewIdentityContentWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,68 +0,0 @@
|
|||||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
import { EmptyState, Spinner } from "@app/components/v2";
|
|
||||||
import { useGetIdentityTokenAuth, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
|
|
||||||
import { IdentityTokenAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm";
|
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
|
||||||
import { IdentityTokenAuthTokensTable } from "./IdentityTokenAuthTokensTable";
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
|
||||||
|
|
||||||
export const ViewIdentityTokenAuthContent = ({
|
|
||||||
identityId,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpOpen,
|
|
||||||
onDelete,
|
|
||||||
popUp
|
|
||||||
}: ViewAuthMethodProps) => {
|
|
||||||
const { data, isPending } = useGetIdentityTokenAuth(identityId);
|
|
||||||
const { data: tokens = [], isPending: clientSecretsPending } =
|
|
||||||
useGetIdentityTokensTokenAuth(identityId);
|
|
||||||
|
|
||||||
if (isPending || clientSecretsPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<Spinner className="text-mineshaft-400" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<EmptyState icon={faBan} title="Could not find Token Auth associated with this Identity." />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popUp.identityAuthMethod.isOpen) {
|
|
||||||
return (
|
|
||||||
<IdentityTokenAuthForm
|
|
||||||
identityId={identityId}
|
|
||||||
isUpdate
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewIdentityContentWrapper
|
|
||||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
|
||||||
onDelete={onDelete}
|
|
||||||
>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
|
||||||
{data.accessTokenTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
|
||||||
{data.accessTokenMaxTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
|
||||||
{data.accessTokenNumUsesLimit}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
|
||||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityTokenAuthTokensTable tokens={tokens} identityId={identityId} />
|
|
||||||
</ViewIdentityContentWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,106 +0,0 @@
|
|||||||
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
|
||||||
import { useTimedReset } from "@app/hooks";
|
|
||||||
import {
|
|
||||||
useGetIdentityUniversalAuth,
|
|
||||||
useGetIdentityUniversalAuthClientSecrets
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm";
|
|
||||||
|
|
||||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
|
||||||
import { IdentityUniversalAuthClientSecretsTable } from "./IdentityUniversalAuthClientSecretsTable";
|
|
||||||
import { ViewAuthMethodProps } from "./types";
|
|
||||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
|
||||||
|
|
||||||
export const ViewIdentityUniversalAuthContent = ({
|
|
||||||
identityId,
|
|
||||||
handlePopUpToggle,
|
|
||||||
handlePopUpOpen,
|
|
||||||
onDelete,
|
|
||||||
popUp
|
|
||||||
}: ViewAuthMethodProps) => {
|
|
||||||
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
|
|
||||||
const { data: clientSecrets = [], isPending: clientSecretsPending } =
|
|
||||||
useGetIdentityUniversalAuthClientSecrets(identityId);
|
|
||||||
|
|
||||||
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
|
|
||||||
initialState: "Copy Client ID to clipboard"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPending || clientSecretsPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center">
|
|
||||||
<Spinner className="text-mineshaft-400" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<EmptyState
|
|
||||||
icon={faBan}
|
|
||||||
title="Could not find Universal Auth associated with this Identity."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (popUp.identityAuthMethod.isOpen) {
|
|
||||||
return (
|
|
||||||
<IdentityUniversalAuthForm
|
|
||||||
identityId={identityId}
|
|
||||||
isUpdate
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewIdentityContentWrapper
|
|
||||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
|
||||||
onDelete={onDelete}
|
|
||||||
>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
|
|
||||||
{data.accessTokenTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
|
|
||||||
{data.accessTokenMaxTTL}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
|
||||||
{data.accessTokenNumUsesLimit}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
|
||||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
|
|
||||||
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
|
||||||
</IdentityAuthFieldDisplay>
|
|
||||||
<div className="col-span-2 my-3">
|
|
||||||
<div className="mb-3 border-b border-mineshaft-500 pb-2">
|
|
||||||
<span className="text-bunker-300">Client ID</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm">{data.clientId}</span>
|
|
||||||
<Tooltip content={copyTextClientId}>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(data.clientId);
|
|
||||||
setCopyTextClientId("Copied");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isCopyingClientId ? faCheck : faCopy} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<IdentityUniversalAuthClientSecretsTable
|
|
||||||
clientSecrets={clientSecrets}
|
|
||||||
identityId={identityId}
|
|
||||||
/>
|
|
||||||
</ViewIdentityContentWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from "./ViewIdentityAuthModal";
|
|
@ -1,12 +0,0 @@
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
|
||||||
|
|
||||||
export type ViewAuthMethodProps = {
|
|
||||||
identityId: string;
|
|
||||||
onDelete: () => void;
|
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan", "identityAuthMethod"]>) => void;
|
|
||||||
handlePopUpToggle: (
|
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
|
||||||
state?: boolean
|
|
||||||
) => void;
|
|
||||||
popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>;
|
|
||||||
};
|
|
@ -2,4 +2,5 @@ export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection/I
|
|||||||
export { IdentityClientSecretModal } from "./IdentityClientSecretModal";
|
export { IdentityClientSecretModal } from "./IdentityClientSecretModal";
|
||||||
export { IdentityDetailsSection } from "./IdentityDetailsSection";
|
export { IdentityDetailsSection } from "./IdentityDetailsSection";
|
||||||
export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection";
|
export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection";
|
||||||
|
export { IdentityTokenListModal } from "./IdentityTokenListModal";
|
||||||
export { IdentityTokenModal } from "./IdentityTokenModal";
|
export { IdentityTokenModal } from "./IdentityTokenModal";
|
||||||
|
Reference in New Issue
Block a user