mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 02:16:18 +00:00
* FIx #7721 UI : Implement Authentication Mechanism on the bots details page * Addressing review comment
This commit is contained in:
parent
cfdc50e18f
commit
448f2de3fb
@ -15,7 +15,10 @@ import { AxiosResponse } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { isNil, isUndefined } from 'lodash';
|
||||
import { SearchIndex } from '../enums/search.enum';
|
||||
import { CreateUser } from '../generated/api/teams/createUser';
|
||||
import {
|
||||
AuthenticationMechanism,
|
||||
CreateUser,
|
||||
} from '../generated/api/teams/createUser';
|
||||
import { JwtAuth } from '../generated/entity/teams/authN/jwtAuth';
|
||||
import { User } from '../generated/entity/teams/user';
|
||||
import { EntityReference } from '../generated/type/entityReference';
|
||||
@ -185,3 +188,11 @@ export const getGroupTypeTeams = async () => {
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAuthMechanismForBotUser = async (botId: string) => {
|
||||
const response = await APIClient.get<AuthenticationMechanism>(
|
||||
`/users/auth-mechanism/${botId}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.ant-space-authMechanism {
|
||||
.ant-space-item:first-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Divider, Input, Space, Typography } from 'antd';
|
||||
import { capitalize } from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import { AuthType } from '../../generated/api/teams/createUser';
|
||||
import { AuthenticationMechanism } from '../../generated/entity/teams/user';
|
||||
import { getTokenExpiry } from '../../utils/BotsUtils';
|
||||
import SVGIcons from '../../utils/SvgUtils';
|
||||
import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton';
|
||||
import './AuthMechanism.less';
|
||||
|
||||
interface Props {
|
||||
authenticationMechanism: AuthenticationMechanism;
|
||||
hasPermission: boolean;
|
||||
onEdit: () => void;
|
||||
onTokenRevoke: () => void;
|
||||
}
|
||||
|
||||
const AuthMechanism: FC<Props> = ({
|
||||
authenticationMechanism,
|
||||
hasPermission,
|
||||
onEdit,
|
||||
onTokenRevoke,
|
||||
}: Props) => {
|
||||
if (authenticationMechanism.authType === AuthType.Jwt) {
|
||||
const JWTToken = authenticationMechanism.config?.JWTToken;
|
||||
const JWTTokenExpiresAt =
|
||||
authenticationMechanism.config?.JWTTokenExpiresAt ?? 0;
|
||||
|
||||
// get the token expiry date
|
||||
const { tokenExpiryDate, isTokenExpired } =
|
||||
getTokenExpiry(JWTTokenExpiresAt);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space className="w-full tw-justify-between">
|
||||
<Typography.Text className="tw-text-base">
|
||||
OpenMetadata JWT Token
|
||||
</Typography.Text>
|
||||
<Space>
|
||||
{JWTToken ? (
|
||||
<Button
|
||||
danger
|
||||
data-testid="revoke-button"
|
||||
disabled={!hasPermission}
|
||||
size="small"
|
||||
type="default"
|
||||
onClick={onTokenRevoke}>
|
||||
Revoke token
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
disabled={!hasPermission}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={onEdit}>
|
||||
Generate New Token
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
<Divider style={{ margin: '8px 0px' }} />
|
||||
<Typography.Paragraph>
|
||||
Token you have generated that can be used to access the OpenMetadata
|
||||
API.
|
||||
</Typography.Paragraph>
|
||||
|
||||
{JWTToken ? (
|
||||
<>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input.Password
|
||||
contentEditable={false}
|
||||
data-testid="token"
|
||||
placeholder="Generate new token..."
|
||||
value={JWTToken}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={JWTToken} />
|
||||
</Space>
|
||||
<p
|
||||
className="tw-text-grey-muted tw-mt-2 tw-italic"
|
||||
data-testid="token-expiry">
|
||||
{JWTTokenExpiresAt ? (
|
||||
isTokenExpired ? (
|
||||
`Expired on ${tokenExpiryDate}.`
|
||||
) : (
|
||||
`Expires on ${tokenExpiryDate}.`
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<SVGIcons alt="warning" icon="error" />
|
||||
<span className="tw-ml-1 tw-align-middle">
|
||||
This token has no expiration date.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="tw-no-description tw-text-sm tw-mt-4"
|
||||
data-testid="no-token">
|
||||
No token available
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (authenticationMechanism.authType === AuthType.Sso) {
|
||||
const authConfig = authenticationMechanism.config?.authConfig;
|
||||
const ssoType = authenticationMechanism.config?.ssoServiceType;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space className="w-full tw-justify-between">
|
||||
<Typography.Text>{`${capitalize(ssoType)} SSO`}</Typography.Text>
|
||||
<Button
|
||||
disabled={!hasPermission}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={onEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
</Space>
|
||||
<Divider style={{ margin: '8px 0px' }} />
|
||||
|
||||
<Space className="w-full" direction="vertical">
|
||||
{authConfig?.secretKey && (
|
||||
<>
|
||||
<Typography.Text>SecretKey</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input.Password
|
||||
contentEditable={false}
|
||||
data-testid="secretKey"
|
||||
value={authConfig?.secretKey}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.secretKey} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.privateKey && (
|
||||
<>
|
||||
<Typography.Text>PrivateKey</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input.Password
|
||||
contentEditable={false}
|
||||
data-testid="privateKey"
|
||||
value={authConfig?.privateKey}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.privateKey} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.clientSecret && (
|
||||
<>
|
||||
<Typography.Text>ClientSecret</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input.Password
|
||||
contentEditable={false}
|
||||
data-testid="clientSecret"
|
||||
value={authConfig?.clientSecret}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.clientSecret} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.audience && (
|
||||
<>
|
||||
<Typography.Text>Audience</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input
|
||||
contentEditable={false}
|
||||
data-testid="audience"
|
||||
value={authConfig?.audience}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.audience} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.clientId && (
|
||||
<>
|
||||
<Typography.Text>ClientId</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input
|
||||
contentEditable={false}
|
||||
data-testid="clientId"
|
||||
value={authConfig?.clientId}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.clientId} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.email && (
|
||||
<>
|
||||
<Typography.Text>Email</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input
|
||||
contentEditable={false}
|
||||
data-testid="email"
|
||||
value={authConfig?.email}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.email} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.orgURL && (
|
||||
<>
|
||||
<Typography.Text>OrgURL</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input
|
||||
contentEditable={false}
|
||||
data-testid="orgURL"
|
||||
value={authConfig?.orgURL}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.orgURL} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.scopes && (
|
||||
<>
|
||||
<Typography.Text>Scopes</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input
|
||||
contentEditable={false}
|
||||
data-testid="scopes"
|
||||
value={authConfig?.scopes.join(',')}
|
||||
/>
|
||||
<CopyToClipboardButton
|
||||
copyText={authConfig?.scopes.join(',')}
|
||||
/>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.domain && (
|
||||
<>
|
||||
<Typography.Text>Domain</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input
|
||||
contentEditable={false}
|
||||
data-testid="domain"
|
||||
value={authConfig?.domain}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.domain} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.authority && (
|
||||
<>
|
||||
<Typography.Text>Authority</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input
|
||||
contentEditable={false}
|
||||
data-testid="authority"
|
||||
value={authConfig?.authority}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.authority} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
{authConfig?.tokenEndpoint && (
|
||||
<>
|
||||
<Typography.Text>TokenEndpoint</Typography.Text>
|
||||
<Space className="w-full tw-justify-between ant-space-authMechanism">
|
||||
<Input
|
||||
contentEditable={false}
|
||||
data-testid="tokenEndpoint"
|
||||
value={authConfig?.tokenEndpoint}
|
||||
/>
|
||||
<CopyToClipboardButton copyText={authConfig?.tokenEndpoint} />
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default AuthMechanism;
|
@ -0,0 +1,542 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Form, Input, Select, Space } from 'antd';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
|
||||
import { SsoServiceType } from '../../generated/entity/teams/authN/ssoAuth';
|
||||
import {
|
||||
AuthenticationMechanism,
|
||||
AuthType,
|
||||
JWTTokenExpiry,
|
||||
} from '../../generated/entity/teams/user';
|
||||
import { Auth0SSOClientConfig } from '../../generated/security/client/auth0SSOClientConfig';
|
||||
import { AzureSSOClientConfig } from '../../generated/security/client/azureSSOClientConfig';
|
||||
import { CustomOidcSSOClientConfig } from '../../generated/security/client/customOidcSSOClientConfig';
|
||||
import { GoogleSSOClientConfig } from '../../generated/security/client/googleSSOClientConfig';
|
||||
import { OktaSSOClientConfig } from '../../generated/security/client/oktaSSOClientConfig';
|
||||
import {
|
||||
getAuthMechanismTypeOptions,
|
||||
getJWTTokenExpiryOptions,
|
||||
} from '../../utils/BotsUtils';
|
||||
import { SSOClientConfig } from '../CreateUser/CreateUser.interface';
|
||||
import Loader from '../Loader/Loader';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface Props {
|
||||
isUpdating: boolean;
|
||||
authenticationMechanism: AuthenticationMechanism;
|
||||
onSave: (updatedAuthMechanism: AuthenticationMechanism) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const AuthMechanismForm: FC<Props> = ({
|
||||
isUpdating,
|
||||
onSave,
|
||||
onCancel,
|
||||
authenticationMechanism,
|
||||
}) => {
|
||||
const { authConfig } = useAuthContext();
|
||||
|
||||
const [authMechanism, setAuthMechanism] = useState<AuthType>(
|
||||
authenticationMechanism.authType ?? AuthType.Jwt
|
||||
);
|
||||
const [tokenExpiry, setTokenExpiry] = useState<JWTTokenExpiry>(
|
||||
authenticationMechanism.config?.JWTTokenExpiry ?? JWTTokenExpiry.OneHour
|
||||
);
|
||||
|
||||
const [ssoClientConfig, setSSOClientConfig] = useState<SSOClientConfig>(
|
||||
(authenticationMechanism.config?.authConfig as SSOClientConfig) ??
|
||||
({} as SSOClientConfig)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const authType = authenticationMechanism.authType;
|
||||
const authConfig = authenticationMechanism.config?.authConfig;
|
||||
const JWTTokenExpiryValue = authenticationMechanism.config?.JWTTokenExpiry;
|
||||
setAuthMechanism(authType ?? AuthType.Jwt);
|
||||
setSSOClientConfig(
|
||||
(authConfig as SSOClientConfig) ?? ({} as SSOClientConfig)
|
||||
);
|
||||
setTokenExpiry(JWTTokenExpiryValue ?? JWTTokenExpiry.OneHour);
|
||||
}, [authenticationMechanism]);
|
||||
|
||||
/**
|
||||
* Handle on change event
|
||||
* @param event
|
||||
*/
|
||||
const handleOnChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
const value = event.target.value;
|
||||
const eleName = event.target.name;
|
||||
|
||||
switch (eleName) {
|
||||
case 'secretKey':
|
||||
case 'audience':
|
||||
case 'clientId':
|
||||
case 'domain':
|
||||
case 'clientSecret':
|
||||
case 'authority':
|
||||
case 'privateKey':
|
||||
case 'orgURL':
|
||||
case 'tokenEndpoint':
|
||||
setSSOClientConfig((previous) => ({
|
||||
...previous,
|
||||
[eleName]: value,
|
||||
}));
|
||||
|
||||
break;
|
||||
|
||||
case 'scopes':
|
||||
setSSOClientConfig((previous) => ({
|
||||
...previous,
|
||||
scopes: value ? value.split(',') : [],
|
||||
}));
|
||||
|
||||
break;
|
||||
|
||||
case 'oktaEmail':
|
||||
setSSOClientConfig((previous) => ({
|
||||
...previous,
|
||||
email: value,
|
||||
}));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const updatedAuthMechanism: AuthenticationMechanism = {
|
||||
authType: authMechanism,
|
||||
config:
|
||||
authMechanism === AuthType.Jwt
|
||||
? {
|
||||
JWTTokenExpiry: tokenExpiry,
|
||||
}
|
||||
: {
|
||||
ssoServiceType: authConfig?.provider as SsoServiceType,
|
||||
authConfig: {
|
||||
...ssoClientConfig,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
onSave(updatedAuthMechanism);
|
||||
};
|
||||
|
||||
const getSSOConfig = () => {
|
||||
switch (authConfig?.provider) {
|
||||
case SsoServiceType.Google: {
|
||||
const googleConfig = ssoClientConfig as GoogleSSOClientConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="SecretKey"
|
||||
name="secretKey"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'SecretKey is required',
|
||||
},
|
||||
]}>
|
||||
<Input.Password
|
||||
data-testid="secretKey"
|
||||
name="secretKey"
|
||||
placeholder="secretKey"
|
||||
value={googleConfig.secretKey}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Audience" name="audience">
|
||||
<Input
|
||||
data-testid="audience"
|
||||
name="audience"
|
||||
placeholder="audience"
|
||||
value={googleConfig.audience}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
case SsoServiceType.Auth0: {
|
||||
const auth0Config = ssoClientConfig as Auth0SSOClientConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="SecretKey"
|
||||
name="secretKey"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'SecretKey is required',
|
||||
},
|
||||
]}>
|
||||
<Input.Password
|
||||
data-testid="secretKey"
|
||||
name="secretKey"
|
||||
placeholder="secretKey"
|
||||
value={auth0Config.secretKey}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="ClientId"
|
||||
name="clientId"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'ClientId is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="clientId"
|
||||
name="clientId"
|
||||
placeholder="clientId"
|
||||
value={auth0Config.clientId}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="domain"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Domain is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="domain"
|
||||
name="domain"
|
||||
placeholder="domain"
|
||||
value={auth0Config.domain}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case SsoServiceType.Azure: {
|
||||
const azureConfig = ssoClientConfig as AzureSSOClientConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="ClientSecret"
|
||||
name="clientSecret"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'ClientSecret is required',
|
||||
},
|
||||
]}>
|
||||
<Input.Password
|
||||
data-testid="clientSecret"
|
||||
name="clientSecret"
|
||||
placeholder="clientSecret"
|
||||
value={azureConfig.clientSecret}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="ClientId"
|
||||
name="clientId"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'ClientId is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="clientId"
|
||||
name="clientId"
|
||||
placeholder="clientId"
|
||||
value={azureConfig.clientId}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Authority"
|
||||
name="authority"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Authority is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="authority"
|
||||
name="authority"
|
||||
placeholder="authority"
|
||||
value={azureConfig.authority}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Scopes"
|
||||
name="scopes"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Scopes is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="scopes"
|
||||
name="scopes"
|
||||
placeholder="Scopes value comma separated"
|
||||
value={azureConfig.scopes.join(',')}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case SsoServiceType.Okta: {
|
||||
const oktaConfig = ssoClientConfig as OktaSSOClientConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="PrivateKey"
|
||||
name="privateKey"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'PrivateKey is required',
|
||||
},
|
||||
]}>
|
||||
<Input.Password
|
||||
data-testid="privateKey"
|
||||
name="privateKey"
|
||||
placeholder="privateKey"
|
||||
value={oktaConfig.privateKey}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="ClientId"
|
||||
name="clientId"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'ClientId is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="clientId"
|
||||
name="clientId"
|
||||
placeholder="clientId"
|
||||
value={oktaConfig.clientId}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="OrgURL"
|
||||
name="orgURL"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'OrgURL is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="orgURL"
|
||||
name="orgURL"
|
||||
placeholder="orgURL"
|
||||
value={oktaConfig.orgURL}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Email"
|
||||
name="oktaEmail"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'email',
|
||||
message: 'Service account Email is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="oktaEmail"
|
||||
name="oktaEmail"
|
||||
placeholder="Okta Service account Email"
|
||||
value={oktaConfig.email}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Scopes" name="scopes">
|
||||
<Input
|
||||
data-testid="scopes"
|
||||
name="scopes"
|
||||
placeholder="Scopes value comma separated"
|
||||
value={oktaConfig.scopes?.join('')}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case SsoServiceType.CustomOidc: {
|
||||
const customOidcConfig = ssoClientConfig as CustomOidcSSOClientConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="SecretKey"
|
||||
name="secretKey"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'SecretKey is required',
|
||||
},
|
||||
]}>
|
||||
<Input.Password
|
||||
data-testid="secretKey"
|
||||
name="secretKey"
|
||||
placeholder="secretKey"
|
||||
value={customOidcConfig.secretKey}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="ClientId"
|
||||
name="clientId"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'ClientId is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="clientId"
|
||||
name="clientId"
|
||||
placeholder="clientId"
|
||||
value={customOidcConfig.clientId}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="TokenEndpoint"
|
||||
name="tokenEndpoint"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'TokenEndpoint is required',
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="tokenEndpoint"
|
||||
name="tokenEndpoint"
|
||||
placeholder="tokenEndpoint"
|
||||
value={customOidcConfig.tokenEndpoint}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
id="update-auth-mechanism-form"
|
||||
layout="vertical"
|
||||
onFinish={handleSave}>
|
||||
<Form.Item
|
||||
label="Auth Mechanism"
|
||||
name="auth-mechanism"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: () => {
|
||||
if (!authMechanism) {
|
||||
return Promise.reject('Auth Mechanism is required');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
className="w-full"
|
||||
data-testid="auth-mechanism"
|
||||
defaultValue={authMechanism}
|
||||
placeholder="Select Auth Mechanism"
|
||||
onChange={(value) => setAuthMechanism(value)}>
|
||||
{getAuthMechanismTypeOptions(authConfig).map((option) => (
|
||||
<Option key={option.value}>{option.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{authMechanism === AuthType.Jwt && (
|
||||
<Form.Item
|
||||
label="Token Expiration"
|
||||
name="token-expiration"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: () => {
|
||||
if (!tokenExpiry) {
|
||||
return Promise.reject('Token Expiration is required');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
className="w-full"
|
||||
data-testid="token-expiry"
|
||||
defaultValue={tokenExpiry}
|
||||
placeholder="Select Token Expiration"
|
||||
onChange={(value) => setTokenExpiry(value)}>
|
||||
{getJWTTokenExpiryOptions().map((option) => (
|
||||
<Option key={option.value}>{option.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{authMechanism === AuthType.Sso && <>{getSSOConfig()}</>}
|
||||
<Space className="w-full tw-justify-end" size={4}>
|
||||
<Button data-testid="cancel-edit" type="link" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="save-edit"
|
||||
form="update-auth-mechanism-form"
|
||||
htmlType="submit"
|
||||
type="primary">
|
||||
{isUpdating ? <Loader size="small" /> : 'Save'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthMechanismForm;
|
@ -12,9 +12,8 @@
|
||||
*/
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Card, Input, Select } from 'antd';
|
||||
import { Card } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
FC,
|
||||
Fragment,
|
||||
@ -23,40 +22,34 @@ import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
|
||||
import { getUserToken, updateUser } from '../../axiosAPIs/userAPI';
|
||||
import {
|
||||
getAuthMechanismForBotUser,
|
||||
updateUser,
|
||||
} from '../../axiosAPIs/userAPI';
|
||||
import {
|
||||
GlobalSettingOptions,
|
||||
GlobalSettingsMenuCategory,
|
||||
} from '../../constants/globalSettings.constants';
|
||||
import {
|
||||
AuthenticationMechanism,
|
||||
AuthType,
|
||||
JWTTokenExpiry,
|
||||
SsoServiceType,
|
||||
User,
|
||||
} from '../../generated/entity/teams/user';
|
||||
import { EntityReference } from '../../generated/type/entityReference';
|
||||
import {
|
||||
getAuthMechanismTypeOptions,
|
||||
getJWTTokenExpiryOptions,
|
||||
getTokenExpiryDate,
|
||||
getTokenExpiryText,
|
||||
} from '../../utils/BotsUtils';
|
||||
import { getEntityName, requiredField } from '../../utils/CommonUtils';
|
||||
import { getEntityName } from '../../utils/CommonUtils';
|
||||
import { getSettingPath } from '../../utils/RouterUtils';
|
||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import { Button } from '../buttons/Button/Button';
|
||||
import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton';
|
||||
import Description from '../common/description/Description';
|
||||
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
|
||||
import PageLayout, { leftPanelAntCardStyle } from '../containers/PageLayout';
|
||||
import { Field } from '../Field/Field';
|
||||
import Loader from '../Loader/Loader';
|
||||
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
|
||||
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
|
||||
import { UserDetails } from '../Users/Users.interface';
|
||||
const { Option } = Select;
|
||||
import AuthMechanism from './AuthMechanism';
|
||||
import AuthMechanismForm from './AuthMechanismForm';
|
||||
|
||||
interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> {
|
||||
botsData: User;
|
||||
botPermission: OperationPermission;
|
||||
@ -64,32 +57,25 @@ interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> {
|
||||
revokeTokenHandler: () => void;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const BotDetails: FC<BotsDetailProp> = ({
|
||||
botsData,
|
||||
updateBotsDetails,
|
||||
revokeTokenHandler,
|
||||
botPermission,
|
||||
}) => {
|
||||
const { authConfig } = useAuthContext();
|
||||
const [displayName, setDisplayName] = useState(botsData.displayName);
|
||||
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
|
||||
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
|
||||
const [botsToken, setBotsToken] = useState<string>('');
|
||||
const [botsTokenExpiry, setBotsTokenExpiry] = useState<number>();
|
||||
const [isRevokingToken, setIsRevokingToken] = useState<boolean>(false);
|
||||
const [generateToken, setGenerateToken] = useState<boolean>(false);
|
||||
|
||||
const [authMechanism, setAuthMechanism] = useState<AuthType>(AuthType.Jwt);
|
||||
const [tokenExpiry, setTokenExpiry] = useState<JWTTokenExpiry>(
|
||||
JWTTokenExpiry.OneHour
|
||||
);
|
||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||
|
||||
const [authenticationMechanism, setAuthenticationMechanism] =
|
||||
useState<AuthenticationMechanism>();
|
||||
|
||||
const [isAuthMechanismEdit, setIsAuthMechanismEdit] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const editAllPermission = useMemo(
|
||||
() => botPermission.EditAll,
|
||||
[botPermission]
|
||||
@ -104,20 +90,18 @@ const BotDetails: FC<BotsDetailProp> = ({
|
||||
[botPermission]
|
||||
);
|
||||
|
||||
const fetchBotsToken = () => {
|
||||
getUserToken(botsData.id)
|
||||
.then((res) => {
|
||||
const { JWTToken, JWTTokenExpiresAt } = res;
|
||||
setBotsToken(JWTToken);
|
||||
setBotsTokenExpiry(JWTTokenExpiresAt);
|
||||
setTokenExpiry(res.JWTTokenExpiry ?? JWTTokenExpiry.OneHour);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(err);
|
||||
});
|
||||
const fetchAuthMechanismForBot = async () => {
|
||||
try {
|
||||
const response = await getAuthMechanismForBotUser(botsData.id);
|
||||
setAuthenticationMechanism(response);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBotTokenDetailUpdate = async () => {
|
||||
const handleAuthMechanismUpdate = async (
|
||||
updatedAuthMechanism: AuthenticationMechanism
|
||||
) => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const {
|
||||
@ -145,30 +129,30 @@ const BotDetails: FC<BotsDetailProp> = ({
|
||||
roles,
|
||||
authenticationMechanism: {
|
||||
...botsData.authenticationMechanism,
|
||||
authType: authMechanism,
|
||||
authType: updatedAuthMechanism.authType,
|
||||
config:
|
||||
authMechanism === AuthType.Jwt
|
||||
updatedAuthMechanism.authType === AuthType.Jwt
|
||||
? {
|
||||
JWTTokenExpiry: tokenExpiry,
|
||||
JWTTokenExpiry: updatedAuthMechanism.config?.JWTTokenExpiry,
|
||||
}
|
||||
: {
|
||||
ssoServiceType: SsoServiceType.Google,
|
||||
authConfig: {},
|
||||
ssoServiceType: updatedAuthMechanism.config?.ssoServiceType,
|
||||
authConfig: updatedAuthMechanism.config?.authConfig,
|
||||
},
|
||||
},
|
||||
} as User);
|
||||
if (response.data) {
|
||||
fetchBotsToken();
|
||||
fetchAuthMechanismForBot();
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
setGenerateToken(false);
|
||||
setIsAuthMechanismEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTokenGeneration = () => setGenerateToken(true);
|
||||
const handleAuthMechanismEdit = () => setIsAuthMechanismEdit(true);
|
||||
|
||||
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDisplayName(e.target.value);
|
||||
@ -297,207 +281,29 @@ const BotDetails: FC<BotsDetailProp> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const fetchRightPanel = () => {
|
||||
return (
|
||||
<Card
|
||||
className="ant-card-feed"
|
||||
style={{
|
||||
...leftPanelAntCardStyle,
|
||||
marginTop: '16px',
|
||||
}}>
|
||||
<div data-testid="right-panel">
|
||||
<div className="tw-flex tw-flex-col">
|
||||
<h6 className="tw-mb-2 tw-text-lg">Token Security</h6>
|
||||
<p className="tw-mb-2">
|
||||
Anyone who has your JWT Token will be able to send REST API
|
||||
requests to the OpenMetadata Server. Do not expose the JWT Token
|
||||
in your application code. Do not share it on GitHub or anywhere
|
||||
else online.
|
||||
</p>
|
||||
</div>
|
||||
const rightPanel = (
|
||||
<Card
|
||||
className="ant-card-feed"
|
||||
style={{
|
||||
...leftPanelAntCardStyle,
|
||||
marginTop: '16px',
|
||||
}}>
|
||||
<div data-testid="right-panel">
|
||||
<div className="tw-flex tw-flex-col">
|
||||
<h6 className="tw-mb-2 tw-text-lg">Token Security</h6>
|
||||
<p className="tw-mb-2">
|
||||
Anyone who has your JWT Token will be able to send REST API requests
|
||||
to the OpenMetadata Server. Do not expose the JWT Token in your
|
||||
application code. Do not share it on GitHub or anywhere else online.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const getCopyComponent = () => {
|
||||
if (botsToken) {
|
||||
return <CopyToClipboardButton copyText={botsToken} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getBotsTokenExpiryDate = () => {
|
||||
if (botsTokenExpiry) {
|
||||
// get the current date timestamp
|
||||
const currentTimeStamp = Date.now();
|
||||
|
||||
const isTokenExpired = currentTimeStamp >= botsTokenExpiry;
|
||||
|
||||
// get the token expiry date
|
||||
const tokenExpiryDate = getTokenExpiryDate(botsTokenExpiry);
|
||||
|
||||
return (
|
||||
<p
|
||||
className="tw-text-grey-muted tw-mt-2 tw-italic"
|
||||
data-testid="token-expiry">
|
||||
{isTokenExpired
|
||||
? `Expired on ${tokenExpiryDate}`
|
||||
: `Expires on ${tokenExpiryDate}`}
|
||||
.
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<p
|
||||
className="tw-text-grey-muted tw-mt-2 tw-italic"
|
||||
data-testid="token-expiry">
|
||||
<SVGIcons alt="warning" icon="error" />
|
||||
<span className="tw-ml-1 tw-align-middle">
|
||||
This token has no expiration date.
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const centerLayout = () => {
|
||||
if (generateToken) {
|
||||
return (
|
||||
<div className="tw-mt-4" data-testid="generate-token-form">
|
||||
<Field>
|
||||
<label
|
||||
className="tw-block tw-form-label tw-mb-0"
|
||||
htmlFor="auth-mechanism">
|
||||
{requiredField('Auth Mechanism')}
|
||||
</label>
|
||||
<Select
|
||||
className="w-full"
|
||||
data-testid="auth-mechanism"
|
||||
defaultValue={authMechanism}
|
||||
placeholder="Select Auth Mechanism"
|
||||
onChange={(value) => setAuthMechanism(value)}>
|
||||
{getAuthMechanismTypeOptions(authConfig).map((option) => (
|
||||
<Option key={option.value}>{option.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<label
|
||||
className="tw-block tw-form-label tw-mb-0"
|
||||
htmlFor="token-expiry">
|
||||
{requiredField('Token Expiration')}
|
||||
</label>
|
||||
<Select
|
||||
className="w-full"
|
||||
data-testid="token-expiry"
|
||||
defaultValue={tokenExpiry}
|
||||
placeholder="Select Token Expiration"
|
||||
onChange={(value) => setTokenExpiry(value)}>
|
||||
{getJWTTokenExpiryOptions().map((option) => (
|
||||
<Option key={option.value}>{option.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
<p className="tw-mt-2">{getTokenExpiryText(tokenExpiry)}</p>
|
||||
</Field>
|
||||
|
||||
<div className="tw-flex tw-justify-end">
|
||||
<Button
|
||||
className={classNames('tw-mr-2')}
|
||||
data-testid="discard-button"
|
||||
size="regular"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
onClick={() => setGenerateToken(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="generate-button"
|
||||
size="regular"
|
||||
theme="primary"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
onClick={handleBotTokenDetailUpdate}>
|
||||
{isUpdating ? <Loader size="small" /> : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (botsToken) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="tw-flex tw-justify-between tw-items-center tw-mt-4">
|
||||
<Input.Password
|
||||
contentEditable={false}
|
||||
data-testid="token"
|
||||
placeholder="Generate new token..."
|
||||
value={botsToken}
|
||||
/>
|
||||
{getCopyComponent()}
|
||||
</div>
|
||||
{getBotsTokenExpiryDate()}
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className="tw-no-description tw-text-sm tw-mt-4"
|
||||
data-testid="no-token">
|
||||
No token available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getCenterLayout = () => {
|
||||
return (
|
||||
<div
|
||||
className="tw-w-full tw-bg-white tw-shadow tw-rounded tw-p-4 tw-mt-4"
|
||||
data-testid="center-panel">
|
||||
<div className="tw-flex tw-justify-between tw-items-center">
|
||||
<h6 className="tw-mb-2 tw-self-center">
|
||||
{generateToken ? 'Generate JWT token' : 'JWT Token'}
|
||||
</h6>
|
||||
{!generateToken && editAllPermission ? (
|
||||
<>
|
||||
{botsToken ? (
|
||||
<Button
|
||||
className="tw-px-2 tw-py-0.5 tw-font-medium tw-ml-2 tw-rounded-md tw-border-error hover:tw-border-error tw-text-error hover:tw-text-error focus:tw-outline-none"
|
||||
data-testid="revoke-button"
|
||||
size="custom"
|
||||
variant="outlined"
|
||||
onClick={() => setIsRevokingToken(true)}>
|
||||
Revoke token
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
data-testid="generate-token"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="outlined"
|
||||
onClick={() => handleTokenGeneration()}>
|
||||
Generate new token
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<hr className="tw-mt-2" />
|
||||
<p className="tw-mt-4">
|
||||
Token you have generated that can be used to access the OpenMetadata
|
||||
API.
|
||||
</p>
|
||||
{centerLayout()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</Card>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (botsData.id) {
|
||||
fetchBotsToken();
|
||||
fetchAuthMechanismForBot();
|
||||
}
|
||||
}, [botsData]);
|
||||
|
||||
@ -520,8 +326,32 @@ const BotDetails: FC<BotsDetailProp> = ({
|
||||
/>
|
||||
}
|
||||
leftPanel={fetchLeftPanel()}
|
||||
rightPanel={fetchRightPanel()}>
|
||||
{getCenterLayout()}
|
||||
rightPanel={rightPanel}>
|
||||
{authenticationMechanism && (
|
||||
<Card
|
||||
data-testid="center-panel"
|
||||
style={{
|
||||
...leftPanelAntCardStyle,
|
||||
marginTop: '16px',
|
||||
}}>
|
||||
{isAuthMechanismEdit ? (
|
||||
<AuthMechanismForm
|
||||
authenticationMechanism={authenticationMechanism}
|
||||
isUpdating={isUpdating}
|
||||
onCancel={() => setIsAuthMechanismEdit(false)}
|
||||
onSave={handleAuthMechanismUpdate}
|
||||
/>
|
||||
) : (
|
||||
<AuthMechanism
|
||||
authenticationMechanism={authenticationMechanism}
|
||||
hasPermission={editAllPermission}
|
||||
onEdit={handleAuthMechanismEdit}
|
||||
onTokenRevoke={() => setIsRevokingToken(true)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isRevokingToken ? (
|
||||
<ConfirmationModal
|
||||
bodyText="Are you sure you want to revoke access for JWT token?"
|
||||
@ -532,7 +362,7 @@ const BotDetails: FC<BotsDetailProp> = ({
|
||||
onConfirm={() => {
|
||||
revokeTokenHandler();
|
||||
setIsRevokingToken(false);
|
||||
handleTokenGeneration();
|
||||
handleAuthMechanismEdit();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -11,29 +11,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
findByTestId,
|
||||
fireEvent,
|
||||
queryByTestId,
|
||||
render,
|
||||
} from '@testing-library/react';
|
||||
import { findByTestId, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { getUserToken } from '../../axiosAPIs/userAPI';
|
||||
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
|
||||
import BotDetails from './BotDetails.component';
|
||||
|
||||
const revokeTokenHandler = jest.fn();
|
||||
const updateBotsDetails = jest.fn();
|
||||
|
||||
const mockToken = {
|
||||
JWTToken:
|
||||
// eslint-disable-next-line max-len
|
||||
'eyJraWQiOiJHYjM4OWEtOWY3Ni1nZGpzLWE5MmotMDI0MmJrOTQzNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzYWNoaW5jaGF1cmFzaXlhY2hvdGV5ODciLCJpc0JvdCI6dHJ1ZSwiaXNzIjoib3Blbi1tZXRhZGF0YS5vcmciLCJleHAiOjE2NTMzMDM5ODcsImlhdCI6MTY1MjY5OTE4NywiZW1haWwiOiJzYWNoaW5jaGF1cmFzaXlhY2hvdGV5ODdAZ21haWwuY29tIn0.qwcyGU_geL9GsZ58lw5H46eP7OY9GNq3gBS5l3DhvOGTjtqWzFBUdtYwg3KdP0ejXHSMW5DD2I-1jbCZI8tuSRZ0kdN7gt0xEhU3o7pweAcDb38mbPB3sgvNTGqrdX9Ya6ICVVDH3v7jVxJuJcykDxfVYFy6fyrwbrW3RxuyacV9xMUIyrD8EyDuAhth4wpwGnj5NqikQFRdqQYEWZlyafskMad4ghMy2eoFjrSc5vv7KN0bkp1SHGjxr_TAd3Oc9lIMWKquUZthGXQnnj5XKxGl1PJnXqK7l3U25DcCobbc5KxOI2_TUxfFNIfxduoHiWsAUBSqshvh7O7nCqiZqw',
|
||||
JWTTokenExpiry: '7',
|
||||
JWTTokenExpiresAt: 1653303987652,
|
||||
};
|
||||
|
||||
const botsData = {
|
||||
id: 'ea09aed1-0251-4a75-b92a-b65641610c53',
|
||||
name: 'sachinchaurasiyachotey87',
|
||||
@ -46,23 +32,20 @@ const botsData = {
|
||||
href: 'http://localhost:8585/api/v1/users/ea09aed1-0251-4a75-b92a-b65641610c53',
|
||||
isBot: true,
|
||||
isAdmin: false,
|
||||
changeDescription: {
|
||||
fieldsAdded: [
|
||||
{
|
||||
name: 'authenticationMechanism',
|
||||
newValue: {
|
||||
config: mockToken,
|
||||
authType: 'JWT',
|
||||
},
|
||||
},
|
||||
],
|
||||
fieldsUpdated: [],
|
||||
fieldsDeleted: [],
|
||||
previousVersion: 0.1,
|
||||
},
|
||||
deleted: false,
|
||||
};
|
||||
|
||||
const mockAuthMechanism = {
|
||||
config: {
|
||||
JWTToken:
|
||||
// eslint-disable-next-line max-len
|
||||
'eyJraWQiOiJHYjM4OWEtOWY3Ni1nZGpzLWE5MmotMDI0MmJrOTQzNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzYWNoaW5jaGF1cmFzaXlhY2hvdGV5ODciLCJpc0JvdCI6dHJ1ZSwiaXNzIjoib3Blbi1tZXRhZGF0YS5vcmciLCJleHAiOjE2NjY3OTE5NjAsImlhdCI6MTY2NDE5OTk2MCwiZW1haWwiOiJzYWNoaW5jaGF1cmFzaXlhY2hvdGV5ODdAZ21haWwuY29tIn0.e5y5hh61EksbcWlLet_GpE84raDYvMho6OXAOLe5MCKrimHYj1roqoY54PFlJDSdrPWJOOeAFsTOxlqnMB_FGhOIufNW9yJwlkIOspWCusNJisLpv8_oYw9ZbrB5ATKyDz9MLTaZRZptx3JirA7s6tV-DJZId-mNzQejW2kiecYZeLZ-ipHqQeVxfzryfxUqcBUGTv-_de0uxlPdklqBuwt24bCy29qVIGxUweFDhrstmdRx_ZyQdrRvmeMHifUB6FCB1OBbII8mKYvF2P0CWF_SsxVLlRHUeOsxKeAeUk1MAA1mHm4UYdMD9OAuFMTZ10gpiELebVWiKrFYYjdICA',
|
||||
JWTTokenExpiry: '30',
|
||||
JWTTokenExpiresAt: 1666791960664,
|
||||
},
|
||||
authType: 'JWT',
|
||||
};
|
||||
|
||||
const mockProp = {
|
||||
botsData,
|
||||
botPermission: {
|
||||
@ -85,9 +68,9 @@ jest.mock('../../utils/PermissionsUtils', () => ({
|
||||
jest.mock('../../axiosAPIs/userAPI', () => {
|
||||
return {
|
||||
updateUser: jest.fn().mockImplementation(() => Promise.resolve(botsData)),
|
||||
getUserToken: jest
|
||||
getAuthMechanismForBotUser: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(mockToken)),
|
||||
.mockImplementation(() => Promise.resolve(mockAuthMechanism)),
|
||||
};
|
||||
});
|
||||
|
||||
@ -125,54 +108,6 @@ describe('Test BotsDetail Component', () => {
|
||||
expect(tokenExpiry).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render no token placeholder if token is not present', async () => {
|
||||
(getUserToken as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockToken,
|
||||
JWTToken: '',
|
||||
JWTTokenExpiresAt: '',
|
||||
})
|
||||
);
|
||||
const { container } = render(<BotDetails {...mockProp} />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const tokenElement = queryByTestId(container, 'token');
|
||||
const tokenExpiry = queryByTestId(container, 'token-expiry');
|
||||
|
||||
expect(tokenElement).not.toBeInTheDocument();
|
||||
expect(tokenExpiry).not.toBeInTheDocument();
|
||||
|
||||
const noToken = await findByTestId(container, 'no-token');
|
||||
|
||||
expect(noToken).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render generate token form if generate token button is clicked', async () => {
|
||||
(getUserToken as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({ ...mockToken, JWTToken: '', JWTTokenExpiresAt: '' })
|
||||
);
|
||||
const { container } = render(<BotDetails {...mockProp} />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const generateToken = await findByTestId(container, 'generate-token');
|
||||
|
||||
expect(generateToken).toHaveTextContent('Generate new token');
|
||||
|
||||
fireEvent.click(generateToken);
|
||||
|
||||
const tokenForm = await findByTestId(container, 'generate-token-form');
|
||||
|
||||
expect(tokenForm).toBeInTheDocument();
|
||||
|
||||
const generateButton = await findByTestId(tokenForm, 'generate-button');
|
||||
const discardButton = await findByTestId(tokenForm, 'discard-button');
|
||||
|
||||
expect(generateButton).toBeInTheDocument();
|
||||
expect(discardButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test Revoke token flow', async () => {
|
||||
const { container } = render(<BotDetails {...mockProp} />, {
|
||||
wrapper: MemoryRouter,
|
||||
|
@ -100,10 +100,18 @@ export const getTokenExpiryText = (expiry: string) => {
|
||||
/**
|
||||
*
|
||||
* @param expiry expiry timestamp
|
||||
* @returns date like "Fri 23rd September, 2022,02:26 PM."
|
||||
* @returns TokenExpiry
|
||||
*/
|
||||
export const getTokenExpiryDate = (expiry: number) => {
|
||||
return moment(expiry).format('ddd Do MMMM, YYYY,hh:mm A');
|
||||
export const getTokenExpiry = (expiry: number) => {
|
||||
// get the current date timestamp
|
||||
const currentTimeStamp = Date.now();
|
||||
|
||||
const isTokenExpired = currentTimeStamp >= expiry;
|
||||
|
||||
return {
|
||||
tokenExpiryDate: moment(expiry).format('ddd Do MMMM, YYYY,hh:mm A'),
|
||||
isTokenExpired,
|
||||
};
|
||||
};
|
||||
|
||||
export const DEFAULT_GOOGLE_SSO_CLIENT_CONFIG = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user