From fe177c6ab3d696e3bc986b57410510867ede9dbf Mon Sep 17 00:00:00 2001
From: jtimpe <111305129+jtimpe@users.noreply.github.com>
Date: Tue, 22 Nov 2022 15:02:36 -0500
Subject: [PATCH] Fix/1744 permission guards (#2258)
* fix role selector
* permission guard component to conditionally render based on redux state
* use permission guard for data file page
* use permission guard for data file nav item
* add component tests for data file permission guard
* require account approval to access data files
* update ci?
* fix ci img
* ci img
* rm old heroku keys workaround
Co-authored-by: Andrew <84722778+andrew-jameson@users.noreply.github.com>
---
.../src/components/Header/Header.jsx | 15 +-
.../src/components/Header/Header.test.js | 74 +++++++
.../PermissionGuard/PermissionGuard.jsx | 47 +++++
.../PermissionGuard/PermissionGuard.test.js | 194 ++++++++++++++++++
.../src/components/PermissionGuard/index.js | 3 +
.../components/PrivateRoute/PrivateRoute.js | 33 ++-
.../PrivateRoute/PrivateRoute.test.js | 28 ++-
tdrs-frontend/src/components/Routes/Routes.js | 6 +-
tdrs-frontend/src/selectors/auth.js | 4 +-
9 files changed, 381 insertions(+), 23 deletions(-)
create mode 100644 tdrs-frontend/src/components/PermissionGuard/PermissionGuard.jsx
create mode 100644 tdrs-frontend/src/components/PermissionGuard/PermissionGuard.test.js
create mode 100644 tdrs-frontend/src/components/PermissionGuard/index.js
diff --git a/tdrs-frontend/src/components/Header/Header.jsx b/tdrs-frontend/src/components/Header/Header.jsx
index c97646e2f..60158504d 100644
--- a/tdrs-frontend/src/components/Header/Header.jsx
+++ b/tdrs-frontend/src/components/Header/Header.jsx
@@ -10,6 +10,7 @@ import {
} from '../../selectors/auth'
import NavItem from '../NavItem/NavItem'
+import PermissionGuard from '../PermissionGuard'
/**
* This component is rendered on every page and contains the navigation bar.
@@ -28,13 +29,6 @@ function Header() {
const userAccessRequestPending = useSelector(accountIsInReview)
const userAccessRequestApproved = useSelector(accountStatusIsApproved)
- const hasPermission = (permissionName) =>
- user?.roles?.[0]?.permissions?.some(
- (perm) => perm.codename === permissionName
- )
-
- const canViewDataFiles = hasPermission('view_datafile')
-
const menuRef = useRef()
const keyListenersMap = useMemo(() => {
@@ -118,13 +112,16 @@ function Header() {
{authenticated && (
<>
hello, world
+ + + ) + + describe('not allowed', () => { + it('shows not allowed if missing one permission', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: [{ codename: 'some_stuff' }] }], + }, + }, + }, + ['allowed'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if missing multiple permissions', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: [{ codename: 'allowed' }] }], + }, + }, + }, + ['allowed', 'super_allowed', 'super_duper_allowed'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if multiple roles missing permissions', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [ + { permissions: [{ codename: 'allowed' }] }, + { permissions: [{ codename: 'super_allowed' }] }, + { permissions: [{ codename: 'nothing' }] }, + ], + }, + }, + }, + ['allowed', 'super_allowed', 'super_duper_allowed'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if user has no roles', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: null, + }, + }, + }, + ['anything'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if user has no permissions', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: [] }], + }, + }, + }, + ['anything'] + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + + it('shows not allowed if requiresApproval and not approved', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: ['anything'] }], + account_approval_status: 'Pending', + }, + }, + }, + [], + true + ) + + expect(screen.queryByText('hello, world')).not.toBeInTheDocument() + expect(screen.queryByText('not allowed')).toBeInTheDocument() + }) + }) + + describe('allowed', () => { + it('shows allowed if no required permissions given', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: null, + }, + }, + }, + null + ) + + expect(screen.queryByText('hello, world')).toBeInTheDocument() + expect(screen.queryByText('not allowed')).not.toBeInTheDocument() + }) + + it('shows allowed if user matches all permissions', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [ + { permissions: [{ codename: 'allowed' }] }, + { permissions: [{ codename: 'super_allowed' }] }, + { permissions: [{ codename: 'super_duper_allowed' }] }, + ], + }, + }, + }, + ['allowed', 'super_allowed', 'super_duper_allowed'] + ) + + expect(screen.queryByText('hello, world')).toBeInTheDocument() + expect(screen.queryByText('not allowed')).not.toBeInTheDocument() + }) + + it('shows allowed if requiresApproval and approved', () => { + setup( + { + auth: { + authenticated: true, + user: { + roles: [{ permissions: ['anything'] }], + account_approval_status: 'Approved', + }, + }, + }, + [], + true + ) + + expect(screen.queryByText('hello, world')).toBeInTheDocument() + expect(screen.queryByText('not allowed')).not.toBeInTheDocument() + }) + }) +}) diff --git a/tdrs-frontend/src/components/PermissionGuard/index.js b/tdrs-frontend/src/components/PermissionGuard/index.js new file mode 100644 index 000000000..9d2b9f3a8 --- /dev/null +++ b/tdrs-frontend/src/components/PermissionGuard/index.js @@ -0,0 +1,3 @@ +import PermissionGuard from './PermissionGuard' + +export default PermissionGuard diff --git a/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js b/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js index af1f7ef30..33df4eb64 100644 --- a/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js +++ b/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js @@ -1,10 +1,11 @@ import React, { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' +import { Navigate, useNavigate } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' import { setAlert, clearAlert } from '../../actions/alert' import { ALERT_INFO } from '../Alert' import PrivateTemplate from '../PrivateTemplate' import IdleTimer from '../IdleTimer/IdleTimer' +import PermissionGuard from '../PermissionGuard' /** * @param {React.ReactNode} children - One or more React components to be @@ -12,9 +13,15 @@ import IdleTimer from '../IdleTimer/IdleTimer' * @param {string} title Page title passed in to PrivateTemplate * which is automatically passed via withRouter */ -function PrivateRoute({ children, title }) { +function PrivateRoute({ + children, + title, + requiredPermissions, + requiresApproval, +}) { const authenticated = useSelector((state) => state.auth.authenticated) const authLoading = useSelector((state) => state.auth.loading) + const navigate = useNavigate() const dispatch = useDispatch() @@ -33,12 +40,22 @@ function PrivateRoute({ children, title }) { } }, [authenticated, authLoading, dispatch, navigate]) - return authenticated ? ( -Hello Private Content