mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-01 02:56:10 +00:00
* 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:
parent
a40b651cc0
commit
c2b0c2687d
@ -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.");
|
||||
|
||||
@ -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"]')
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -21,6 +21,7 @@ export enum AssetsOfEntity {
|
||||
TEAM = 'TEAM',
|
||||
MY_DATA = 'MY_DATA',
|
||||
FOLLOWING = 'FOLLOWING',
|
||||
ACCESS_TOKEN = 'ACCESS_TOKEN',
|
||||
}
|
||||
|
||||
export interface AssetsTabsProps {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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]
|
||||
);
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -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!",
|
||||
|
||||
@ -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!",
|
||||
|
||||
@ -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!",
|
||||
|
||||
@ -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 !",
|
||||
|
||||
@ -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!",
|
||||
|
||||
@ -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!",
|
||||
|
||||
@ -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": "Не удалось запустить пайплайн!",
|
||||
|
||||
@ -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": "触发工作流失败!",
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user