Skip to content

Commit

Permalink
Merge pull request #12871 from mozilla/FXA-5027-fix-remote-group-reso…
Browse files Browse the repository at this point in the history
…lution

 fix(admin-panel): Fix remote group resolution
  • Loading branch information
dschom authored May 13, 2022
2 parents 7233c94 + 6339b0b commit 9c14190
Show file tree
Hide file tree
Showing 21 changed files with 352 additions and 59 deletions.
3 changes: 2 additions & 1 deletion packages/fxa-admin-panel/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { IncomingHttpHeaders } from 'http';
import { USER_EMAIL_HEADER, USER_GROUP_HEADER } from '../constants';
import { IGroup } from 'fxa-shared/guards';
import { AdminPanelGuard, IGroup } from 'fxa-shared/guards';

export interface IUserInfo {
group: IGroup;
Expand All @@ -17,6 +17,7 @@ export interface IServerInfo {

export interface IClientConfig {
env: string;
guard: AdminPanelGuard;
user: IUserInfo;
servers: {
admin: IServerInfo;
Expand Down
2 changes: 2 additions & 0 deletions packages/fxa-admin-panel/server/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import convict from 'convict';
import fs from 'fs';
import path from 'path';
import { GuardConfig } from 'fxa-shared/guards';

convict.addFormats(require('convict-format-with-moment'));
convict.addFormats(require('convict-format-with-validator'));
Expand Down Expand Up @@ -179,6 +180,7 @@ const conf = convict({
format: String,
},
},
...GuardConfig,
});

// handle configuration files. you can specify a CSV list of configuration
Expand Down
41 changes: 38 additions & 3 deletions packages/fxa-admin-panel/server/lib/client-config/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
import { ClientConfig } from '.';
import { SERVER_CONFIG_PLACEHOLDER } from '../../../constants';
import { JSDOM } from 'jsdom';
import { AdminPanelGroup, PermissionLevel } from 'fxa-shared/guards';
import {
AdminPanelEnv,
AdminPanelGroup,
PermissionLevel,
USER_EMAIL_HEADER,
USER_GROUP_HEADER,
} from 'fxa-shared/guards';
import { IUserInfo } from '../../../interfaces';

describe('ClientConfig', () => {
Expand All @@ -15,8 +21,8 @@ describe('ClientConfig', () => {
user,
});
const injectedHtml = ClientConfig.injectIntoHtml(html, {
'REMOTE-GROUP': remoteHeader,
'oidc-claim-id-token-email': user.email,
[USER_GROUP_HEADER]: remoteHeader,
[USER_EMAIL_HEADER]: user.email,
});

const injectedVal = JSDOM.fragment(injectedHtml)
Expand All @@ -42,6 +48,7 @@ describe('ClientConfig', () => {
name: 'Admin',
header: 'vpn_fxa_admin_panel_prod',
level: PermissionLevel.Admin,
env: AdminPanelEnv.Prod,
},
});
});
Expand All @@ -53,6 +60,7 @@ describe('ClientConfig', () => {
name: 'Support',
header: 'vpn_fxa_supportagent_prod',
level: PermissionLevel.Support,
env: AdminPanelEnv.Prod,
},
});
});
Expand All @@ -67,4 +75,31 @@ describe('ClientConfig', () => {
},
});
});

it('handles noisy remote groups header', () => {
configCheck('test,test2,vpn_fxa_supportagent_prod,test3', {
email: 'hello@mozilla.com',
group: {
name: 'Support',
header: AdminPanelGroup.SupportAgentProd,
level: PermissionLevel.Support,
env: AdminPanelEnv.Prod,
},
});
});

it('handles multiple remote group headers', () => {
configCheck(
'test,vpn_default,vpn_fxa_admin_panel_prod,vpn_fxa_admin_panel_stage,test',
{
email: 'hello@mozilla.com',
group: {
name: 'Admin',
header: AdminPanelGroup.AdminProd,
level: PermissionLevel.Admin,
env: AdminPanelEnv.Prod,
},
}
);
});
});
5 changes: 4 additions & 1 deletion packages/fxa-admin-panel/server/lib/client-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import {
USER_GROUP_HEADER,
SERVER_CONFIG_PLACEHOLDER,
} from '../../../constants';
import { guard } from 'fxa-shared/guards';
import { AdminPanelGuard } from 'fxa-shared/guards';
import log from '../logging';

const guard = new AdminPanelGuard(config.get('guard.env'));

