#14114: Support for personal access token (#14247)

* added user access token

* added locale files

* Add invalidate call when calling api for personal token

* added test cases

* refactor code

* code refactor

* change styling

* added test cases and code refactor

* added locale files

* fix failing test

* code refactor

* code refactor

* added common comp in bots page

* fix cypress tests

* add cypress tests

* added locale files

---------

Co-authored-by: mohitdeuex <mohit.y@deuexsolutions.com>
Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Harsh Vador 2023-12-13 18:25:22 +05:30 committed by GitHub
parent a40b651cc0
commit c2b0c2687d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 718 additions and 202 deletions

View File

@ -1129,6 +1129,7 @@ public class UserResource extends EntityResource<User, UserRepository> {
null);
PersonalAccessToken personalAccessToken = TokenUtil.getPersonalAccessToken(tokenRequest, user, authMechanism);
tokenRepository.insertToken(personalAccessToken);
UserTokenCache.invalidateToken(user.getName());
return Response.status(Response.Status.OK).entity(personalAccessToken).build();
}
throw new CustomExceptionMessage(BAD_REQUEST, "Bots cannot have a Personal Access Token.");

View File

@ -40,29 +40,23 @@ const expirationTime = {
const getCreatedBot = () => {
interceptURL('GET', `/api/v1/bots/name/${botName}*`, 'getCreatedBot');
// Click on created Bot name
cy.get(`[data-testid="bot-link-${botName}"]`)
.should('exist')
.should('be.visible')
.click();
cy.get(`[data-testid="bot-link-${botName}"]`).should('exist').click();
verifyResponseStatusCode('@getCreatedBot', 200);
};
const revokeToken = () => {
// Click on revoke button
cy.get('[data-testid="revoke-button"]')
.should('contain', 'Revoke token')
.should('be.visible')
.click();
cy.get('[data-testid="revoke-button"]').click();
// Verify the revoke text
cy.get('[data-testid="body-text"]').should(
'contain',
'Are you sure you want to revoke access for JWT token?'
);
interceptURL('PUT', `/api/v1/users/revokeToken`, 'revokeToken');
// Click on confirm button
cy.get('[data-testid="save-button"]')
.should('exist')
.should('be.visible')
.click();
cy.get('[data-testid="save-button"]').click();
verifyResponseStatusCode('@revokeToken', 200);
// Verify the revoke is successful
cy.get('[data-testid="revoke-button"]').should('not.exist');
cy.get('[data-testid="auth-mechanism"]')
@ -143,15 +137,11 @@ describe('Bots Page should work properly', () => {
Object.values(expirationTime).forEach((expiry) => {
it(`Update token expiration for ${expiry} days`, () => {
getCreatedBot();
revokeToken();
// Click on token expiry dropdown
cy.get('[data-testid="token-expiry"]').should('be.visible').click();
// Click on dropdown
cy.get('[data-testid="token-expiry"]').click();
// Select the expiration period
cy.contains(`${expiry} days`)
.should('exist')
.should('be.visible')
.click();
cy.contains(`${expiry} days`).should('exist').click();
// Save the updated date
const expiryDate = customFormatDateTime(
getEpochMillisForFutureDays(expiry),
@ -173,17 +163,11 @@ describe('Bots Page should work properly', () => {
getCreatedBot();
revokeToken();
// Click on expiry token dropdown
cy.get('[data-testid="token-expiry"]')
.should('exist')
.should('be.visible')
.click();
cy.get('[data-testid="token-expiry"]').click();
// Select unlimited days
cy.contains('Unlimited days').should('exist').should('be.visible').click();
cy.contains('Unlimited days').click();
// Save the selected changes
cy.get('[data-testid="save-edit"]')
.should('exist')
.should('be.visible')
.click();
cy.get('[data-testid="save-edit"]').click();
// Verify the updated expiry time
cy.get('[data-testid="center-panel"]')
.find('[data-testid="revoke-button"]')

View File

@ -12,6 +12,10 @@
*/
// eslint-disable-next-line spaced-comment
/// <reference types="cypress" />
import {
customFormatDateTime,
getEpochMillisForFutureDays,
} from '../../../src/utils/date-time/DateTimeUtils';
import {
addUser,
deleteSoftDeletedUser,
@ -30,6 +34,28 @@ const adminEmail = `${adminName}@gmail.com`;
const searchBotText = 'bot';
const expirationTime = {
oneday: '1',
sevendays: '7',
onemonth: '30',
twomonths: '60',
threemonths: '90',
};
const revokeToken = () => {
// Click on revoke button
cy.get('[data-testid="revoke-button"]').should('be.visible').click();
// Verify the revoke text
cy.get('[data-testid="body-text"]').should(
'contain',
'Are you sure you want to revoke access for Personal Access Token?'
);
// Click on confirm button
cy.get('[data-testid="save-button"]').click();
// Verify the revoke is successful
cy.get('[data-testid="revoke-button"]').should('not.exist');
};
describe('Users flow should work properly', () => {
beforeEach(() => {
cy.login();
@ -134,4 +160,68 @@ describe('Admin flow should work properly', () => {
softDeleteUser(adminName, true);
deleteSoftDeletedUser(adminName);
});
describe('Personal Access Token flow should work properly', () => {
beforeEach(() => {
cy.login();
});
it('Token should be generated and revoked', function () {
// Enter profile section
cy.get('.username').click();
cy.get('[data-testid="user-name"] > .ant-typography').click();
cy.get('[data-testid="access-token"] > .ant-space-item').click();
cy.get('[data-testid="access-token"] > .ant-space-item').should(
'be.visible'
);
// generate token
cy.get('[data-testid="no-token"]').should('be.visible');
cy.get('[data-testid="auth-mechanism"] > span').click();
cy.get('[data-testid="token-expiry"]').should('be.visible').click();
cy.contains('1 hr').should('exist').should('be.visible').click();
cy.get('[data-testid="token-expiry"]').should('be.visible');
cy.get('[data-testid="save-edit"]').should('be.visible').click();
// revoke token
cy.get('[data-testid="revoke-button"] > span').should('be.visible');
cy.get('[data-testid="revoke-button"] > span').click();
cy.get('[data-testid="save-button"] > span').click();
cy.get(':nth-child(1) > .ant-row > .ant-form-item-label > label').should(
'be.visible'
);
});
Object.values(expirationTime).forEach((expiry) => {
it(`Update token expiration for ${expiry} days`, () => {
cy.get('.username').click();
cy.get('[data-testid="user-name"] > .ant-typography').click();
cy.get('[data-testid="access-token"] > .ant-space-item').click();
cy.get('[data-testid="access-token"] > .ant-space-item').should(
'be.visible'
);
cy.get('[data-testid="no-token"]').should('be.visible');
cy.get('[data-testid="auth-mechanism"] > span').click();
cy.get('[data-testid="token-expiry"]').click();
// Select the expiration period
cy.contains(`${expiry} days`).click();
// Save the updated date
const expiryDate = customFormatDateTime(
getEpochMillisForFutureDays(expiry),
`ccc d'th' MMMM, yyyy`
);
cy.get('[data-testid="save-edit"]').click();
cy.get('[data-testid="center-panel"]')
.find('[data-testid="revoke-button"]')
.should('be.visible');
// Verify the expiry time
cy.get('[data-testid="token-expiry"]')
.invoke('text')
.should('contain', `Expires on ${expiryDate}`);
cy.get('[data-testid="token-expiry"]').click();
revokeToken();
});
});
});
});

View File

@ -0,0 +1,254 @@
/*
* Copyright 2023 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 { Card } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { t } from 'i18next';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import {
PersonalAccessToken,
TokenType,
} from '../../generated/auth/personalAccessToken';
import {
AuthenticationMechanism,
AuthType,
} from '../../generated/entity/teams/user';
import { createBotWithPut } from '../../rest/botsAPI';
import {
createUserWithPut,
getAuthMechanismForBotUser,
getUserAccessToken,
revokeAccessToken,
updateUserAccessToken,
} from '../../rest/userAPI';
import { showErrorToast } from '../../utils/ToastUtils';
import AuthMechanism from '../BotDetails/AuthMechanism';
import AuthMechanismForm from '../BotDetails/AuthMechanismForm';
import Loader from '../Loader/Loader';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import { MockProps } from './AccessTokenCard.interfaces';
const AccessTokenCard: FC<MockProps> = ({
isBot,
botData,
botUserData,
revokeTokenHandlerBot,
}: MockProps) => {
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isAuthMechanismEdit, setIsAuthMechanismEdit] =
useState<boolean>(false);
const [authenticationMechanism, setAuthenticationMechanism] =
useState<PersonalAccessToken>({
tokenType: TokenType.PersonalAccessToken,
} as PersonalAccessToken);
const [authenticationMechanismBot, setAuthenticationMechanismBot] =
useState<AuthenticationMechanism>({
authType: AuthType.Jwt,
} as AuthenticationMechanism);
const handleAuthMechanismEdit = () => setIsAuthMechanismEdit(true);
const fetchAuthMechanismForUser = async () => {
try {
const response = await getUserAccessToken();
if (response.length) {
setAuthenticationMechanism(response[0]);
}
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const fetchAuthMechanismForBot = async () => {
try {
setIsLoading(true);
if (botUserData) {
const response = await getAuthMechanismForBotUser(botUserData.id);
setAuthenticationMechanismBot(response);
}
} catch (error) {
showErrorToast(error as AxiosError);
setIsLoading(false);
} finally {
setIsLoading(false);
}
};
const handleAuthMechanismUpdateForBot = async (
updatedAuthMechanism: AuthenticationMechanism
) => {
setIsUpdating(true);
if (botUserData && botData) {
try {
const {
isAdmin,
timezone,
name,
description,
displayName,
profile,
email,
isBot,
} = botUserData;
const response = await createUserWithPut({
isAdmin,
timezone,
name,
description,
displayName,
profile,
email,
isBot,
authenticationMechanism: {
...botUserData.authenticationMechanism,
authType: updatedAuthMechanism.authType,
config:
updatedAuthMechanism.authType === AuthType.Jwt
? {
JWTTokenExpiry: updatedAuthMechanism.config?.JWTTokenExpiry,
}
: {
ssoServiceType: updatedAuthMechanism.config?.ssoServiceType,
authConfig: updatedAuthMechanism.config?.authConfig,
},
},
botName: botData.name,
});
if (response) {
if (botData) {
await createBotWithPut({
name: botData.name,
description: botData.description,
displayName: botData.displayName,
botUser: response.name,
});
fetchAuthMechanismForBot();
}
}
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsUpdating(false);
setIsAuthMechanismEdit(false);
}
}
};
const handleAuthMechanismUpdate = async (data: AuthenticationMechanism) => {
if (data.config) {
setIsUpdating(true);
try {
const response = await updateUserAccessToken({
JWTTokenExpiry: data.config.JWTTokenExpiry,
tokenName: 'test',
});
if (response) {
setAuthenticationMechanism(response[0]);
fetchAuthMechanismForUser();
}
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsUpdating(false);
setIsAuthMechanismEdit(false);
}
}
};
const revokeTokenHandler = () => {
revokeAccessToken('removeAll=true')
.then(() => setIsModalOpen(false))
.catch((err: AxiosError) => {
showErrorToast(err);
});
};
useEffect(() => {
fetchAuthMechanismForUser();
}, [isModalOpen]);
useEffect(() => {
if (botUserData && botUserData.id) {
fetchAuthMechanismForBot();
}
}, [botUserData]);
const authenticationMechanismData = useMemo(() => {
return isBot ? authenticationMechanismBot : authenticationMechanism;
}, [isBot, authenticationMechanismBot, authenticationMechanism]);
const onSave = useMemo(() => {
return isBot ? handleAuthMechanismUpdateForBot : handleAuthMechanismUpdate;
}, [isBot, handleAuthMechanismUpdateForBot, handleAuthMechanismUpdateForBot]);
const tokenRevoke = useCallback(() => {
return isBot ? revokeTokenHandlerBot?.() : revokeTokenHandler();
}, [isBot, revokeTokenHandlerBot, revokeTokenHandler]);
const confirmMessage = useMemo(() => {
return isBot
? t('message.are-you-sure-to-revoke-access')
: t('message.are-you-sure-to-revoke-access-personal-access');
}, [isBot]);
return isLoading ? (
<Loader />
) : (
<Card
className={classNames(
'm-t-md',
isBot ? 'page-layout-v1-left-panel mt-2 ' : 'p-md m-l-md m-r-lg w-auto'
)}
data-testid="center-panel">
<>
{isAuthMechanismEdit ? (
<AuthMechanismForm
authenticationMechanism={authenticationMechanismData}
isBot={isBot}
isUpdating={isUpdating}
onCancel={() => setIsAuthMechanismEdit(false)}
onSave={onSave}
/>
) : (
<AuthMechanism
hasPermission
authenticationMechanism={authenticationMechanismData}
isBot={isBot}
onEdit={handleAuthMechanismEdit}
onTokenRevoke={() => setIsModalOpen(true)}
/>
)}
</>
<ConfirmationModal
bodyText={confirmMessage}
cancelText={t('label.cancel')}
confirmText={t('label.confirm')}
header={t('message.are-you-sure')}
visible={isModalOpen}
onCancel={() => setIsModalOpen(false)}
onConfirm={() => {
tokenRevoke();
handleAuthMechanismEdit();
setIsModalOpen(false);
}}
/>
</Card>
);
};
export default AccessTokenCard;

View File

@ -0,0 +1,22 @@
/*
* Copyright 2023 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 { Bot } from '../../generated/entity/bot';
import { User } from '../../generated/entity/teams/user';
export type MockProps = {
isBot: boolean;
botData?: Bot;
botUserData?: User;
revokeTokenHandlerBot?: () => void;
};

View File

@ -0,0 +1,116 @@
/*
* Copyright 2023 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 '@testing-library/jest-dom/extend-expect';
import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { revokeAccessToken } from '../../rest/userAPI';
import { mockAccessData } from '../Users/mocks/User.mocks';
import AccessTokenCard from './AccessTokenCard.component';
jest.mock('../BotDetails/AuthMechanism', () => {
return jest.fn().mockImplementation(({ onTokenRevoke }) => (
<p>
AuthMechanism{' '}
<button data-testid="open-modal-button" onClick={onTokenRevoke}>
open
</button>
</p>
));
});
jest.mock('../BotDetails/AuthMechanismForm', () => {
return jest.fn().mockReturnValue(<p>AuthMechanismForm</p>);
});
jest.mock('../Modals/ConfirmationModal/ConfirmationModal', () => {
return jest.fn().mockImplementation(({ onConfirm, onCancel, visible }) =>
visible ? (
<div data-testid="confirmation-modal">
<button data-testid="cancel-button" onClick={onCancel}>
Cancel
</button>
<button data-testid="confirm-button" onClick={onConfirm}>
Confirm
</button>
</div>
) : (
<div data-testid="closed-confirmation-modal" />
)
);
});
jest.mock('../../rest/userAPI', () => {
return {
getUserAccessToken: jest
.fn()
.mockImplementation(() => Promise.resolve([mockAccessData])),
updateUserAccessToken: jest.fn().mockImplementation(() =>
Promise.resolve({
JWTTokenExpiry: 'tesst',
tokenName: 'test',
})
),
revokeAccessToken: jest.fn().mockImplementation(() => Promise.resolve()),
};
});
describe('AccessTokenCard Component', () => {
it('should render initial state with AuthMechanism', async () => {
render(<AccessTokenCard isBot={false} />);
expect(screen.getByText('AuthMechanism')).toBeInTheDocument();
});
it('should render AuthMechanism when isAuthMechanismEdit is false', async () => {
await act(async () => {
render(<AccessTokenCard isBot={false} />);
});
expect(await screen.findByText('AuthMechanism')).toBeInTheDocument();
});
it('should call onConfirm when Confirm button is clicked', async () => {
const mockRevokeAccessToken = revokeAccessToken as jest.Mock;
render(<AccessTokenCard isBot={false} />);
const isModalClose = await screen.findByTestId('closed-confirmation-modal');
expect(isModalClose).toBeInTheDocument();
const openModalButton = await screen.findByTestId('open-modal-button');
await act(async () => {
fireEvent.click(openModalButton);
});
const confirmButton = await screen.findByTestId('confirm-button');
fireEvent.click(confirmButton);
expect(mockRevokeAccessToken).toHaveBeenCalled();
});
it('should call onCancel when Cancel button is clicked', async () => {
await render(<AccessTokenCard isBot={false} />);
const isModalClose = await screen.getByTestId('closed-confirmation-modal');
expect(isModalClose).toBeInTheDocument();
const openModalButton = await screen.getByTestId('open-modal-button');
await act(async () => {
fireEvent.click(openModalButton);
});
const cancelButton = await screen.getByTestId('cancel-button');
fireEvent.click(cancelButton);
const closedModal = await screen.getByTestId('closed-confirmation-modal');
expect(closedModal).toBeInTheDocument();
});
});

View File

@ -13,7 +13,8 @@
import { Button, Divider, Input, Space, Typography } from 'antd';
import { t } from 'i18next';
import React, { FC } from 'react';
import React, { FC, useMemo } from 'react';
import { PersonalAccessToken } from '../../generated/auth/personalAccessToken';
import { AuthenticationMechanism } from '../../generated/entity/teams/user';
import { getTokenExpiry } from '../../utils/BotsUtils';
import SVGIcons from '../../utils/SvgUtils';
@ -21,10 +22,11 @@ import CopyToClipboardButton from '../CopyToClipboardButton/CopyToClipboardButto
import './auth-mechanism.less';
interface Props {
authenticationMechanism: AuthenticationMechanism;
authenticationMechanism: AuthenticationMechanism | PersonalAccessToken;
hasPermission: boolean;
onEdit: () => void;
onTokenRevoke: () => void;
onEdit?: () => void;
onTokenRevoke?: () => void;
isBot: boolean;
}
const AuthMechanism: FC<Props> = ({
@ -32,19 +34,32 @@ const AuthMechanism: FC<Props> = ({
hasPermission,
onEdit,
onTokenRevoke,
isBot,
}: Props) => {
const JWTToken = authenticationMechanism.config?.JWTToken;
const JWTTokenExpiresAt =
authenticationMechanism.config?.JWTTokenExpiresAt ?? 0;
const { JWTToken, JWTTokenExpiresAt } = useMemo(() => {
if (isBot) {
const botData = authenticationMechanism as AuthenticationMechanism;
return {
JWTToken: botData?.config?.JWTToken,
JWTTokenExpiresAt: botData?.config?.JWTTokenExpiresAt ?? 0,
};
}
const personalAccessData = authenticationMechanism as PersonalAccessToken;
return {
JWTToken: personalAccessData?.jwtToken,
JWTTokenExpiresAt: personalAccessData?.expiryDate ?? 0,
};
}, [isBot, authenticationMechanism]);
// get the token expiry date
const { tokenExpiryDate, isTokenExpired } = getTokenExpiry(JWTTokenExpiresAt);
return (
<>
<Space className="w-full justify-between">
<Typography.Text className="text-base">
{t('label.om-jwt-token')}
{isBot ? t('label.om-jwt-token') : t('message.personal-access-token')}
</Typography.Text>
<Space>
{JWTToken ? (
@ -59,6 +74,7 @@ const AuthMechanism: FC<Props> = ({
</Button>
) : (
<Button
data-testid="auth-mechanism"
disabled={!hasPermission}
size="small"
type="primary"

View File

@ -13,23 +13,28 @@
import { Button, Form, FormProps, Select, Space } from 'antd';
import { isEmpty } from 'lodash';
import React, { FC } from 'react';
import React, { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
PersonalAccessToken,
TokenType,
} from '../../generated/auth/personalAccessToken';
import {
AuthenticationMechanism,
AuthType,
JWTTokenExpiry,
} from '../../generated/entity/teams/user';
import { getJWTOption, getJWTTokenExpiryOptions } from '../../utils/BotsUtils';
import { getJWTTokenExpiryOptions } from '../../utils/BotsUtils';
import Loader from '../Loader/Loader';
const { Option } = Select;
interface Props {
isUpdating: boolean;
authenticationMechanism: AuthenticationMechanism;
authenticationMechanism: AuthenticationMechanism | PersonalAccessToken;
onSave: (updatedAuthMechanism: AuthenticationMechanism) => void;
onCancel: () => void;
onCancel?: () => void;
isBot: boolean;
}
const AuthMechanismForm: FC<Props> = ({
@ -37,11 +42,9 @@ const AuthMechanismForm: FC<Props> = ({
onSave,
onCancel,
authenticationMechanism,
isBot,
}) => {
const { t } = useTranslation();
const jwtOption = getJWTOption();
const handleSave: FormProps['onFinish'] = (values) => {
const updatedAuthMechanism: AuthenticationMechanism = {
authType: values?.authType ?? AuthType.Jwt,
@ -49,20 +52,44 @@ const AuthMechanismForm: FC<Props> = ({
JWTTokenExpiry: values.tokenExpiry as JWTTokenExpiry,
},
};
onSave(updatedAuthMechanism);
};
const authOptions = useMemo(() => {
const botValue = {
label: 'OpenMetadata JWT',
value: 'JWT',
};
const accessTokenValue = {
label: 'Personal Access Token',
value: TokenType.PersonalAccessToken,
};
return isBot ? botValue : accessTokenValue;
}, [isBot]);
const { authType, tokenExpiry } = useMemo(() => {
if (isBot) {
const botData = authenticationMechanism as AuthenticationMechanism;
return {
authType: botData?.authType,
tokenExpiry: JWTTokenExpiry.OneHour,
};
}
const personalAccessData = authenticationMechanism as PersonalAccessToken;
return {
authType: personalAccessData?.tokenType ?? TokenType.PersonalAccessToken,
tokenExpiry: JWTTokenExpiry.OneHour,
};
}, [isBot, authenticationMechanism]);
return (
<>
<Form
id="update-auth-mechanism-form"
initialValues={{
authType: authenticationMechanism.authType ?? AuthType.Jwt,
tokenExpiry:
authenticationMechanism.config?.JWTTokenExpiry ??
JWTTokenExpiry.OneHour,
}}
initialValues={{ authType, tokenExpiry }}
layout="vertical"
onFinish={handleSave}>
<Form.Item label={t('label.auth-mechanism')} name="authType">
@ -73,7 +100,7 @@ const AuthMechanismForm: FC<Props> = ({
placeholder={t('label.select-field', {
field: t('label.auth-mechanism'),
})}>
<Option key={jwtOption.value}>{jwtOption.label}</Option>
<Option key={authOptions.value}>{authOptions.label}</Option>
</Select>
</Form.Item>
@ -89,9 +116,17 @@ const AuthMechanismForm: FC<Props> = ({
className="w-full"
data-testid="token-expiry"
placeholder={t('message.select-token-expiration')}>
{getJWTTokenExpiryOptions().map((option) => (
<Option key={option.value}>{option.label}</Option>
))}
{isBot
? getJWTTokenExpiryOptions().map((option) => (
<Option key={option.value}>{option.label}</Option>
))
: getJWTTokenExpiryOptions()
.filter((option) => option.value !== 'Unlimited')
.map((filteredOption) => (
<Option key={filteredOption.value}>
{filteredOption.label}
</Option>
))}
</Select>
</Form.Item>
@ -106,7 +141,7 @@ const AuthMechanismForm: FC<Props> = ({
form="update-auth-mechanism-form"
htmlType="submit"
type="primary">
{isUpdating ? <Loader size="small" /> : t('label.save')}
{isUpdating ? <Loader size="small" /> : t('label.generate')}
</Button>
</Space>
</Form>

View File

@ -25,16 +25,7 @@ import {
GlobalSettingsMenuCategory,
} from '../../constants/GlobalSettings.constants';
import { Role } from '../../generated/entity/teams/role';
import {
AuthenticationMechanism,
AuthType,
} from '../../generated/entity/teams/user';
import { createBotWithPut } from '../../rest/botsAPI';
import {
createUserWithPut,
getAuthMechanismForBotUser,
getRoles,
} from '../../rest/userAPI';
import { getRoles } from '../../rest/userAPI';
import { getEntityName } from '../../utils/EntityUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import { showErrorToast } from '../../utils/ToastUtils';
@ -42,13 +33,11 @@ import Description from '../common/EntityDescription/Description';
import InheritedRolesCard from '../common/InheritedRolesCard/InheritedRolesCard.component';
import RolesCard from '../common/RolesCard/RolesCard.component';
import TitleBreadcrumb from '../common/TitleBreadcrumb/TitleBreadcrumb.component';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import AuthMechanism from './AuthMechanism';
import AuthMechanismForm from './AuthMechanismForm';
import './bot-details.less';
import { BotsDetailProps } from './BotDetails.interfaces';
import { ReactComponent as IconBotProfile } from '../../assets/svg/bot-profile.svg';
import AccessTokenCard from '../AccessTokenCard/AccessTokenCard.component';
const BotDetails: FC<BotsDetailProps> = ({
botData,
@ -61,18 +50,10 @@ const BotDetails: FC<BotsDetailProps> = ({
const [displayName, setDisplayName] = useState(botData.displayName);
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
const [isRevokingToken, setIsRevokingToken] = useState<boolean>(false);
const [selectedRoles, setSelectedRoles] = useState<Array<string>>([]);
const [roles, setRoles] = useState<Array<Role>>([]);
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const { t } = useTranslation();
const [authenticationMechanism, setAuthenticationMechanism] =
useState<AuthenticationMechanism>();
const [isAuthMechanismEdit, setIsAuthMechanismEdit] =
useState<boolean>(false);
const editAllPermission = useMemo(
() => botPermission.EditAll,
@ -88,15 +69,6 @@ const BotDetails: FC<BotsDetailProps> = ({
[botPermission]
);
const fetchAuthMechanismForBot = async () => {
try {
const response = await getAuthMechanismForBotUser(botUserData.id);
setAuthenticationMechanism(response);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const fetchRoles = async () => {
try {
const { data } = await getRoles();
@ -107,64 +79,6 @@ const BotDetails: FC<BotsDetailProps> = ({
}
};
const handleAuthMechanismUpdate = async (
updatedAuthMechanism: AuthenticationMechanism
) => {
setIsUpdating(true);
try {
const {
isAdmin,
timezone,
name,
description,
displayName,
profile,
email,
isBot,
} = botUserData;
const response = await createUserWithPut({
isAdmin,
timezone,
name,
description,
displayName,
profile,
email,
isBot,
authenticationMechanism: {
...botUserData.authenticationMechanism,
authType: updatedAuthMechanism.authType,
config:
updatedAuthMechanism.authType === AuthType.Jwt
? {
JWTTokenExpiry: updatedAuthMechanism.config?.JWTTokenExpiry,
}
: {
ssoServiceType: updatedAuthMechanism.config?.ssoServiceType,
authConfig: updatedAuthMechanism.config?.authConfig,
},
},
botName: botData.name,
});
if (response) {
await createBotWithPut({
name: botData.name,
description: botData.description,
displayName: botData.displayName,
botUser: response.name,
});
fetchAuthMechanismForBot();
}
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsUpdating(false);
setIsAuthMechanismEdit(false);
}
};
const handleAuthMechanismEdit = () => setIsAuthMechanismEdit(true);
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.target.value);
};
@ -295,9 +209,6 @@ const BotDetails: FC<BotsDetailProps> = ({
useEffect(() => {
prepareSelectedRoles();
if (botUserData.id) {
fetchAuthMechanismForBot();
}
}, [botUserData]);
return (
@ -331,50 +242,13 @@ const BotDetails: FC<BotsDetailProps> = ({
{ name: botData.name || '', url: '', activeTitle: true },
]}
/>
<Card
className="page-layout-v1-left-panel mt-2 m-t-md"
data-testid="center-panel">
{authenticationMechanism ? (
<>
{isAuthMechanismEdit ? (
<AuthMechanismForm
authenticationMechanism={authenticationMechanism}
isUpdating={isUpdating}
onCancel={() => setIsAuthMechanismEdit(false)}
onSave={handleAuthMechanismUpdate}
/>
) : (
<AuthMechanism
authenticationMechanism={authenticationMechanism}
hasPermission={editAllPermission}
onEdit={handleAuthMechanismEdit}
onTokenRevoke={() => setIsRevokingToken(true)}
/>
)}
</>
) : (
<AuthMechanismForm
authenticationMechanism={{} as AuthenticationMechanism}
isUpdating={isUpdating}
onCancel={() => setIsAuthMechanismEdit(false)}
onSave={handleAuthMechanismUpdate}
/>
)}
</Card>
<AccessTokenCard
isBot
botData={botData}
botUserData={botUserData}
revokeTokenHandlerBot={revokeTokenHandler}
/>
</div>
<ConfirmationModal
bodyText={t('message.are-you-sure-to-revoke-access')}
cancelText={t('label.cancel')}
confirmText={t('label.confirm')}
header={t('message.are-you-sure')}
visible={isRevokingToken}
onCancel={() => setIsRevokingToken(false)}
onConfirm={() => {
revokeTokenHandler();
setIsRevokingToken(false);
handleAuthMechanismEdit();
}}
/>
</PageLayoutV1>
);
};

View File

@ -14,6 +14,7 @@
import {
act,
findByTestId,
findByText,
fireEvent,
render,
screen,
@ -193,7 +194,7 @@ describe('Test BotsDetail Component', () => {
expect(revokeTokenHandler).toHaveBeenCalled();
});
it('Should render the edit form if the authmechanism is empty', async () => {
it('Should render the generate form if the authmechanism is empty', async () => {
(getAuthMechanismForBotUser as jest.Mock).mockImplementationOnce(() => {
return Promise.resolve(undefined);
});
@ -202,10 +203,7 @@ describe('Test BotsDetail Component', () => {
wrapper: MemoryRouter,
});
const authMechanismForm = await findByTestId(
container,
'AuthMechanismForm'
);
const authMechanismForm = await findByText(container, 'label.om-jwt-token');
expect(authMechanismForm).toBeInTheDocument();
});

View File

@ -21,6 +21,7 @@ export enum AssetsOfEntity {
TEAM = 'TEAM',
MY_DATA = 'MY_DATA',
FOLLOWING = 'FOLLOWING',
ACCESS_TOKEN = 'ACCESS_TOKEN',
}
export interface AssetsTabsProps {

View File

@ -89,8 +89,4 @@ const ConfirmationModal = ({
);
};
/**
*
* @deprecated Please use {@link https://github.com/open-metadata/OpenMetadata/blob/main/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx DeleteWidgetModal}
*/
export default ConfirmationModal;

View File

@ -17,11 +17,19 @@ import {
findByText,
queryByTestId,
render,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { mockTeamsData, mockUserData, mockUserRole } from './mocks/User.mocks';
import { AuthProvider } from '../../generated/settings/settings';
import { useAuthContext } from '../Auth/AuthProviders/AuthProvider';
import {
mockAccessData,
mockTeamsData,
mockUserData,
mockUserRole,
} from './mocks/User.mocks';
import Users from './Users.component';
import { UserPageTabs } from './Users.interface';
@ -108,6 +116,16 @@ jest.mock(
.mockImplementation(() => <>ActivityFeedTabTest</>),
})
);
jest.mock('../Auth/AuthProviders/AuthProvider', () => ({
useAuthContext: jest.fn(() => ({
authConfig: {
provider: AuthProvider.Basic,
},
currentUser: {
name: 'test',
},
})),
}));
jest.mock('../../rest/teamsAPI', () => ({
getTeams: jest.fn().mockImplementation(() => Promise.resolve(mockTeamsData)),
@ -257,4 +275,26 @@ describe('Test User Component', () => {
expect(assetComponent).toBeInTheDocument();
});
it('Access Token tab should show user access component', async () => {
(useAuthContext as jest.Mock).mockImplementationOnce(() => ({
currentUser: {
name: 'test',
},
}));
mockParams.tab = UserPageTabs.ACCESS_TOKEN;
render(
<Users
authenticationMechanism={mockAccessData}
userData={mockUserData}
{...mockProp}
/>,
{
wrapper: MemoryRouter,
}
);
const assetComponent = await screen.findByTestId('center-panel');
expect(assetComponent).toBeInTheDocument();
});
});

View File

@ -30,6 +30,7 @@ import { searchData } from '../../rest/miscAPI';
import { getEntityName } from '../../utils/EntityUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getDecodedFqn } from '../../utils/StringsUtils';
import AccessTokenCard from '../AccessTokenCard/AccessTokenCard.component';
import { useAuthContext } from '../Auth/AuthProviders/AuthProvider';
import Chip from '../common/Chip/Chip.component';
import DescriptionV1 from '../common/EntityDescription/DescriptionV1';
@ -76,7 +77,6 @@ const Users = ({ userData, queryFilters, updateUserDetails }: Props) => {
() => isAdminUser || isLoggedInUser,
[isAdminUser, isLoggedInUser]
);
const fetchAssetsCount = async (query: string) => {
try {
const res = await searchData('', 1, 0, query, '', '', SearchIndex.ALL);
@ -200,6 +200,21 @@ const Users = ({ userData, queryFilters, updateUserDetails }: Props) => {
},
}),
},
...(isLoggedInUser
? [
{
label: (
<TabsLabel
id={UserPageTabs.ACCESS_TOKEN}
isActive={activeTab === UserPageTabs.ACCESS_TOKEN}
name={t('label.access-token')}
/>
),
key: UserPageTabs.ACCESS_TOKEN,
children: <AccessTokenCard isBot={false} />,
},
]
: []),
],
[activeTab, userData, decodedUsername, setPreviewAsset, tabDataRender]
);

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { PersonalAccessToken } from '../../generated/auth/personalAccessToken';
import { User } from '../../generated/entity/teams/user';
export interface Props {
@ -21,10 +22,12 @@ export interface Props {
};
handlePaginate: (page: string | number) => void;
updateUserDetails: (data: Partial<User>) => Promise<void>;
authenticationMechanism?: PersonalAccessToken;
}
export enum UserPageTabs {
ACTIVITY = 'activity_feed',
MY_DATA = 'mydata',
FOLLOWING = 'following',
ACCESS_TOKEN = 'access-token',
}

View File

@ -12,6 +12,7 @@
*/
import { SearchIndex } from '../../../enums/search.enum';
import { TokenType } from '../../../generated/auth/personalAccessToken';
import { DashboardServiceType } from '../../../generated/entity/data/dashboard';
import {
DashboardSearchSource,
@ -1030,3 +1031,11 @@ export const mockEntityData: {
total: 6,
currPage: 1,
};
export const mockAccessData = {
expiryDate: 1701714886101,
jwtToken: 'eyJraWQiOiJHYjM4OWEtOWY3Ni1nZGpzLWE5Mmot',
token: '4946956adf3a8d646',
tokenName: 'test',
tokenType: TokenType.PersonalAccessToken,
userId: '445291f4d62c1bae',
};

View File

@ -5,6 +5,7 @@
"accept-suggestion": "Vorschlag akzeptieren",
"access": "Zugriff",
"access-block-time": "Access block time",
"access-token": "Access Token",
"accessed": "Accessed",
"account": "Konto",
"account-email": "Konto-E-Mail",
@ -1216,6 +1217,7 @@
"are-you-sure-delete-property": "Sind Sie sicher, dass Sie die Eigenschaft {{propertyName}} dauerhaft löschen möchten?",
"are-you-sure-delete-tag": "Sind Sie sicher, dass Sie den {{type}} \"{{tagName}}\" dauerhaft löschen möchten?",
"are-you-sure-to-revoke-access": "Sind Sie sicher, dass Sie den Zugriff für das JWT-Token widerrufen möchten?",
"are-you-sure-to-revoke-access-personal-access": "Are you sure you want to revoke access for Personal Access Token?",
"are-you-sure-want-to-enable": "Are you sure you want to enable {{entity}}?",
"are-you-sure-want-to-text": "Sind Sie sicher, dass Sie {{text}} möchten?",
"are-you-sure-you-want-to-remove-child-from-parent": "Sind Sie sicher, dass Sie das {{child}} aus {{parent}} entfernen möchten?",
@ -1519,6 +1521,7 @@
"path-of-the-dbt-files-stored": "Pfad zum Ordner, in dem die dbt-Dateien gespeichert sind",
"permanently-delete-metadata": "Das dauerhafte Löschen dieses {{entityName}} entfernt seine Metadaten dauerhaft aus OpenMetadata.",
"permanently-delete-metadata-and-dependents": "Das dauerhafte Löschen dieses {{entityName}} entfernt seine Metadaten sowie die Metadaten von {{dependents}} dauerhaft aus OpenMetadata.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Beschreibung der Pipeline.",
"pipeline-scheduler-message": "Der Ingestion Scheduler kann nicht antworten. Bitte wende dich an den Collate-Support. Vielen Dank.",
"pipeline-trigger-failed-message": "Auslösen der Pipeline fehlgeschlagen!",

View File

@ -5,6 +5,7 @@
"accept-suggestion": "Accept Suggestion",
"access": "Access",
"access-block-time": "Access block time",
"access-token": "Access Token",
"accessed": "Accessed",
"account": "Account",
"account-email": "Account email",
@ -1215,7 +1216,8 @@
"are-you-sure-delete-entity": "Are you sure you want to delete the property {{entity}}",
"are-you-sure-delete-property": "Are you sure you want to delete the property {{propertyName}}?",
"are-you-sure-delete-tag": "Are you sure you want to delete the {{type}} \"{{tagName}}\"?",
"are-you-sure-to-revoke-access": "Are you sure you want to revoke access for JWT token?",
"are-you-sure-to-revoke-access": "Are you sure you want to revoke access for JWT Token?",
"are-you-sure-to-revoke-access-personal-access": "Are you sure you want to revoke access for Personal Access Token?",
"are-you-sure-want-to-enable": "Are you sure you want to enable {{entity}}?",
"are-you-sure-want-to-text": "Are you sure you want to {{text}}?",
"are-you-sure-you-want-to-remove-child-from-parent": "Are you sure you want to remove the {{child}} from {{parent}}?",
@ -1519,6 +1521,7 @@
"path-of-the-dbt-files-stored": "Path of the folder where the dbt files are stored",
"permanently-delete-metadata": "Permanently deleting this {{entityName}} will remove its metadata from OpenMetadata permanently.",
"permanently-delete-metadata-and-dependents": "Permanently deleting this {{entityName}} will remove its metadata, as well as the metadata of {{dependents}} from OpenMetadata permanently.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Description of the pipeline.",
"pipeline-scheduler-message": "The Ingestion Scheduler is unable to respond. Please reach out to Collate support. Thank you.",
"pipeline-trigger-failed-message": "Failed to trigger the pipeline!",

View File

@ -5,6 +5,7 @@
"accept-suggestion": "Aceptar sugerencia",
"access": "Acceso",
"access-block-time": "Access block time",
"access-token": "Access Token",
"accessed": "Accessed",
"account": "Cuenta",
"account-email": "Correo electrónico de la cuenta",
@ -1216,6 +1217,7 @@
"are-you-sure-delete-property": "¿Estás seguro de que quieres eliminar la propiedad {{propertyName}}?",
"are-you-sure-delete-tag": "¿Estás seguro de que quieres eliminar la {{type}} \"{{tagName}}\"?",
"are-you-sure-to-revoke-access": "¿Estás seguro de que quieres revocar el acceso para el token JWT?",
"are-you-sure-to-revoke-access-personal-access": "Are you sure you want to revoke access for Personal Access Token?",
"are-you-sure-want-to-enable": "Are you sure you want to enable {{entity}}?",
"are-you-sure-want-to-text": "¿Estás seguro de que quieres {{text}}?",
"are-you-sure-you-want-to-remove-child-from-parent": "¿Estás seguro de que quieres eliminar {{child}} de {{parent}}?",
@ -1519,6 +1521,7 @@
"path-of-the-dbt-files-stored": "Ruta de la carpeta donde se almacenan los archivos dbt",
"permanently-delete-metadata": "Al eliminar permanentemente este {{entityName}}, se eliminaran sus metadatos de OpenMetadata permanentemente.",
"permanently-delete-metadata-and-dependents": "Al eliminar permanentemente este {{entityName}}, se eliminaran sus metadatos, así como los metadatos de {{dependents}} de OpenMetadata permanentemente.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Descripción del pipeline.",
"pipeline-scheduler-message": "The Ingestion Scheduler is unable to respond. Please reach out to Collate support. Thank you.",
"pipeline-trigger-failed-message": "Failed to trigger the pipeline!",

View File

@ -5,6 +5,7 @@
"accept-suggestion": "Accepter la Suggestion",
"access": "Accès",
"access-block-time": "Access block time",
"access-token": "Access Token",
"accessed": "Accessed",
"account": "Compte",
"account-email": "Compte email",
@ -1216,6 +1217,7 @@
"are-you-sure-delete-property": "Êtes-vous sûr de vouloir supprimer la propriété {{propertyName}}",
"are-you-sure-delete-tag": "Êtes-vous sûr de vouloir supprimer le tag {{isCategory}} \"{{tagName}}\"?",
"are-you-sure-to-revoke-access": "Êtes-vous sûr de vouloir supprimer l'accès au Jeton JWT ?",
"are-you-sure-to-revoke-access-personal-access": "Are you sure you want to revoke access for Personal Access Token?",
"are-you-sure-want-to-enable": "Are you sure you want to enable {{entity}}?",
"are-you-sure-want-to-text": "Êtes-vous sûr de vouloir {{text}}",
"are-you-sure-you-want-to-remove-child-from-parent": "Êtes-vous sûr de vouloir supprimer le {{child}} de {{parent}} ?",
@ -1519,6 +1521,7 @@
"path-of-the-dbt-files-stored": "Chemin du dossier où sont situés les fichiers dbt.",
"permanently-delete-metadata": "La suppression permanente de cette {{entityName}} supprimera ses métadonnées de façon permanente d'OpenMetadata.",
"permanently-delete-metadata-and-dependents": "La suppression permanente de cette {{entityName}} supprimera ses métadonnées ainsi que les métadonnées de {{dependents}} de façon permanente d'OpenMetadata.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Description du pipeline.",
"pipeline-scheduler-message": "Le planificateur d'ingestion ne peut pas répondre. Veuillez contacter le support Collate. Merci.",
"pipeline-trigger-failed-message": "Échec de la déclenchement du pipeline !",

View File

@ -5,6 +5,7 @@
"accept-suggestion": "提案を受け入れる",
"access": "アクセス",
"access-block-time": "Access block time",
"access-token": "Access Token",
"accessed": "Accessed",
"account": "アカウント",
"account-email": "アカウントのEmail",
@ -1216,6 +1217,7 @@
"are-you-sure-delete-property": "本当にプロパティ {{propertyName}} を削除してよろしいですか?",
"are-you-sure-delete-tag": "本当に {{type}} \"{{tagName}}\"を削除してよろしいですか?",
"are-you-sure-to-revoke-access": "Are you sure you want to revoke access for JWT token?",
"are-you-sure-to-revoke-access-personal-access": "Are you sure you want to revoke access for Personal Access Token?",
"are-you-sure-want-to-enable": "Are you sure you want to enable {{entity}}?",
"are-you-sure-want-to-text": "本当に{{text}}してよろしいですか?",
"are-you-sure-you-want-to-remove-child-from-parent": "本当に{{parent}}から{{child}}を削除しますか?",
@ -1519,6 +1521,7 @@
"path-of-the-dbt-files-stored": "Path of the folder where the dbt files are stored",
"permanently-delete-metadata": "Permanently deleting this {{entityName}} will remove its metadata from OpenMetadata permanently.",
"permanently-delete-metadata-and-dependents": "Permanently deleting this {{entityName}} will remove its metadata, as well as the metadata of {{dependents}} from OpenMetadata permanently.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "パイプラインの説明",
"pipeline-scheduler-message": "The Ingestion Scheduler is unable to respond. Please reach out to Collate support. Thank you.",
"pipeline-trigger-failed-message": "Failed to trigger the pipeline!",

View File

@ -5,6 +5,7 @@
"accept-suggestion": "Aceitar Sugestão",
"access": "Acesso",
"access-block-time": "Tempo de bloqueio de acesso",
"access-token": "Access Token",
"accessed": "Acessado",
"account": "Conta",
"account-email": "E-mail da conta",
@ -1216,6 +1217,7 @@
"are-you-sure-delete-property": "Você tem certeza que deseja deletar a propriedade {{propertyName}}?",
"are-you-sure-delete-tag": "Você tem certeza que deseja deletar a {{type}} \"{{tagName}}\"?",
"are-you-sure-to-revoke-access": "Você tem certeza que deseja revogar o acesso para o token JWT?",
"are-you-sure-to-revoke-access-personal-access": "Are you sure you want to revoke access for Personal Access Token?",
"are-you-sure-want-to-enable": "Você tem certeza que deseja habilitar {{entity}}?",
"are-you-sure-want-to-text": "Você tem certeza que deseja {{text}}?",
"are-you-sure-you-want-to-remove-child-from-parent": "Você tem certeza que deseja remover o {{child}} de {{parent}}?",
@ -1519,6 +1521,7 @@
"path-of-the-dbt-files-stored": "Caminho da pasta onde os arquivos dbt são armazenados",
"permanently-delete-metadata": "Excluir permanentemente este(a) {{entityName}} removerá seus metadados do OpenMetadata permanentemente.",
"permanently-delete-metadata-and-dependents": "Excluir permanentemente este(a) {{entityName}} removerá seus metadados, bem como os metadados de {{dependents}} do OpenMetadata permanentemente.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Descrição do pipeline.",
"pipeline-scheduler-message": "O Agendador de Ingestão não está respondendo. Entre em contato com o suporte da Collate. Obrigado.",
"pipeline-trigger-failed-message": "Falha ao acionar o pipeline!",

View File

@ -5,6 +5,7 @@
"accept-suggestion": "Согласовать предложение",
"access": "Доступ",
"access-block-time": "Access block time",
"access-token": "Access Token",
"accessed": "Accessed",
"account": "Аккаунт",
"account-email": "Адрес электронной почты",
@ -1216,6 +1217,7 @@
"are-you-sure-delete-property": "Вы уверены, что хотите удалить свойство {{propertyName}}?",
"are-you-sure-delete-tag": "Вы уверены, что хотите удалить {{type}} \"{{tagName}}\"?",
"are-you-sure-to-revoke-access": "Вы уверены, что хотите отозвать доступ к токену JWT?",
"are-you-sure-to-revoke-access-personal-access": "Are you sure you want to revoke access for Personal Access Token?",
"are-you-sure-want-to-enable": "Are you sure you want to enable {{entity}}?",
"are-you-sure-want-to-text": "Вы уверены, что хотите {{text}}?",
"are-you-sure-you-want-to-remove-child-from-parent": "Вы уверены, что хотите удалить {{child}} для {{parent}}?",
@ -1519,6 +1521,7 @@
"path-of-the-dbt-files-stored": "Путь к папке, в которой хранятся файлы dbt",
"permanently-delete-metadata": "При окончательном удалении этого объекта {{entityName}} его метаданные будут навсегда удалены из OpenMetadata.",
"permanently-delete-metadata-and-dependents": "Безвозвратное удаление этого {{entityName}} удалит его метаданные, а также метаданные {{dependers}} из OpenMetadata навсегда.",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "Описание пайплайна.",
"pipeline-scheduler-message": "Планировщик загрузки не может ответить. Обратитесь в службу поддержки. Спасибо.",
"pipeline-trigger-failed-message": "Не удалось запустить пайплайн!",

View File

@ -5,6 +5,7 @@
"accept-suggestion": "接受建议",
"access": "访问",
"access-block-time": "Access block time",
"access-token": "Access Token",
"accessed": "Accessed",
"account": "帐号",
"account-email": "帐号邮箱",
@ -1216,6 +1217,7 @@
"are-you-sure-delete-property": "您确定要删除属性{{propertyName}}吗?",
"are-you-sure-delete-tag": "您确定要删除{{type}}\"{{tagName}}\"吗?",
"are-you-sure-to-revoke-access": "您确定要吊销 JWT 令牌的访问权限吗?",
"are-you-sure-to-revoke-access-personal-access": "Are you sure you want to revoke access for Personal Access Token?",
"are-you-sure-want-to-enable": "Are you sure you want to enable {{entity}}?",
"are-you-sure-want-to-text": "您确定要{{text}}吗?",
"are-you-sure-you-want-to-remove-child-from-parent": "您确定要将{{child}}从{{parent}}中删除吗?",
@ -1519,6 +1521,7 @@
"path-of-the-dbt-files-stored": "存储 dbt 文件的文件夹路径",
"permanently-delete-metadata": "永久删除此{{entityName}}将永久从 OpenMetadata 中删除其元数据",
"permanently-delete-metadata-and-dependents": "永久删除此{{entityName}}将永久从 OpenMetadata 中删除其元数据以及{{dependents}}的元数据",
"personal-access-token": "Personal Access Token",
"pipeline-description-message": "工作流的描述信息",
"pipeline-scheduler-message": "元数据提取编排器无回复,请联系系统管理员",
"pipeline-trigger-failed-message": "触发工作流失败!",

View File

@ -21,6 +21,7 @@ import {
CreateUser,
} from '../generated/api/teams/createUser';
import { JwtAuth } from '../generated/auth/jwtAuth';
import { PersonalAccessToken } from '../generated/auth/personalAccessToken';
import { Bot } from '../generated/entity/bot';
import { Role } from '../generated/entity/teams/role';
import { User } from '../generated/entity/teams/user';
@ -233,3 +234,40 @@ export const createUserWithPut = async (userDetails: CreateUser) => {
return response.data;
};
export const getUserAccessToken = async () => {
const response = await APIClient.get<{
data: PersonalAccessToken[];
}>('/users/security/token');
return response.data.data;
};
export const updateUserAccessToken = async ({
JWTTokenExpiry,
tokenName,
}: {
JWTTokenExpiry?: string;
tokenName?: string;
}) => {
const response = await APIClient.put<
{
JWTTokenExpiry?: string;
tokenName?: string;
},
AxiosResponse<PersonalAccessToken[]>
>(`/users/security/token`, {
JWTTokenExpiry,
tokenName,
});
return response.data;
};
export const revokeAccessToken = async (params: string) => {
const response = await APIClient.put<PersonalAccessToken>(
'/users/security/token/revoke?' + params
);
return response.data;
};