From 35804bca1a10b4d22a65f71f194581b284323de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Lavelle?= <121855584+aindriu-aiven@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:48:30 +0100 Subject: [PATCH] Allow super user access to the coral UI (#2455) * Allow super user access to the coral UI. Signed-off-by: Aindriu Lavelle * Remove unnessecary const and unit test Signed-off-by: Aindriu Lavelle * Allow super user access to the coral UI. Signed-off-by: Aindriu Lavelle * Update to fix dashboard landing page Signed-off-by: Aindriu Lavelle * Remove request new topic button for superadmin Signed-off-by: Mirjam Aulbach * Remove request new connector button for superadmin Signed-off-by: Mirjam Aulbach * Remove unused variable Signed-off-by: Mirjam Aulbach * Remove unneeded test for AuthUser Signed-off-by: Mirjam Aulbach * Update e2e tests Signed-off-by: Mirjam Aulbach * Add isSuperAdminUser to auth hook, extend tests Signed-off-by: Mirjam Aulbach --------- Signed-off-by: Aindriu Lavelle Signed-off-by: Mirjam Aulbach Co-authored-by: Mirjam Aulbach --- .../context-provider/AuthProvider.test.tsx | 120 +++++++--------- .../src/app/context-provider/AuthProvider.tsx | 31 ++--- .../configuration/clusters/Clusters.test.tsx | 75 ++++------ .../layout/header/HeaderNavigation.test.tsx | 12 +- .../app/layout/header/HeaderNavigation.tsx | 8 +- .../main-navigation/MainNavigation.test.tsx | 18 ++- .../layout/main-navigation/MainNavigation.tsx | 11 +- .../configuration/clusters/index.test.tsx | 2 + coral/src/app/pages/connectors/index.test.tsx | 129 ++++++++++++++++++ coral/src/app/pages/connectors/index.tsx | 16 ++- coral/src/app/pages/topics/index.test.tsx | 57 +++++++- coral/src/app/pages/topics/index.tsx | 17 ++- coral/src/app/router.tsx | 21 +-- coral/src/services/feature-flags/types.ts | 1 - .../src/services/router-utils/route-utils.tsx | 12 +- coral/vite.config.ts | 3 - .../klaw/service/UtilControllerService.java | 3 +- .../main/resources/templates/clusters.html | 2 +- core/src/main/resources/templates/envs.html | 2 +- core/src/main/resources/templates/index.html | 9 +- .../main/resources/templates/showTeams.html | 2 +- .../main/resources/templates/showUsers.html | 2 +- e2e/tests/basic-health-check.spec.ts | 33 ++++- 23 files changed, 355 insertions(+), 231 deletions(-) create mode 100644 coral/src/app/pages/connectors/index.test.tsx diff --git a/coral/src/app/context-provider/AuthProvider.test.tsx b/coral/src/app/context-provider/AuthProvider.test.tsx index 6971ac70a1..1c0b4e51bf 100644 --- a/coral/src/app/context-provider/AuthProvider.test.tsx +++ b/coral/src/app/context-provider/AuthProvider.test.tsx @@ -1,13 +1,19 @@ -import { cleanup, screen, within } from "@testing-library/react"; -import { customRender } from "src/services/test-utils/render-with-wrappers"; +import { + cleanup, + render, + renderHook, + screen, + waitFor, + within, +} from "@testing-library/react"; import { AuthProvider, useAuthContext, } from "src/app/context-provider/AuthProvider"; import { waitForElementToBeRemoved } from "@testing-library/react/pure"; import { testAuthUser } from "src/domain/auth-user/auth-user-test-helper"; -import { setupFeatureFlagMock } from "src/services/feature-flags/test-helper"; -import { FeatureFlag } from "src/services/feature-flags/types"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { getQueryClientForTests } from "src/services/test-utils/query-client-tests"; const getAuthMock = jest.fn(); jest.mock("src/domain/auth-user", () => ({ @@ -15,6 +21,12 @@ jest.mock("src/domain/auth-user", () => ({ getAuth: () => getAuthMock(), })); +const AllProviders = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + const ChildComponent = () => { const authUser = useAuthContext(); @@ -22,17 +34,20 @@ const ChildComponent = () => { }; const mockAuthUser = { ...testAuthUser, username: "Jon Snow" }; +const mockAuthSuperAdminUser = { + ...testAuthUser, + username: "Arya Stark", + userrole: "SUPERADMIN", +}; + describe("AuthProvider.tsx", () => { describe("gets the auth user", () => { beforeEach(() => { getAuthMock.mockReturnValue({}); - customRender( - + render( + - , - { - queryClient: true, - } + ); }); @@ -54,13 +69,10 @@ describe("AuthProvider.tsx", () => { describe("renders an auth provider with given children when auth user is available", () => { beforeEach(async () => { getAuthMock.mockReturnValue(mockAuthUser); - customRender( - + render( + - , - { - queryClient: true, - } + ); await waitForElementToBeRemoved(screen.getByText("Loading Klaw")); @@ -84,75 +96,35 @@ describe("AuthProvider.tsx", () => { }); }); - describe("does not render the auth provider with given children when auth user is SUPERADMIN", () => { - beforeEach(async () => { - getAuthMock.mockReturnValue({ mockAuthUser, userrole: "SUPERADMIN" }); - customRender( - - - , - { - queryClient: true, - } - ); - - await waitForElementToBeRemoved(screen.getByText("Loading Klaw")); - }); - + describe("useAuthContext hook", () => { afterEach(() => { jest.resetAllMocks(); cleanup(); }); - it("does not returns context provider with given children", () => { - const childElement = screen.queryByTestId("auth-provider-child"); - expect(childElement).not.toBeInTheDocument(); - }); + it("returns the correct user data and identifies a SUPERADMIN user", async () => { + getAuthMock.mockResolvedValue(mockAuthSuperAdminUser); - it("shows a dialog informing superadmin they can not access coral", async () => { - const dialog = await screen.findByRole("dialog", { - name: "You're currently logged in as superadmin.", + const { result } = renderHook(() => useAuthContext(), { + wrapper: AllProviders, }); - expect(dialog).toBeVisible(); - }); - }); - - describe("render the auth provider with given children when auth user is SUPERADMIN and feature flag is enabled", () => { - beforeEach(async () => { - setupFeatureFlagMock( - FeatureFlag.FEATURE_FLAG_SUPER_ADMIN_ACCESS_CORAL, - true - ); - getAuthMock.mockReturnValue({ mockAuthUser, userrole: "SUPERADMIN" }); - customRender( - - - , - { - queryClient: true, - } - ); - - await waitForElementToBeRemoved(screen.getByText("Loading Klaw")); - }); - - afterEach(() => { - jest.resetAllMocks(); - cleanup(); - }); - - it("returns context provider with given children", () => { - const childElement = screen.getByTestId("auth-provider-child"); - expect(childElement).toBeVisible(); + await waitFor(() => { + expect(result.current.username).toEqual("Arya Stark"); + expect(result.current.isSuperAdminUser).toBe(true); + }); }); - it("does not show dialog informing superadmin they can not access coral", () => { - const dialog = screen.queryByText( - "You're currently logged in as superadmin." - ); + it("returns the correct user data and identifies a non-SUPERADMIN user", async () => { + getAuthMock.mockResolvedValue(mockAuthUser); + const { result } = renderHook(() => useAuthContext(), { + wrapper: AllProviders, + }); - expect(dialog).not.toBeInTheDocument(); + await waitFor(() => { + expect(result.current.username).toEqual("Jon Snow"); + expect(result.current.isSuperAdminUser).toBe(false); + }); }); }); }); diff --git a/coral/src/app/context-provider/AuthProvider.tsx b/coral/src/app/context-provider/AuthProvider.tsx index 25e17a6ba2..53ab503a44 100644 --- a/coral/src/app/context-provider/AuthProvider.tsx +++ b/coral/src/app/context-provider/AuthProvider.tsx @@ -4,9 +4,6 @@ import { AuthUser, getAuth, isSuperAdmin } from "src/domain/auth-user"; import { BasePage } from "src/app/layout/page/BasePage"; import { Box, Icon } from "@aivenio/aquarium"; import loading from "@aivenio/aquarium/icons/loading"; -import { NoCoralAccessSuperadmin } from "src/app/components/NoCoralAccessSuperadmin"; -import useFeatureFlag from "src/services/feature-flags/hook/useFeatureFlag"; -import { FeatureFlag } from "src/services/feature-flags/types"; /** We don't do Authentication on Corals side * at the moment, so we only have a AuthUser @@ -50,7 +47,15 @@ const AuthContext = createContext({ }, }); -const useAuthContext = () => useContext(AuthContext); +type UseAuthContext = AuthUser & { isSuperAdminUser: boolean }; + +const useAuthContext = (): UseAuthContext => { + const authUser = useContext(AuthContext); + + const isSuperAdminUser = isSuperAdmin(authUser); + + return { ...authUser, isSuperAdminUser }; +}; const AuthProvider = ({ children }: { children: ReactNode }) => { const { data: authUser, isLoading } = useQuery( @@ -58,23 +63,6 @@ const AuthProvider = ({ children }: { children: ReactNode }) => { getAuth ); - const superAdminAccessCoralEnabled = useFeatureFlag( - FeatureFlag.FEATURE_FLAG_SUPER_ADMIN_ACCESS_CORAL - ); - - // SUPERADMIN does not have access to coral, so we show a reduced page with - // information about that and nothing else. - if ( - !isLoading && - authUser && - isSuperAdmin(authUser) && - !superAdminAccessCoralEnabled - ) { - return ( - } content={} /> - ); - } - if (!isLoading && authUser) { return ( {children} @@ -94,3 +82,4 @@ const AuthProvider = ({ children }: { children: ReactNode }) => { }; export { useAuthContext, AuthProvider }; +export type { UseAuthContext }; diff --git a/coral/src/app/features/configuration/clusters/Clusters.test.tsx b/coral/src/app/features/configuration/clusters/Clusters.test.tsx index 8c7e963f19..71005fe6b8 100644 --- a/coral/src/app/features/configuration/clusters/Clusters.test.tsx +++ b/coral/src/app/features/configuration/clusters/Clusters.test.tsx @@ -11,45 +11,16 @@ import { KlawApiError } from "src/services/api"; import { mockIntersectionObserver } from "src/services/test-utils/mock-intersection-observer"; import { userEvent } from "@testing-library/user-event"; import { clusterTypeToString } from "src/services/formatter/cluster-type-formatter"; -import { useAuthContext } from "src/app/context-provider/AuthProvider"; -import { AuthUser } from "src/domain/auth-user"; - -const INITIAL_AUTH_DATA: AuthUser = { - username: "", - userrole: "", - teamname: "", - teamId: "", - canSwitchTeams: "", - totalTeamTopics: 0, - totalOrgTopics: 0, - permissions: { - addDeleteEditClusters: false, - canShutdownKw: false, - canUpdatePermissions: false, - addEditRoles: false, - viewTopics: false, - requestItems: false, - viewKafkaConnect: false, - syncBackTopics: false, - syncBackSchemas: false, - syncBackAcls: false, - updateServerConfig: false, - showServerConfigEnvProperties: false, - addUser: false, - addTeams: false, - syncTopicsAcls: false, - syncConnectors: false, - manageConnectors: false, - syncSchemas: false, - approveAtleastOneRequest: false, - approveDeclineTopics: false, - approveDeclineOperationalReqs: false, - approveDeclineSubscriptions: false, - approveDeclineSchemas: false, - approveDeclineConnectors: false, - showAddDeleteTenants: false, - addDeleteEditEnvs: false, - }, +import { + UseAuthContext, + useAuthContext, +} from "src/app/context-provider/AuthProvider"; + +import { testAuthUser } from "src/domain/auth-user/auth-user-test-helper"; + +const INITIAL_AUTH_USER_CONTEXT_DATA: UseAuthContext = { + ...testAuthUser, + isSuperAdminUser: false, }; jest.mock("src/domain/cluster/cluster-api.ts"); @@ -114,7 +85,7 @@ describe("Clusters.tsx", () => { totalPages: 1, entries: [], }); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, memoryRouter: true }); }); @@ -137,7 +108,7 @@ describe("Clusters.tsx", () => { totalPages: 1, entries: [], }); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, memoryRouter: true }); await waitForElementToBeRemoved(screen.getByTestId("skeleton-table")); @@ -168,7 +139,7 @@ describe("Clusters.tsx", () => { beforeAll(async () => { jest.spyOn(console, "error").mockImplementation((error) => error); mockGetClustersPaginated.mockRejectedValue(testError); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, memoryRouter: true }); @@ -201,7 +172,7 @@ describe("Clusters.tsx", () => { totalPages: 1, entries: testCluster, }); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, memoryRouter: true }); @@ -271,7 +242,7 @@ describe("Clusters.tsx", () => { totalPages: 3, entries: testCluster, }); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, memoryRouter: true }); @@ -307,7 +278,7 @@ describe("Clusters.tsx", () => { totalPages: 5, entries: testCluster, }); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, memoryRouter: true }); @@ -350,7 +321,7 @@ describe("Clusters.tsx", () => { totalPages: 5, entries: testCluster, }); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, memoryRouter: true }); @@ -390,7 +361,7 @@ describe("Clusters.tsx", () => { totalPages: 5, entries: testCluster, }); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, memoryRouter: true }); @@ -432,7 +403,7 @@ describe("Clusters.tsx", () => { totalPages: 1, entries: [testCluster[0]], }); - mockUseAuthContext.mockReturnValue(INITIAL_AUTH_DATA); + mockUseAuthContext.mockReturnValue(INITIAL_AUTH_USER_CONTEXT_DATA); customRender(, { queryClient: true, @@ -475,9 +446,9 @@ describe("Clusters.tsx", () => { }); mockUseAuthContext.mockReturnValue({ - ...INITIAL_AUTH_DATA, + ...INITIAL_AUTH_USER_CONTEXT_DATA, permissions: { - ...INITIAL_AUTH_DATA.permissions, + ...INITIAL_AUTH_USER_CONTEXT_DATA.permissions, addDeleteEditClusters: true, }, }); @@ -541,9 +512,9 @@ describe("Clusters.tsx", () => { }); mockUseAuthContext.mockReturnValue({ - ...INITIAL_AUTH_DATA, + ...INITIAL_AUTH_USER_CONTEXT_DATA, permissions: { - ...INITIAL_AUTH_DATA.permissions, + ...INITIAL_AUTH_USER_CONTEXT_DATA.permissions, addDeleteEditClusters: false, }, }); diff --git a/coral/src/app/layout/header/HeaderNavigation.test.tsx b/coral/src/app/layout/header/HeaderNavigation.test.tsx index a8cfdcc414..7648485a0c 100644 --- a/coral/src/app/layout/header/HeaderNavigation.test.tsx +++ b/coral/src/app/layout/header/HeaderNavigation.test.tsx @@ -7,6 +7,7 @@ import { } from "src/services/test-utils/tabbing"; import * as hook from "src/app/hooks/usePendingRequests"; import { testAuthUser } from "src/domain/auth-user/auth-user-test-helper"; +import { UseAuthContext } from "src/app/context-provider/AuthProvider"; const mockToast = jest.fn(); const mockDismiss = jest.fn(); @@ -36,9 +37,12 @@ const mockedPendingRequests = { TOTAL_NOTIFICATIONS: 6, }; -let mockAuthUser = testAuthUser; +let mockAuthUserContext: UseAuthContext = { + ...testAuthUser, + isSuperAdminUser: false, +}; jest.mock("src/app/context-provider/AuthProvider", () => ({ - useAuthContext: () => mockAuthUser, + useAuthContext: () => mockAuthUserContext, })); describe("HeaderNavigation.tsx", () => { @@ -109,7 +113,7 @@ describe("HeaderNavigation.tsx", () => { describe("removes specific elements if user is superadmin", () => { beforeAll(() => { - mockAuthUser = { ...testAuthUser, userrole: "SUPERADMIN" }; + mockAuthUserContext = { ...testAuthUser, isSuperAdminUser: true }; jest .spyOn(hook, "usePendingRequests") .mockImplementation(() => mockedNoPendingRequests); @@ -118,7 +122,7 @@ describe("HeaderNavigation.tsx", () => { }); afterAll(() => { - mockAuthUser = testAuthUser; + mockAuthUserContext = { ...testAuthUser, isSuperAdminUser: false }; cleanup(); jest.clearAllMocks(); }); diff --git a/coral/src/app/layout/header/HeaderNavigation.tsx b/coral/src/app/layout/header/HeaderNavigation.tsx index ae060088d2..88e0aabc60 100644 --- a/coral/src/app/layout/header/HeaderNavigation.tsx +++ b/coral/src/app/layout/header/HeaderNavigation.tsx @@ -19,14 +19,12 @@ const requestNewEntityPaths: { [key: string]: string } = { }; function HeaderNavigation() { - const { userrole } = useAuthContext(); + const { isSuperAdminUser } = useAuthContext(); const navigate = useNavigate(); - const userIsSuperAdmin = userrole === "SUPERADMIN"; - return ( - {!userIsSuperAdmin && ( + {!isSuperAdminUser && ( { if (requestNewEntityPaths[key.toString()] !== undefined) { @@ -67,7 +65,7 @@ function HeaderNavigation() { colGap={"l2"} alignItems={"baseline"} > - {!userIsSuperAdmin && ( + {!isSuperAdminUser && (
  • diff --git a/coral/src/app/layout/main-navigation/MainNavigation.test.tsx b/coral/src/app/layout/main-navigation/MainNavigation.test.tsx index 2f3e22cb76..b95da62805 100644 --- a/coral/src/app/layout/main-navigation/MainNavigation.test.tsx +++ b/coral/src/app/layout/main-navigation/MainNavigation.test.tsx @@ -8,8 +8,7 @@ import { tabThroughBackward, tabThroughForward, } from "src/services/test-utils/tabbing"; -import { setupFeatureFlagMock } from "src/services/feature-flags/test-helper"; -import { FeatureFlag } from "src/services/feature-flags/types"; +import { UseAuthContext } from "src/app/context-provider/AuthProvider"; jest.mock("src/domain/team/team-api.ts"); @@ -20,9 +19,12 @@ jest.mock("src/domain/requests/requests-api.ts", () => ({ getRequestsWaitingForApproval: () => mockGetRequestsWaitingForApproval(), })); -let mockAuthUser = testAuthUser; +let mockAuthUserContext: UseAuthContext = { + ...testAuthUser, + isSuperAdminUser: false, +}; jest.mock("src/app/context-provider/AuthProvider", () => ({ - useAuthContext: () => mockAuthUser, + useAuthContext: () => mockAuthUserContext, })); const mockedUseToast = jest.fn(); @@ -299,11 +301,7 @@ describe("MainNavigation.tsx", () => { ]; beforeAll(() => { - mockAuthUser = { ...testAuthUser, userrole: "SUPERADMIN" }; - setupFeatureFlagMock( - FeatureFlag.FEATURE_FLAG_SUPER_ADMIN_ACCESS_CORAL, - true - ); + mockAuthUserContext = { ...testAuthUser, isSuperAdminUser: true }; mockGetRequestsStatistics.mockResolvedValue([]); mockGetRequestsWaitingForApproval.mockResolvedValue([]); customRender(, { @@ -315,7 +313,7 @@ describe("MainNavigation.tsx", () => { afterAll(() => { cleanup(); - mockAuthUser = testAuthUser; + mockAuthUserContext = { ...testAuthUser, isSuperAdminUser: false }; }); it("renders the main navigation", () => { diff --git a/coral/src/app/layout/main-navigation/MainNavigation.tsx b/coral/src/app/layout/main-navigation/MainNavigation.tsx index c5478cae91..29627acf55 100644 --- a/coral/src/app/layout/main-navigation/MainNavigation.tsx +++ b/coral/src/app/layout/main-navigation/MainNavigation.tsx @@ -15,24 +15,17 @@ import { TeamInfo } from "src/app/features/team-info/TeamInfo"; import { usePendingRequests } from "src/app/hooks/usePendingRequests"; import MainNavigationLink from "src/app/layout/main-navigation/MainNavigationLink"; import MainNavigationSubmenuList from "src/app/layout/main-navigation/MainNavigationSubmenuList"; -import useFeatureFlag from "src/services/feature-flags/hook/useFeatureFlag"; -import { FeatureFlag } from "src/services/feature-flags/types"; import { Routes } from "src/services/router-utils/types"; function MainNavigation() { const { pathname } = useLocation(); const { TOTAL_NOTIFICATIONS } = usePendingRequests(); - const { userrole } = useAuthContext(); + const { isSuperAdminUser } = useAuthContext(); const [hideFeedbackForm, setHideFeedbackForm] = useState( Boolean(localStorage.getItem("hideFeedbackForm")) ); - const superadminAccessCoralEnabled = useFeatureFlag( - FeatureFlag.FEATURE_FLAG_SUPER_ADMIN_ACCESS_CORAL - ); - - const showNavigationForSuperAdmin = - superadminAccessCoralEnabled && userrole === "SUPERADMIN"; + const showNavigationForSuperAdmin = isSuperAdminUser; return ( { it('renders "Add new cluster" button when addDeleteEditClusters permission is true', () => { mockUseAuthContext.mockReturnValue({ ...testAuthUser, + isSuperAdminUser: false, permissions: { ...testAuthUser.permissions, addDeleteEditClusters: true }, }); @@ -35,6 +36,7 @@ describe("ClustersPage", () => { it('does not render "Add new cluster" button when addDeleteEditClusters permission is false', () => { mockUseAuthContext.mockReturnValue({ ...testAuthUser, + isSuperAdminUser: false, permissions: { ...testAuthUser.permissions, addDeleteEditClusters: false, diff --git a/coral/src/app/pages/connectors/index.test.tsx b/coral/src/app/pages/connectors/index.test.tsx new file mode 100644 index 0000000000..a01a4ef024 --- /dev/null +++ b/coral/src/app/pages/connectors/index.test.tsx @@ -0,0 +1,129 @@ +import { cleanup, screen } from "@testing-library/react/pure"; +import { userEvent } from "@testing-library/user-event"; +import Connectors from "src/app/pages/connectors"; +import { mockIntersectionObserver } from "src/services/test-utils/mock-intersection-observer"; +import { customRender } from "src/services/test-utils/render-with-wrappers"; +import { tabNavigateTo } from "src/services/test-utils/tabbing"; +import { testAuthUser } from "src/domain/auth-user/auth-user-test-helper"; + +// Mocking the BrowseConnectors component +// so this test only confirms the correct component +// (that is already tested) is rendered. +// eslint-disable-next-line react/display-name +jest.mock("src/app/features/connectors/browse/BrowseConnectors", () => () => ( +
    +)); + +const mockedNavigator = jest.fn(); +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useNavigate: () => mockedNavigator, +})); + +let mockAuthContext = { ...testAuthUser, isSuperAdminUser: false }; +jest.mock("src/app/context-provider/AuthProvider", () => ({ + useAuthContext: () => mockAuthContext, +})); + +describe("Connectors", () => { + beforeAll(() => { + mockIntersectionObserver(); + }); + + describe("renders default view with data for users", () => { + beforeAll(() => { + mockAuthContext = { ...testAuthUser, isSuperAdminUser: false }; + + customRender(, { + memoryRouter: true, + queryClient: true, + aquariumContext: true, + }); + }); + + afterAll(() => { + cleanup(); + }); + + it("shows a headline", async () => { + const headline = screen.getByRole("heading", { + name: "Connectors", + }); + + expect(headline).toBeVisible(); + }); + + it("renders 'Request new connector' button in heading", async () => { + const button = screen.getByRole("button", { + name: "Request new connector", + }); + + expect(button).toBeVisible(); + expect(button).toBeEnabled(); + }); + + it("navigates to '/connectors/request' when user clicks the button 'Request new connector'", async () => { + const button = screen.getByRole("button", { + name: "Request new connector", + }); + + await userEvent.click(button); + + expect(mockedNavigator).toHaveBeenCalledWith("/connectors/request"); + }); + + it("navigates to '/connectors/request' when user presses Enter while 'Request new topic' button is focused", async () => { + const button = screen.getByRole("button", { + name: "Request new connector", + }); + + await tabNavigateTo({ targetElement: button }); + + await userEvent.keyboard("{Enter}"); + + expect(mockedNavigator).toHaveBeenCalledWith("/connectors/request"); + }); + + it("renders the BrowserConnectors component rendering the table", async () => { + const component = screen.getByTestId("mocked-BrowseConnectors"); + + expect(component).toBeVisible(); + }); + }); + + describe("does not render the button to request a new topic for superadmin", () => { + beforeAll(() => { + mockAuthContext = { ...testAuthUser, isSuperAdminUser: true }; + + customRender(, { + memoryRouter: true, + queryClient: true, + aquariumContext: true, + }); + }); + + afterAll(() => { + cleanup(); + }); + + it("shows the same headline", async () => { + const headline = screen.getByRole("heading", { + name: "Connectors", + }); + + expect(headline).toBeVisible(); + }); + + it("renders the BrowserConnectors component rendering the table", async () => { + const component = screen.getByTestId("mocked-BrowseConnectors"); + + expect(component).toBeVisible(); + }); + + it("does not renders 'Request new connector' button", async () => { + const button = screen.queryByText("Request new connector"); + + expect(button).not.toBeInTheDocument(); + }); + }); +}); diff --git a/coral/src/app/pages/connectors/index.tsx b/coral/src/app/pages/connectors/index.tsx index df137b9d06..4326433bee 100644 --- a/coral/src/app/pages/connectors/index.tsx +++ b/coral/src/app/pages/connectors/index.tsx @@ -3,20 +3,26 @@ import add from "@aivenio/aquarium/dist/src/icons/add"; import { useNavigate } from "react-router-dom"; import PreviewBanner from "src/app/components/PreviewBanner"; import BrowseConnectors from "src/app/features/connectors/browse/BrowseConnectors"; +import { useAuthContext } from "src/app/context-provider/AuthProvider"; const ConnectorsPage = () => { const navigate = useNavigate(); + const { isSuperAdminUser } = useAuthContext(); return ( <> navigate("/connectors/request"), - icon: add, - }} + primaryAction={ + !isSuperAdminUser + ? { + text: "Request new connector", + onClick: () => navigate("/connectors/request"), + icon: add, + } + : undefined + } /> diff --git a/coral/src/app/pages/topics/index.test.tsx b/coral/src/app/pages/topics/index.test.tsx index e9fea93729..8b7f2a39fc 100644 --- a/coral/src/app/pages/topics/index.test.tsx +++ b/coral/src/app/pages/topics/index.test.tsx @@ -14,6 +14,8 @@ import { TopicApiResponse } from "src/domain/topic/topic-types"; import { mockIntersectionObserver } from "src/services/test-utils/mock-intersection-observer"; import { customRender } from "src/services/test-utils/render-with-wrappers"; import { tabNavigateTo } from "src/services/test-utils/tabbing"; +import { testAuthUser } from "src/domain/auth-user/auth-user-test-helper"; +import { UseAuthContext } from "src/app/context-provider/AuthProvider"; const mockedNavigator = jest.fn(); jest.mock("react-router-dom", () => ({ @@ -21,6 +23,14 @@ jest.mock("react-router-dom", () => ({ useNavigate: () => mockedNavigator, })); +let mockAuthUserContext: UseAuthContext = { + ...testAuthUser, + isSuperAdminUser: false, +}; +jest.mock("src/app/context-provider/AuthProvider", () => ({ + useAuthContext: () => mockAuthUserContext, +})); + jest.mock("src/domain/team/team-api.ts"); jest.mock("src/domain/topic/topic-api.ts"); jest.mock("src/domain/environment/environment-api.ts"); @@ -39,8 +49,9 @@ describe("Topics", () => { mockIntersectionObserver(); }); - describe("renders default view with data from API", () => { + describe("renders default view with data from API for users", () => { beforeAll(async () => { + mockAuthUserContext = { ...testAuthUser, isSuperAdminUser: false }; mockGetTeams.mockResolvedValue([]); mockGetEnvironments.mockResolvedValue([]); mockGetTopics.mockResolvedValue(mockGetTopicsResponse); @@ -74,7 +85,7 @@ describe("Topics", () => { expect(button).toBeEnabled(); }); - it("navigates to '/requestTopics' when user clicks the button 'Request new topic'", async () => { + it("navigates to '/topics/request' when user clicks the button 'Request new topic'", async () => { const button = screen.getByRole("button", { name: "Request new topic", }); @@ -84,7 +95,7 @@ describe("Topics", () => { expect(mockedNavigator).toHaveBeenCalledWith("/topics/request"); }); - it("navigates to '/requestTopics' when user presses Enter while 'Request new topic' button is focused", async () => { + it("navigates to '/topics/request' when user presses Enter while 'Request new topic' button is focused", async () => { const button = screen.getByRole("button", { name: "Request new topic", }); @@ -110,4 +121,44 @@ describe("Topics", () => { expect(row).toHaveLength(mockedResponseTransformed.entries.length + 1); }); }); + + describe("does not render the button to request a new topic for superadmin", () => { + beforeAll(async () => { + mockAuthUserContext = { ...testAuthUser, isSuperAdminUser: true }; + mockGetTeams.mockResolvedValue([]); + mockGetEnvironments.mockResolvedValue([]); + mockGetTopics.mockResolvedValue(mockGetTopicsResponse); + + customRender(, { + memoryRouter: true, + queryClient: true, + aquariumContext: true, + }); + await waitForElementToBeRemoved(screen.getByTestId("skeleton-table")); + }); + + afterAll(() => { + cleanup(); + }); + + it("shows the same headline", async () => { + const headline = screen.getByRole("heading", { + name: "Topics", + }); + + expect(headline).toBeVisible(); + }); + + it("shows the table with topics", async () => { + const table = screen.getByRole("table", { name: /Topics overview/ }); + + expect(table).toBeVisible(); + }); + + it("does not renders 'Request new topic' button", async () => { + const button = screen.queryByText("Request new topic"); + + expect(button).not.toBeInTheDocument(); + }); + }); }); diff --git a/coral/src/app/pages/topics/index.tsx b/coral/src/app/pages/topics/index.tsx index c777d55a28..d89a5071ee 100644 --- a/coral/src/app/pages/topics/index.tsx +++ b/coral/src/app/pages/topics/index.tsx @@ -3,19 +3,26 @@ import add from "@aivenio/aquarium/dist/src/icons/add"; import { useNavigate } from "react-router-dom"; import PreviewBanner from "src/app/components/PreviewBanner"; import BrowseTopics from "src/app/features/topics/browse/BrowseTopics"; +import { useAuthContext } from "src/app/context-provider/AuthProvider"; const Topics = () => { const navigate = useNavigate(); + const { isSuperAdminUser } = useAuthContext(); + return ( <> navigate("/topics/request"), - icon: add, - }} + primaryAction={ + !isSuperAdminUser + ? { + text: "Request new topic", + onClick: () => navigate("/topics/request"), + icon: add, + } + : undefined + } /> diff --git a/coral/src/app/router.tsx b/coral/src/app/router.tsx index 6865a7bb5f..033918ad25 100644 --- a/coral/src/app/router.tsx +++ b/coral/src/app/router.tsx @@ -69,11 +69,6 @@ import { filteredRoutesForSuperAdmin, SuperadminRouteMap, } from "src/services/router-utils/route-utils"; -import { isFeatureFlagActive } from "src/services/feature-flags/utils"; - -const superAdminAccessCoralEnabled = isFeatureFlagActive( - FeatureFlag.FEATURE_FLAG_SUPER_ADMIN_ACCESS_CORAL -); const routes: Array = [ { @@ -461,15 +456,11 @@ const superadminRouteMap: SuperadminRouteMap = { }, }; -// until we have permission in place like planned, -// we are filtering the `routes` object and handling -// routing based on role -const routeToUse = superAdminAccessCoralEnabled - ? filteredRoutesForSuperAdmin(routes, superadminRouteMap) - : routes; - -const router = createBrowserRouter(routeToUse, { - basename: getRouterBasename(), -}); +const router = createBrowserRouter( + filteredRoutesForSuperAdmin(routes, superadminRouteMap), + { + basename: getRouterBasename(), + } +); export default router; diff --git a/coral/src/services/feature-flags/types.ts b/coral/src/services/feature-flags/types.ts index 6ebbda0f5d..2caf70cb70 100644 --- a/coral/src/services/feature-flags/types.ts +++ b/coral/src/services/feature-flags/types.ts @@ -1,6 +1,5 @@ enum FeatureFlag { FEATURE_FLAG_ADD_CLUSTER = "FEATURE_FLAG_ADD_CLUSTER", - FEATURE_FLAG_SUPER_ADMIN_ACCESS_CORAL = "FEATURE_FLAG_SUPER_ADMIN_ACCESS_CORAL", } export { FeatureFlag }; diff --git a/coral/src/services/router-utils/route-utils.tsx b/coral/src/services/router-utils/route-utils.tsx index a5e4a8fc26..7dccddc7dd 100644 --- a/coral/src/services/router-utils/route-utils.tsx +++ b/coral/src/services/router-utils/route-utils.tsx @@ -181,7 +181,7 @@ const SuperadminRoute = ({ showNotFound?: boolean; removeChildren?: boolean; }) => { - const { userrole } = useAuthContext(); + const { isSuperAdminUser } = useAuthContext(); const routeParams = useParams(); const redirectStaticPart = redirectSuperAdmin.split(":")[0]; @@ -189,23 +189,21 @@ const SuperadminRoute = ({ const redirectParam = routeParams[param] ? routeParams[param] : ""; const redirectPath = `${redirectStaticPart}${redirectParam}`; - const isSuperadmin = userrole === "SUPERADMIN"; - useEffect(() => { - if (isSuperadmin && !showNotFound) { + if (isSuperAdminUser && !showNotFound) { window.location.replace(`${window.location.origin}${redirectPath}`); } }); - if (!isSuperadmin) { + if (!isSuperAdminUser) { return children; } - if (isSuperadmin && showNotFound) { + if (isSuperAdminUser && showNotFound) { return ; } - if (isSuperadmin && removeChildren) { + if (isSuperAdminUser && removeChildren) { return []; } diff --git a/coral/vite.config.ts b/coral/vite.config.ts index d81060814a..a81c75d1e4 100644 --- a/coral/vite.config.ts +++ b/coral/vite.config.ts @@ -140,9 +140,6 @@ export default defineConfig(({ mode }) => { FEATURE_FLAG_ADD_CLUSTER: ["development", "remote-api"] .includes(mode) .toString(), - FEATURE_FLAG_SUPER_ADMIN_ACCESS_CORAL: ["development", "remote-api"] - .includes(mode) - .toString(), }, }, css: { diff --git a/core/src/main/java/io/aiven/klaw/service/UtilControllerService.java b/core/src/main/java/io/aiven/klaw/service/UtilControllerService.java index 71205b470b..f6754bfaba 100644 --- a/core/src/main/java/io/aiven/klaw/service/UtilControllerService.java +++ b/core/src/main/java/io/aiven/klaw/service/UtilControllerService.java @@ -519,8 +519,7 @@ public AuthenticationInfo getAuth() { authenticationInfo.setGoogleFeedbackFormLink(GOOGLE_FEEDBACK_FORM_LINK); // coral attributes - authenticationInfo.setCoralEnabled( - Boolean.toString(coralEnabled && isCoralBuilt && !isUserSuperadmin)); + authenticationInfo.setCoralEnabled(Boolean.toString(coralEnabled && isCoralBuilt)); authenticationInfo.setCoralAvailableForUser(Boolean.toString(coralEnabled && isCoralBuilt)); diff --git a/core/src/main/resources/templates/clusters.html b/core/src/main/resources/templates/clusters.html index 9682e05a31..932291b3b1 100644 --- a/core/src/main/resources/templates/clusters.html +++ b/core/src/main/resources/templates/clusters.html @@ -460,7 +460,7 @@

    Shortcuts

    -
    +
    New user interface available

    diff --git a/core/src/main/resources/templates/envs.html b/core/src/main/resources/templates/envs.html index 0fc0ae615c..7df64f6427 100644 --- a/core/src/main/resources/templates/envs.html +++ b/core/src/main/resources/templates/envs.html @@ -437,7 +437,7 @@

    Shortcuts

    -
    +
    New user interface available

    diff --git a/core/src/main/resources/templates/index.html b/core/src/main/resources/templates/index.html index 833d49469f..c8dc28e75b 100644 --- a/core/src/main/resources/templates/index.html +++ b/core/src/main/resources/templates/index.html @@ -441,14 +441,7 @@

    Shortcuts

    -
    -
    -
    Explore the new user interface
    -

    - You're currently logged in as superadmin. To experience the new user interface, switch to your user account. -

    -
    -
    +
    diff --git a/core/src/main/resources/templates/showTeams.html b/core/src/main/resources/templates/showTeams.html index d1227613c9..18e5f41057 100644 --- a/core/src/main/resources/templates/showTeams.html +++ b/core/src/main/resources/templates/showTeams.html @@ -444,7 +444,7 @@

    Shortcuts

    -
    +
    New user interface available

    diff --git a/core/src/main/resources/templates/showUsers.html b/core/src/main/resources/templates/showUsers.html index 1508aac365..d4faec9ff6 100644 --- a/core/src/main/resources/templates/showUsers.html +++ b/core/src/main/resources/templates/showUsers.html @@ -438,7 +438,7 @@

    Shortcuts

    -
    +
    New user interface available

    diff --git a/e2e/tests/basic-health-check.spec.ts b/e2e/tests/basic-health-check.spec.ts index c7e46c9ed8..722f22756a 100644 --- a/e2e/tests/basic-health-check.spec.ts +++ b/e2e/tests/basic-health-check.spec.ts @@ -34,6 +34,29 @@ test("user can login with default superadmin user", async ({ page }) => { await expect(profileForSuperAdmin).toBeVisible(); }); +test("Klaw is build", async ({ page }) => { + await page.goto("/"); + const loader = page.locator(".preloader"); + await expect(loader).not.toBeVisible(); + + await page.getByPlaceholder("Username").fill(superAdminUserName); + await page.getByPlaceholder("Password").fill(superAdminPassword); + await page.getByRole("button", { name: "Continue" }).click(); + + const profileForSuperAdmin = await page.getByRole("button", { + name: "superadmin", + exact: true, + }); + + await expect(profileForSuperAdmin).toBeVisible(); + + const klawConfigurationWizard = await page.getByRole("heading", { + name: /configure klaw wizard/i, + }); + + await expect(klawConfigurationWizard).toBeVisible(); +}); + test("coral is build", async ({ page }) => { await page.goto("/"); const loader = page.locator(".preloader"); @@ -52,9 +75,13 @@ test("coral is build", async ({ page }) => { await page.goto("/coral/"); - const coralSuperAdminDialog = await page.getByRole("dialog", { - name: /you're currently logged in as superadmin\./i, + const coralPreviewBanner = await page.getByRole("region", { + name: /preview disclaimer/i, + }); + const coralDashboardPage = await page.getByRole("heading", { + name: /dashboard/i, }); - await expect(coralSuperAdminDialog).toBeVisible(); + await expect(coralPreviewBanner).toBeVisible(); + await expect(coralDashboardPage).toBeVisible(); });