diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java index 4e6ae01a1b6..c83dea258c8 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java @@ -1129,6 +1129,7 @@ public class UserResource extends EntityResource { 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."); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.js index 0c7b7cef5aa..2ad0f5dd5f2 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.js @@ -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"]') diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Users.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Users.spec.js index 35775888365..d5cbb08c2dd 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Users.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Users.spec.js @@ -12,6 +12,10 @@ */ // eslint-disable-next-line spaced-comment /// +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(); + }); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.component.tsx new file mode 100644 index 00000000000..3a1795cc320 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.component.tsx @@ -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 = ({ + isBot, + botData, + botUserData, + revokeTokenHandlerBot, +}: MockProps) => { + const [isUpdating, setIsUpdating] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isAuthMechanismEdit, setIsAuthMechanismEdit] = + useState(false); + const [authenticationMechanism, setAuthenticationMechanism] = + useState({ + tokenType: TokenType.PersonalAccessToken, + } as PersonalAccessToken); + const [authenticationMechanismBot, setAuthenticationMechanismBot] = + useState({ + 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 ? ( + + ) : ( + + <> + {isAuthMechanismEdit ? ( + setIsAuthMechanismEdit(false)} + onSave={onSave} + /> + ) : ( + setIsModalOpen(true)} + /> + )} + + setIsModalOpen(false)} + onConfirm={() => { + tokenRevoke(); + handleAuthMechanismEdit(); + setIsModalOpen(false); + }} + /> + + ); +}; + +export default AccessTokenCard; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.interfaces.ts b/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.interfaces.ts new file mode 100644 index 00000000000..c2a9975f8d0 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.interfaces.ts @@ -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; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.test.tsx new file mode 100644 index 00000000000..d3c009e2d44 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AccessTokenCard/AccessTokenCard.test.tsx @@ -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 }) => ( +

+ AuthMechanism{' '} + +