/** Client Config Defaults provided by env */
const defaultConfig: IClientConfig = {
env: config.get('env'),
guard,
user: {
group: guard.getBestGroup(config.get('user.group')),
email: config.get('user.email'),
Expand Down
8 changes: 7 additions & 1 deletion packages/fxa-admin-panel/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
import { mockConfigBuilder } from './lib/config';
import { AdminPanelGroup, guard } from 'fxa-shared/guards';
import {
AdminPanelEnv,
AdminPanelGroup,
AdminPanelGuard,
} from 'fxa-shared/guards';

it('renders without imploding', () => {
const guard = new AdminPanelGuard(AdminPanelEnv.Prod);
const config = mockConfigBuilder({
guard,
user: {
email: 'hello@mozilla.com',
group: guard.getGroup(AdminPanelGroup.SupportAgentProd),
Expand Down
44 changes: 24 additions & 20 deletions packages/fxa-admin-panel/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,42 @@

import { useState } from 'react';
import { UserContext } from './hooks/UserContext';
import { GuardContext } from './hooks/GuardContext';
import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom';
import AppLayout from './components/AppLayout';
import AccountSearch from './components/AccountSearch';
import Permissions from './components/Permissions';
import AdminLogs from './components/AdminLogs';
import SiteStatus from './components/SiteStatus';
import { IClientConfig, IUserInfo } from '../interfaces';
import { AdminPanelFeature, guard } from 'fxa-shared/guards';
import { AdminPanelFeature, AdminPanelGuard } from 'fxa-shared/guards';

const App = ({ config }: { config: IClientConfig }) => {
const [guard, setGuard] = useState<AdminPanelGuard>(config.guard);
const [user, setUser] = useState<IUserInfo>(config.user);
return (
<BrowserRouter>
<UserContext.Provider value={{ user, setUser }}>
<AppLayout>
<Routes>
{guard.allow(AdminPanelFeature.AccountLogs, user.group) && (
<Route path="/admin-logs" element={<AdminLogs />} />
)}
{guard.allow(AdminPanelFeature.SiteStatus, user.group) && (
<Route path="/site-status" element={<SiteStatus />} />
)}
{guard.allow(AdminPanelFeature.AccountSearch, user.group) && (
<Route path="/account-search" element={<AccountSearch />} />
)}
{guard.allow(AdminPanelFeature.AccountSearch, user.group) && (
<Route path="/" element={<Navigate to="/account-search" />} />
)}
<Route path="/permissions" element={<Permissions />} />
</Routes>
</AppLayout>
</UserContext.Provider>
<GuardContext.Provider value={{ guard, setGuard }}>
<UserContext.Provider value={{ user, setUser }}>
<AppLayout>
<Routes>
{guard.allow(AdminPanelFeature.AccountLogs, user.group) && (
<Route path="/admin-logs" element={<AdminLogs />} />
)}
{guard.allow(AdminPanelFeature.SiteStatus, user.group) && (
<Route path="/site-status" element={<SiteStatus />} />
)}
{guard.allow(AdminPanelFeature.AccountSearch, user.group) && (
<Route path="/account-search" element={<AccountSearch />} />
)}
{guard.allow(AdminPanelFeature.AccountSearch, user.group) && (
<Route path="/" element={<Navigate to="/account-search" />} />
)}
<Route path="/permissions" element={<Permissions />} />
</Routes>
</AppLayout>
</UserContext.Provider>
</GuardContext.Provider>
</BrowserRouter>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,28 @@
import { render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { Account, AccountProps } from './index';
import { AdminPanelGroup, guard } from 'fxa-shared/guards';
import {
AdminPanelEnv,
AdminPanelGroup,
AdminPanelGuard,
} from 'fxa-shared/guards';
import { IClientConfig } from '../../../../interfaces';
import { mockConfigBuilder } from '../../../lib/config';

const mockGuard = new AdminPanelGuard(AdminPanelEnv.Prod);
const mockGroup = mockGuard.getGroup(AdminPanelGroup.SupportAgentProd);

export const mockConfig: IClientConfig = mockConfigBuilder({
user: {
email: 'test@mozilla.com',
group: guard.getGroup(AdminPanelGroup.SupportAgentProd),
group: mockGroup,
},
});

jest.mock('../../../hooks/UserContext.ts', () => ({
useUserContext: () => {
const ctx = {
guard: mockGuard,
user: mockConfig.user,
setUser: () => {},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ it('renders each field as expected', () => {

screen.getByText(subscription.productName);
screen.getByText(subscription.status);
expect(screen.getAllByText('1970-01-19 @', { exact: false })).toHaveLength(4);

// The date is rendered based on user local time. So depending on the user's clock
// the date could land on the 18th or the 19th.
expect(screen.getAllByText(/1970-01-1[89] @/, { exact: false })).toHaveLength(
4
);
screen.getByText('No');

screen.getByText(subscription.subscriptionId);
Expand Down
13 changes: 11 additions & 2 deletions packages/fxa-admin-panel/src/components/Guard/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,28 @@ import React from 'react';
import { render } from '@testing-library/react';
import { IClientConfig } from '../../../interfaces';
import { Guard } from './index';
import { AdminPanelFeature, AdminPanelGroup, guard } from 'fxa-shared/guards';
import {
AdminPanelEnv,
AdminPanelFeature,
AdminPanelGroup,
AdminPanelGuard,
} from 'fxa-shared/guards';
import { mockConfigBuilder } from '../../lib/config';

const mockGuard = new AdminPanelGuard(AdminPanelEnv.Prod);
const mockGroup = mockGuard.getGroup(AdminPanelGroup.SupportAgentProd);

export const mockConfig: IClientConfig = mockConfigBuilder({
user: {
email: 'test@mozilla.com',
group: guard.getGroup(AdminPanelGroup.SupportAgentProd),
group: mockGroup,
},
});

jest.mock('../../hooks/UserContext.ts', () => ({
useUserContext: () => {
const ctx = {
guard: mockGuard,
user: mockConfig.user,
setUser: () => {},
};
Expand Down
5 changes: 4 additions & 1 deletion packages/fxa-admin-panel/src/components/Guard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@

import React from 'react';
import { useUserContext } from '../../hooks/UserContext';
import { AdminPanelFeature, guard } from 'fxa-shared/guards';
import { AdminPanelFeature } from 'fxa-shared/guards';
import { useGuardContext } from '../../hooks/GuardContext';

export type GuardProps = {
features: AdminPanelFeature[];
};

export const Guard: React.FC<GuardProps> = ({ features, children }) => {
const { user } = useUserContext();
const { guard } = useGuardContext();

return features.some((x) => guard.allow(x, user.group)) ? (
<>{children}</>
) : (
Expand Down
16 changes: 12 additions & 4 deletions packages/fxa-admin-panel/src/components/Permissions/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@ import React from 'react';
import { render, RenderResult } from '@testing-library/react';
import { IClientConfig } from '../../../interfaces';
import { Permissions } from './index';
import { AdminPanelGroup, guard } from 'fxa-shared/guards';
import {
AdminPanelEnv,
AdminPanelGroup,
AdminPanelGuard,
} from 'fxa-shared/guards';
import { mockConfigBuilder } from '../../lib/config';

const mockGuard = new AdminPanelGuard(AdminPanelEnv.Prod);
const mockGroup = mockGuard.getGroup(AdminPanelGroup.SupportAgentProd);

export const mockConfig: IClientConfig = mockConfigBuilder({
user: {
email: 'test@mozilla.com',
group: guard.getGroup(AdminPanelGroup.SupportAgentProd),
group: mockGroup,
},
});

jest.mock('../../hooks/UserContext.ts', () => ({
useUserContext: () => {
const ctx = {
guard: mockGuard,
user: mockConfig.user,
setUser: () => {},
};
Expand Down Expand Up @@ -50,7 +58,7 @@ describe('Permissions', () => {
});

it('has enabled feature', () => {
const enabledFeature = guard
const enabledFeature = mockGuard
.getFeatureFlags(mockConfig.user.group)
.find((x) => x.enabled);

Expand All @@ -64,7 +72,7 @@ describe('Permissions', () => {
});

it('has disabled feature', () => {
const disabledFeature = guard
const disabledFeature = mockGuard
.getFeatureFlags(mockConfig.user.group)
.find((x) => !x.enabled);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import React from 'react';
import { guard, IFeatureFlag } from 'fxa-shared/guards';
import { IFeatureFlag } from 'fxa-shared/guards';
import { useUserContext } from '../../hooks/UserContext';
import { useGuardContext } from '../../hooks/GuardContext';

const styleClasses = {
label: 'px-4 py-2',
Expand Down Expand Up @@ -69,6 +70,8 @@ export const PermissionsTable = ({

export const Permissions = () => {
const { user } = useUserContext();
const { guard } = useGuardContext();

const featureFlags: IFeatureFlag[] = guard.getFeatureFlags(user.group);

return (
Expand Down
21 changes: 21 additions & 0 deletions packages/fxa-admin-panel/src/hooks/GuardContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* 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 { createContext, useContext } from 'react';
import { AdminPanelGuard } from 'fxa-shared/guards';

export interface IGuardContext {
guard: AdminPanelGuard;
setGuard: (guard: AdminPanelGuard) => void;
}

let _guard = new AdminPanelGuard();
export const GuardContext = createContext<IGuardContext>({
guard: _guard,
setGuard: (guard: AdminPanelGuard) => {
_guard = guard;
},
});

export const useGuardContext = () => useContext(GuardContext);
Loading

0 comments on commit 9c14190

Please sign in to comment.