Skip to content

Commit

Permalink
feat(admin): Create 'Relying Parties' page & display RPs from db
Browse files Browse the repository at this point in the history
Because:
* We want a single source of truth to view FxA / Subplat relying parties

This commit:
* Adds a new RP model and resolver, uses a new query to read desired rows from clients table in fxa_oauth database
* Displays this data in the UI with a new /relying-parties page
* Adds RP guard to existing admin panel guards and to server/client
* Contains a Nav account icon update, CSS tweaks
  • Loading branch information
LZoog committed Jun 6, 2022
1 parent 1000766 commit 174b144
Show file tree
Hide file tree
Showing 23 changed files with 571 additions and 32 deletions.
13 changes: 10 additions & 3 deletions packages/fxa-admin-panel/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import AdminLogs from './components/AdminLogs';
import SiteStatus from './components/SiteStatus';
import { IClientConfig, IUserInfo } from '../interfaces';
import { AdminPanelFeature, AdminPanelGuard } from 'fxa-shared/guards';
import PageRelyingParties from './components/PageRelyingParties';

const App = ({ config }: { config: IClientConfig }) => {
const [guard, setGuard] = useState<AdminPanelGuard>(config.guard);
Expand All @@ -30,10 +31,16 @@ const App = ({ config }: { config: IClientConfig }) => {
<Route path="/site-status" element={<SiteStatus />} />
)}
{guard.allow(AdminPanelFeature.AccountSearch, user.group) && (
<Route path="/account-search" element={<AccountSearch />} />
<>
<Route path="/account-search" element={<AccountSearch />} />
<Route path="/" element={<Navigate to="/account-search" />} />
</>
)}
{guard.allow(AdminPanelFeature.AccountSearch, user.group) && (
<Route path="/" element={<Navigate to="/account-search" />} />
{guard.allow(AdminPanelFeature.RelyingParties, user.group) && (
<Route
path="/relying-parties"
element={<PageRelyingParties />}
/>
)}
<Route path="/permissions" element={<Permissions />} />
</Routes>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
<Nav />

<main className="flex-4">
<div className="p-4 rounded-md bg-white border border-grey-100">
<div className="p-4 rounded-md bg-white border border-grey-100 text-grey-600">
{children}
</div>
</main>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import iconExternalLink from '../../images/icon-external-link.svg';

export const LinkAbout = () => (
<LinkExternal
className="inline-flex align-self-center font-semibold text-white text-shadow-md hover:opacity-75 focus:opacity-75"
className="no-underline inline-flex align-self-center font-semibold text-white text-shadow-md hover:opacity-75 focus:opacity-75"
href="https://github.com/mozilla/fxa/blob/main/packages/fxa-admin-panel/README.md"
>
<span className="pr-2">About</span>{' '}
Expand Down
51 changes: 28 additions & 23 deletions packages/fxa-admin-panel/src/components/Nav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@

import React from 'react';
import { NavLink } from 'react-router-dom';
import mailIcon from '../../images/icon-mail.svg';
import accountIcon from '../../images/icon-account.svg';
import keyIcon from '../../images/icon-key.svg';
import statusIcon from '../../images/icon-site-status.svg';
import logsIcon from '../../images/icon-logs.svg';
import { AdminPanelFeature } from 'fxa-shared/guards';
import Guard from '../Guard';

const getNavLinkClassName = (isActive: boolean) =>
`rounded text-grey-600 flex mt-2 px-3 py-2 no-underline hover:bg-grey-100 focus:bg-grey-100 ${
isActive ? 'bg-grey-50 font-semibold' : 'bg-grey-10'
}`;

export const Nav = () => (
<nav className="mb-4 desktop:mr-5 desktop:flex-1 desktop:mb-0">
<div className="p-4 rounded-md bg-white border border-grey-100">
Expand All @@ -21,16 +27,12 @@ export const Nav = () => (
<li>
<NavLink
to="/account-search"
className={({ isActive }) =>
`rounded text-grey-600 flex mt-2 px-3 py-2 no-underline hover:bg-grey-100 focus:bg-grey-100 ${
isActive ? 'bg-grey-50 font-semibold' : 'bg-grey-10'
}`
}
className={({ isActive }) => getNavLinkClassName(isActive)}
>
<img
className="inline-flex mr-2 w-4"
src={mailIcon}
alt="mail icon"
src={accountIcon}
alt="account icon"
/>
Account Search
</NavLink>
Expand All @@ -40,11 +42,7 @@ export const Nav = () => (
<li>
<NavLink
to="/site-status"
className={({ isActive }) =>
`rounded text-grey-600 flex mt-2 px-3 py-2 no-underline hover:bg-grey-100 focus:bg-grey-100 ${
isActive ? 'bg-grey-50 font-semibold' : 'bg-grey-10'
}`
}
className={({ isActive }) => getNavLinkClassName(isActive)}
>
<img
className="inline-flex mr-2 w-4"
Expand All @@ -59,11 +57,7 @@ export const Nav = () => (
<li>
<NavLink
to="/admin-logs"
className={({ isActive }) =>
`rounded text-grey-600 flex mt-2 px-3 py-2 no-underline hover:bg-grey-100 focus:bg-grey-100 ${
isActive ? 'bg-grey-50 font-semibold' : 'bg-grey-10'
}`
}
className={({ isActive }) => getNavLinkClassName(isActive)}
>
<img
className="inline-flex mr-2 w-4"
Expand All @@ -74,14 +68,25 @@ export const Nav = () => (
</NavLink>
</li>
</Guard>
<Guard features={[AdminPanelFeature.RelyingParties]}>
<li>
<NavLink
to="/relying-parties"
className={({ isActive }) => getNavLinkClassName(isActive)}
>
<img
className="inline-flex mr-2 w-4"
src={keyIcon}
alt="key icon"
/>
Relying Parties
</NavLink>
</li>
</Guard>
<li>
<NavLink
to="/permissions"
className={({ isActive }) =>
`rounded text-grey-600 flex mt-2 px-3 py-2 no-underline hover:bg-grey-100 focus:bg-grey-100 ${
isActive ? 'bg-grey-50 font-semibold' : 'bg-grey-10'
}`
}
className={({ isActive }) => getNavLinkClassName(isActive)}
>
<img
className="inline-flex mr-2 w-4"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import React from 'react';
import { render, screen } from '@testing-library/react';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import PageRelyingParties, { GET_RELYING_PARTIES } from '.';
import { RelyingParty } from 'fxa-admin-server/src/graphql';

const mockGetRelyingParties = (
relyingParties: RelyingParty[] = []
): MockedResponse => ({
request: {
query: GET_RELYING_PARTIES,
},
result: {
data: {
relyingParties,
},
},
});

const MOCK_RP_ALL_FIELDS = {
id: 'fced6b5e3f4c66b9',
name: 'Firefox Send local-dev',
redirectUri: 'http://localhost:1337/oauth',
canGrant: true,
publicClient: true,
createdAt: 1583259953,
trusted: true,
imageUri:
'https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.png',
allowedScopes: 'https://identity.mozilla.com/apps/send',
} as RelyingParty;

const MOCK_RP_FALSY_FIELDS = {
id: '38a6b9b3a65a1871',
name: '123Done PKCE',
redirectUri: 'http://localhost:8080/?oauth_pkce_redirect=1',
canGrant: false,
publicClient: false,
createdAt: 1583259953,
trusted: false,
imageUri: '',
allowedScopes: null,
} as RelyingParty;

it('renders without imploding and shows loading text', () => {
render(
<MockedProvider mocks={[mockGetRelyingParties()]} addTypename={false}>
<PageRelyingParties />
</MockedProvider>
);
const rpHeading = screen.getByRole('heading', { level: 2 });
expect(rpHeading).toHaveTextContent('Relying Parties');
screen.getByText('Loading...');
});

it('renders as expected with zero relying parties', async () => {
render(
<MockedProvider mocks={[mockGetRelyingParties()]} addTypename={false}>
<PageRelyingParties />
</MockedProvider>
);
await new Promise((resolve) => setTimeout(resolve, 0));
screen.getByText('No relying parties were found', { exact: false });
});

it('renders as expected with a relying party containing all fields', async () => {
render(
<MockedProvider
mocks={[mockGetRelyingParties([MOCK_RP_ALL_FIELDS])]}
addTypename={false}
>
<PageRelyingParties />
</MockedProvider>
);
await new Promise((resolve) => setTimeout(resolve, 0));
screen.getByText(MOCK_RP_ALL_FIELDS.id);
screen.getByText(MOCK_RP_ALL_FIELDS.name);
screen.getByText(MOCK_RP_ALL_FIELDS.redirectUri);
screen.getByText(MOCK_RP_ALL_FIELDS.allowedScopes!);
screen.getByText('1970', { exact: false });
expect(screen.getAllByText('Yes')).toHaveLength(3);
});

it('renders as expected with a relying party containing falsy fields', async () => {
render(
<MockedProvider
mocks={[mockGetRelyingParties([MOCK_RP_FALSY_FIELDS])]}
addTypename={false}
>
<PageRelyingParties />
</MockedProvider>
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(screen.getAllByText('No')).toHaveLength(3);
screen.getByText('(empty string)');
screen.getByText('NULL');
});
Loading

0 comments on commit 174b144

Please sign in to comment.