+ )); +}); + +jest.mock('../BotDetails/AuthMechanismForm', () => { + return jest.fn().mockReturnValue(

AuthMechanismForm

); +}); + +jest.mock('../Modals/ConfirmationModal/ConfirmationModal', () => { + return jest.fn().mockImplementation(({ onConfirm, onCancel, visible }) => + visible ? ( +
+ + +
+ ) : ( +
+ ) + ); +}); + +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(); + + expect(screen.getByText('AuthMechanism')).toBeInTheDocument(); + }); + + it('should render AuthMechanism when isAuthMechanismEdit is false', async () => { + await act(async () => { + render(); + }); + + expect(await screen.findByText('AuthMechanism')).toBeInTheDocument(); + }); + + it('should call onConfirm when Confirm button is clicked', async () => { + const mockRevokeAccessToken = revokeAccessToken as jest.Mock; + render(); + 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(); + 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(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanism.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanism.tsx index f82dd8eb7fe..f2b67041202 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanism.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanism.tsx @@ -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 = ({ @@ -32,19 +34,32 @@ const AuthMechanism: FC = ({ 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 ( <> - {t('label.om-jwt-token')} + {isBot ? t('label.om-jwt-token') : t('message.personal-access-token')} {JWTToken ? ( @@ -59,6 +74,7 @@ const AuthMechanism: FC = ({ ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.component.tsx index ad199d450e5..c02d74cf193 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.component.tsx @@ -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 = ({ botData, @@ -61,18 +50,10 @@ const BotDetails: FC = ({ const [displayName, setDisplayName] = useState(botData.displayName); const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false); const [isDescriptionEdit, setIsDescriptionEdit] = useState(false); - const [isRevokingToken, setIsRevokingToken] = useState(false); const [selectedRoles, setSelectedRoles] = useState>([]); const [roles, setRoles] = useState>([]); - const [isUpdating, setIsUpdating] = useState(false); - const { t } = useTranslation(); - const [authenticationMechanism, setAuthenticationMechanism] = - useState(); - - const [isAuthMechanismEdit, setIsAuthMechanismEdit] = - useState(false); const editAllPermission = useMemo( () => botPermission.EditAll, @@ -88,15 +69,6 @@ const BotDetails: FC = ({ [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 = ({ } }; - 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) => { setDisplayName(e.target.value); }; @@ -295,9 +209,6 @@ const BotDetails: FC = ({ useEffect(() => { prepareSelectedRoles(); - if (botUserData.id) { - fetchAuthMechanismForBot(); - } }, [botUserData]); return ( @@ -331,50 +242,13 @@ const BotDetails: FC = ({ { name: botData.name || '', url: '', activeTitle: true }, ]} /> - - {authenticationMechanism ? ( - <> - {isAuthMechanismEdit ? ( - setIsAuthMechanismEdit(false)} - onSave={handleAuthMechanismUpdate} - /> - ) : ( - setIsRevokingToken(true)} - /> - )} - - ) : ( - setIsAuthMechanismEdit(false)} - onSave={handleAuthMechanismUpdate} - /> - )} - +
- setIsRevokingToken(false)} - onConfirm={() => { - revokeTokenHandler(); - setIsRevokingToken(false); - handleAuthMechanismEdit(); - }} - /> ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.test.tsx index d0eb2b1b2f3..8291cd57c2d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.test.tsx @@ -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(); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface.ts index 774a2027557..e4225350397 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface.ts @@ -21,6 +21,7 @@ export enum AssetsOfEntity { TEAM = 'TEAM', MY_DATA = 'MY_DATA', FOLLOWING = 'FOLLOWING', + ACCESS_TOKEN = 'ACCESS_TOKEN', } export interface AssetsTabsProps { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ConfirmationModal/ConfirmationModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ConfirmationModal/ConfirmationModal.tsx index 3b501c83a29..1274434af57 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ConfirmationModal/ConfirmationModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ConfirmationModal/ConfirmationModal.tsx @@ -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; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.test.tsx index 61412fdd5b9..52beca237e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.test.tsx @@ -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( + , + { + wrapper: MemoryRouter, + } + ); + const assetComponent = await screen.findByTestId('center-panel'); + + expect(assetComponent).toBeInTheDocument(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx index 7c79a747650..d8add2df572 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx @@ -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: ( + + ), + key: UserPageTabs.ACCESS_TOKEN, + children: , + }, + ] + : []), ], [activeTab, userData, decodedUsername, setPreviewAsset, tabDataRender] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.interface.ts index 0f47e011d1b..8ec6a13a873 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.interface.ts @@ -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) => Promise; + authenticationMechanism?: PersonalAccessToken; } export enum UserPageTabs { ACTIVITY = 'activity_feed', MY_DATA = 'mydata', FOLLOWING = 'following', + ACCESS_TOKEN = 'access-token', } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/mocks/User.mocks.ts b/openmetadata-ui/src/main/resources/ui/src/components/Users/mocks/User.mocks.ts index a7ba55acf25..7832c71855e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Users/mocks/User.mocks.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/mocks/User.mocks.ts @@ -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', +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index ee396df5a88..9860e022e0b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -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!", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index e1f08bb199e..76d25352879 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -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!", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index c231de47884..bea24335758 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -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!", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 1baf576eda2..4b8ce4650d9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -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 !", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 93fef93cb71..dc1790238b4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -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!", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index e917b96d5b8..fbe38bae767 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -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!", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 046e87bc822..3c64a6dfd59 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -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": "Не удалось запустить пайплайн!", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index c14989d07aa..24ed528a81b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -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": "触发工作流失败!", diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts index 5ef0e5423f4..a229b9c3cd6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts @@ -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 + >(`/users/security/token`, { + JWTTokenExpiry, + tokenName, + }); + + return response.data; +}; + +export const revokeAccessToken = async (params: string) => { + const response = await APIClient.put( + '/users/security/token/revoke?' + params + ); + + return response.data; +};