mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 18:36:08 +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 { Operation } from 'fast-json-patch';
|
||||||
import { isNil, isUndefined } from 'lodash';
|
import { isNil, isUndefined } from 'lodash';
|
||||||
import { SearchIndex } from '../enums/search.enum';
|
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 { JwtAuth } from '../generated/entity/teams/authN/jwtAuth';
|
||||||
import { User } from '../generated/entity/teams/user';
|
import { User } from '../generated/entity/teams/user';
|
||||||
import { EntityReference } from '../generated/type/entityReference';
|
import { EntityReference } from '../generated/type/entityReference';
|
||||||
@ -185,3 +188,11 @@ export const getGroupTypeTeams = async () => {
|
|||||||
|
|
||||||
return response.data;
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Card, Input, Select } from 'antd';
|
import { Card } from 'antd';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, {
|
import React, {
|
||||||
FC,
|
FC,
|
||||||
Fragment,
|
Fragment,
|
||||||
@ -23,40 +22,34 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
|
import {
|
||||||
import { getUserToken, updateUser } from '../../axiosAPIs/userAPI';
|
getAuthMechanismForBotUser,
|
||||||
|
updateUser,
|
||||||
|
} from '../../axiosAPIs/userAPI';
|
||||||
import {
|
import {
|
||||||
GlobalSettingOptions,
|
GlobalSettingOptions,
|
||||||
GlobalSettingsMenuCategory,
|
GlobalSettingsMenuCategory,
|
||||||
} from '../../constants/globalSettings.constants';
|
} from '../../constants/globalSettings.constants';
|
||||||
import {
|
import {
|
||||||
|
AuthenticationMechanism,
|
||||||
AuthType,
|
AuthType,
|
||||||
JWTTokenExpiry,
|
|
||||||
SsoServiceType,
|
|
||||||
User,
|
User,
|
||||||
} from '../../generated/entity/teams/user';
|
} from '../../generated/entity/teams/user';
|
||||||
import { EntityReference } from '../../generated/type/entityReference';
|
import { EntityReference } from '../../generated/type/entityReference';
|
||||||
import {
|
import { getEntityName } from '../../utils/CommonUtils';
|
||||||
getAuthMechanismTypeOptions,
|
|
||||||
getJWTTokenExpiryOptions,
|
|
||||||
getTokenExpiryDate,
|
|
||||||
getTokenExpiryText,
|
|
||||||
} from '../../utils/BotsUtils';
|
|
||||||
import { getEntityName, requiredField } from '../../utils/CommonUtils';
|
|
||||||
import { getSettingPath } from '../../utils/RouterUtils';
|
import { getSettingPath } from '../../utils/RouterUtils';
|
||||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||||
import { showErrorToast } from '../../utils/ToastUtils';
|
import { showErrorToast } from '../../utils/ToastUtils';
|
||||||
import { Button } from '../buttons/Button/Button';
|
import { Button } from '../buttons/Button/Button';
|
||||||
import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton';
|
|
||||||
import Description from '../common/description/Description';
|
import Description from '../common/description/Description';
|
||||||
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
|
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
|
||||||
import PageLayout, { leftPanelAntCardStyle } from '../containers/PageLayout';
|
import PageLayout, { leftPanelAntCardStyle } from '../containers/PageLayout';
|
||||||
import { Field } from '../Field/Field';
|
|
||||||
import Loader from '../Loader/Loader';
|
|
||||||
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
|
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
|
||||||
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
|
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
|
||||||
import { UserDetails } from '../Users/Users.interface';
|
import { UserDetails } from '../Users/Users.interface';
|
||||||
const { Option } = Select;
|
import AuthMechanism from './AuthMechanism';
|
||||||
|
import AuthMechanismForm from './AuthMechanismForm';
|
||||||
|
|
||||||
interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> {
|
interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> {
|
||||||
botsData: User;
|
botsData: User;
|
||||||
botPermission: OperationPermission;
|
botPermission: OperationPermission;
|
||||||
@ -64,32 +57,25 @@ interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> {
|
|||||||
revokeTokenHandler: () => void;
|
revokeTokenHandler: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Option {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BotDetails: FC<BotsDetailProp> = ({
|
const BotDetails: FC<BotsDetailProp> = ({
|
||||||
botsData,
|
botsData,
|
||||||
updateBotsDetails,
|
updateBotsDetails,
|
||||||
revokeTokenHandler,
|
revokeTokenHandler,
|
||||||
botPermission,
|
botPermission,
|
||||||
}) => {
|
}) => {
|
||||||
const { authConfig } = useAuthContext();
|
|
||||||
const [displayName, setDisplayName] = useState(botsData.displayName);
|
const [displayName, setDisplayName] = useState(botsData.displayName);
|
||||||
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
|
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
|
||||||
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
|
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
|
||||||
const [botsToken, setBotsToken] = useState<string>('');
|
|
||||||
const [botsTokenExpiry, setBotsTokenExpiry] = useState<number>();
|
|
||||||
const [isRevokingToken, setIsRevokingToken] = useState<boolean>(false);
|
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 [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [authenticationMechanism, setAuthenticationMechanism] =
|
||||||
|
useState<AuthenticationMechanism>();
|
||||||
|
|
||||||
|
const [isAuthMechanismEdit, setIsAuthMechanismEdit] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
const editAllPermission = useMemo(
|
const editAllPermission = useMemo(
|
||||||
() => botPermission.EditAll,
|
() => botPermission.EditAll,
|
||||||
[botPermission]
|
[botPermission]
|
||||||
@ -104,20 +90,18 @@ const BotDetails: FC<BotsDetailProp> = ({
|
|||||||
[botPermission]
|
[botPermission]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchBotsToken = () => {
|
const fetchAuthMechanismForBot = async () => {
|
||||||
getUserToken(botsData.id)
|
try {
|
||||||
.then((res) => {
|
const response = await getAuthMechanismForBotUser(botsData.id);
|
||||||
const { JWTToken, JWTTokenExpiresAt } = res;
|
setAuthenticationMechanism(response);
|
||||||
setBotsToken(JWTToken);
|
} catch (error) {
|
||||||
setBotsTokenExpiry(JWTTokenExpiresAt);
|
showErrorToast(error as AxiosError);
|
||||||
setTokenExpiry(res.JWTTokenExpiry ?? JWTTokenExpiry.OneHour);
|
}
|
||||||
})
|
|
||||||
.catch((err: AxiosError) => {
|
|
||||||
showErrorToast(err);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBotTokenDetailUpdate = async () => {
|
const handleAuthMechanismUpdate = async (
|
||||||
|
updatedAuthMechanism: AuthenticationMechanism
|
||||||
|
) => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@ -145,30 +129,30 @@ const BotDetails: FC<BotsDetailProp> = ({
|
|||||||
roles,
|
roles,
|
||||||
authenticationMechanism: {
|
authenticationMechanism: {
|
||||||
...botsData.authenticationMechanism,
|
...botsData.authenticationMechanism,
|
||||||
authType: authMechanism,
|
authType: updatedAuthMechanism.authType,
|
||||||
config:
|
config:
|
||||||
authMechanism === AuthType.Jwt
|
updatedAuthMechanism.authType === AuthType.Jwt
|
||||||
? {
|
? {
|
||||||
JWTTokenExpiry: tokenExpiry,
|
JWTTokenExpiry: updatedAuthMechanism.config?.JWTTokenExpiry,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
ssoServiceType: SsoServiceType.Google,
|
ssoServiceType: updatedAuthMechanism.config?.ssoServiceType,
|
||||||
authConfig: {},
|
authConfig: updatedAuthMechanism.config?.authConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as User);
|
} as User);
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
fetchBotsToken();
|
fetchAuthMechanismForBot();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error as AxiosError);
|
showErrorToast(error as AxiosError);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
setGenerateToken(false);
|
setIsAuthMechanismEdit(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTokenGeneration = () => setGenerateToken(true);
|
const handleAuthMechanismEdit = () => setIsAuthMechanismEdit(true);
|
||||||
|
|
||||||
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setDisplayName(e.target.value);
|
setDisplayName(e.target.value);
|
||||||
@ -297,207 +281,29 @@ const BotDetails: FC<BotsDetailProp> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchRightPanel = () => {
|
const rightPanel = (
|
||||||
return (
|
<Card
|
||||||
<Card
|
className="ant-card-feed"
|
||||||
className="ant-card-feed"
|
style={{
|
||||||
style={{
|
...leftPanelAntCardStyle,
|
||||||
...leftPanelAntCardStyle,
|
marginTop: '16px',
|
||||||
marginTop: '16px',
|
}}>
|
||||||
}}>
|
<div data-testid="right-panel">
|
||||||
<div data-testid="right-panel">
|
<div className="tw-flex tw-flex-col">
|
||||||
<div className="tw-flex tw-flex-col">
|
<h6 className="tw-mb-2 tw-text-lg">Token Security</h6>
|
||||||
<h6 className="tw-mb-2 tw-text-lg">Token Security</h6>
|
<p className="tw-mb-2">
|
||||||
<p className="tw-mb-2">
|
Anyone who has your JWT Token will be able to send REST API requests
|
||||||
Anyone who has your JWT Token will be able to send REST API
|
to the OpenMetadata Server. Do not expose the JWT Token in your
|
||||||
requests to the OpenMetadata Server. Do not expose the JWT Token
|
application code. Do not share it on GitHub or anywhere else online.
|
||||||
in your application code. Do not share it on GitHub or anywhere
|
</p>
|
||||||
else online.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
</Card>
|
||||||
};
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (botsData.id) {
|
if (botsData.id) {
|
||||||
fetchBotsToken();
|
fetchAuthMechanismForBot();
|
||||||
}
|
}
|
||||||
}, [botsData]);
|
}, [botsData]);
|
||||||
|
|
||||||
@ -520,8 +326,32 @@ const BotDetails: FC<BotsDetailProp> = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
leftPanel={fetchLeftPanel()}
|
leftPanel={fetchLeftPanel()}
|
||||||
rightPanel={fetchRightPanel()}>
|
rightPanel={rightPanel}>
|
||||||
{getCenterLayout()}
|
{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 ? (
|
{isRevokingToken ? (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
bodyText="Are you sure you want to revoke access for JWT token?"
|
bodyText="Are you sure you want to revoke access for JWT token?"
|
||||||
@ -532,7 +362,7 @@ const BotDetails: FC<BotsDetailProp> = ({
|
|||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
revokeTokenHandler();
|
revokeTokenHandler();
|
||||||
setIsRevokingToken(false);
|
setIsRevokingToken(false);
|
||||||
handleTokenGeneration();
|
handleAuthMechanismEdit();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -11,29 +11,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { findByTestId, fireEvent, render } from '@testing-library/react';
|
||||||
findByTestId,
|
|
||||||
fireEvent,
|
|
||||||
queryByTestId,
|
|
||||||
render,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { getUserToken } from '../../axiosAPIs/userAPI';
|
|
||||||
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
|
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
|
||||||
import BotDetails from './BotDetails.component';
|
import BotDetails from './BotDetails.component';
|
||||||
|
|
||||||
const revokeTokenHandler = jest.fn();
|
const revokeTokenHandler = jest.fn();
|
||||||
const updateBotsDetails = 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 = {
|
const botsData = {
|
||||||
id: 'ea09aed1-0251-4a75-b92a-b65641610c53',
|
id: 'ea09aed1-0251-4a75-b92a-b65641610c53',
|
||||||
name: 'sachinchaurasiyachotey87',
|
name: 'sachinchaurasiyachotey87',
|
||||||
@ -46,23 +32,20 @@ const botsData = {
|
|||||||
href: 'http://localhost:8585/api/v1/users/ea09aed1-0251-4a75-b92a-b65641610c53',
|
href: 'http://localhost:8585/api/v1/users/ea09aed1-0251-4a75-b92a-b65641610c53',
|
||||||
isBot: true,
|
isBot: true,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
changeDescription: {
|
|
||||||
fieldsAdded: [
|
|
||||||
{
|
|
||||||
name: 'authenticationMechanism',
|
|
||||||
newValue: {
|
|
||||||
config: mockToken,
|
|
||||||
authType: 'JWT',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
fieldsUpdated: [],
|
|
||||||
fieldsDeleted: [],
|
|
||||||
previousVersion: 0.1,
|
|
||||||
},
|
|
||||||
deleted: false,
|
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 = {
|
const mockProp = {
|
||||||
botsData,
|
botsData,
|
||||||
botPermission: {
|
botPermission: {
|
||||||
@ -85,9 +68,9 @@ jest.mock('../../utils/PermissionsUtils', () => ({
|
|||||||
jest.mock('../../axiosAPIs/userAPI', () => {
|
jest.mock('../../axiosAPIs/userAPI', () => {
|
||||||
return {
|
return {
|
||||||
updateUser: jest.fn().mockImplementation(() => Promise.resolve(botsData)),
|
updateUser: jest.fn().mockImplementation(() => Promise.resolve(botsData)),
|
||||||
getUserToken: jest
|
getAuthMechanismForBotUser: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(() => Promise.resolve(mockToken)),
|
.mockImplementation(() => Promise.resolve(mockAuthMechanism)),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -125,54 +108,6 @@ describe('Test BotsDetail Component', () => {
|
|||||||
expect(tokenExpiry).toBeInTheDocument();
|
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 () => {
|
it('Test Revoke token flow', async () => {
|
||||||
const { container } = render(<BotDetails {...mockProp} />, {
|
const { container } = render(<BotDetails {...mockProp} />, {
|
||||||
wrapper: MemoryRouter,
|
wrapper: MemoryRouter,
|
||||||
|
@ -100,10 +100,18 @@ export const getTokenExpiryText = (expiry: string) => {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param expiry expiry timestamp
|
* @param expiry expiry timestamp
|
||||||
* @returns date like "Fri 23rd September, 2022,02:26 PM."
|
* @returns TokenExpiry
|
||||||
*/
|
*/
|
||||||
export const getTokenExpiryDate = (expiry: number) => {
|
export const getTokenExpiry = (expiry: number) => {
|
||||||
return moment(expiry).format('ddd Do MMMM, YYYY,hh:mm A');
|
// 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 = {
|
export const DEFAULT_GOOGLE_SSO_CLIENT_CONFIG = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user