From 4d2c64ebb459c799a3e8fed43711439fcc3f7d76 Mon Sep 17 00:00:00 2001 From: nixocio Date: Thu, 20 May 2021 15:54:55 -0400 Subject: [PATCH] Add RBAC rules to the side-nav Add RBAC rules to the side-nav System Admin System Auditor Org Admin Notification Admin Execution Environment Admin Normal User Those are the user profiles taken in consideration when displaying the side-nav. See: https://github.com/ansible/awx/issues/4426 --- awx/ui_next/src/App.jsx | 21 +- .../AppContainer/NavExpandableGroup.jsx | 2 +- awx/ui_next/src/contexts/Config.jsx | 40 ++- awx/ui_next/src/routeConfig.jsx | 27 +- awx/ui_next/src/routeConfig.test.jsx | 248 ++++++++++++++++++ 5 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 awx/ui_next/src/routeConfig.test.jsx diff --git a/awx/ui_next/src/App.jsx b/awx/ui_next/src/App.jsx index 88c91406c0..59a8e28cd9 100644 --- a/awx/ui_next/src/App.jsx +++ b/awx/ui_next/src/App.jsx @@ -12,7 +12,11 @@ import { ErrorBoundary } from 'react-error-boundary'; import { I18nProvider } from '@lingui/react'; import { i18n } from '@lingui/core'; import { Card, PageSection } from '@patternfly/react-core'; -import { ConfigProvider, useAuthorizedPath } from './contexts/Config'; +import { + ConfigProvider, + useAuthorizedPath, + useUserProfile, +} from './contexts/Config'; import { SessionProvider, useSession } from './contexts/Session'; import AppContainer from './components/AppContainer'; import Background from './components/Background'; @@ -38,6 +42,17 @@ function ErrorFallback({ error }) { ); } +const RenderAppContainer = () => { + const userProfile = useUserProfile(); + const navRouteConfig = getRouteConfig(userProfile); + + return ( + + + + ); +}; + const AuthorizedRoutes = ({ routeConfig }) => { const isAuthorized = useAuthorizedPath(); const match = useRouteMatch(); @@ -150,9 +165,7 @@ function App() { - - - + diff --git a/awx/ui_next/src/components/AppContainer/NavExpandableGroup.jsx b/awx/ui_next/src/components/AppContainer/NavExpandableGroup.jsx index 80d7c2dc2c..bfd5636b33 100644 --- a/awx/ui_next/src/components/AppContainer/NavExpandableGroup.jsx +++ b/awx/ui_next/src/components/AppContainer/NavExpandableGroup.jsx @@ -27,7 +27,7 @@ class NavExpandableGroup extends Component { render() { const { groupId, groupTitle, routes } = this.props; - if (routes.length === 1) { + if (routes.length === 1 && groupId === 'settings') { const [{ path }] = routes; return ( diff --git a/awx/ui_next/src/contexts/Config.jsx b/awx/ui_next/src/contexts/Config.jsx index e87c2a24f2..5580bd9192 100644 --- a/awx/ui_next/src/contexts/Config.jsx +++ b/awx/ui_next/src/contexts/Config.jsx @@ -3,7 +3,7 @@ import { useRouteMatch } from 'react-router-dom'; import { t } from '@lingui/macro'; -import { ConfigAPI, MeAPI } from '../api'; +import { ConfigAPI, MeAPI, UsersAPI, OrganizationsAPI } from '../api'; import useRequest, { useDismissableError } from '../util/useRequest'; import AlertModal from '../components/AlertModal'; import ErrorDetail from '../components/ErrorDetail'; @@ -35,9 +35,32 @@ export const ConfigProvider = ({ children }) => { }, }, ] = await Promise.all([ConfigAPI.read(), MeAPI.read()]); - return { ...data, me }; + + const [ + { + data: { count: adminOrgCount }, + }, + { + data: { count: notifAdminCount }, + }, + { + data: { count: execEnvAdminCount }, + }, + ] = await Promise.all([ + UsersAPI.readAdminOfOrganizations(me?.id), + OrganizationsAPI.read({ + page_size: 1, + role_level: 'notification_admin_role', + }), + OrganizationsAPI.read({ + page_size: 1, + role_level: 'execution_environment_admin_role', + }), + ]); + + return { ...data, me, adminOrgCount, notifAdminCount, execEnvAdminCount }; }, []), - {} + { adminOrgCount: 0, notifAdminCount: 0, execEnvAdminCount: 0 } ); const { error, dismissError } = useDismissableError(configError); @@ -77,6 +100,17 @@ export const ConfigProvider = ({ children }) => { ); }; +export const useUserProfile = () => { + const config = useConfig(); + return { + isSuperUser: !!config.me?.is_superuser, + isSystemAuditor: !!config.me?.is_system_auditor, + isOrgAdmin: config.adminOrgCount, + isNotificationAdmin: config.notifAdminCount, + isExecEnvAdmin: config.execEnvAdminCount, + }; +}; + export const useAuthorizedPath = () => { const config = useConfig(); const subscriptionMgmtRoute = useRouteMatch({ diff --git a/awx/ui_next/src/routeConfig.jsx b/awx/ui_next/src/routeConfig.jsx index fba640a2d8..5d29072fcd 100644 --- a/awx/ui_next/src/routeConfig.jsx +++ b/awx/ui_next/src/routeConfig.jsx @@ -22,8 +22,8 @@ import Users from './screens/User'; import WorkflowApprovals from './screens/WorkflowApproval'; import { Jobs } from './screens/Job'; -function getRouteConfig() { - return [ +function getRouteConfig(userProfile = {}) { + let routeConfig = [ { groupTitle: Views, groupId: 'views_group', @@ -155,6 +155,29 @@ function getRouteConfig() { ], }, ]; + + const deleteRoute = name => { + routeConfig.forEach(group => { + group.routes = group.routes.filter(({ path }) => !path.includes(name)); + }); + routeConfig = routeConfig.filter(groups => groups.routes.length); + }; + + const deleteRouteGroup = name => { + routeConfig = routeConfig.filter(({ groupId }) => !groupId.includes(name)); + }; + + if (userProfile?.isSuperUser || userProfile?.isSystemAuditor) + return routeConfig; + deleteRouteGroup('settings'); + deleteRoute('management_jobs'); + deleteRoute('credential_types'); + if (userProfile?.isOrgAdmin) return routeConfig; + deleteRoute('applications'); + deleteRoute('instance_groups'); + if (!userProfile?.isNotificationAdmin) deleteRoute('notification_templates'); + + return routeConfig; } export default getRouteConfig; diff --git a/awx/ui_next/src/routeConfig.test.jsx b/awx/ui_next/src/routeConfig.test.jsx new file mode 100644 index 0000000000..b4382bbcd0 --- /dev/null +++ b/awx/ui_next/src/routeConfig.test.jsx @@ -0,0 +1,248 @@ +import getRouteConfig from './routeConfig'; + +const userProfile = { + isSuperUser: false, + isSystemAuditor: false, + isOrgAdmin: false, + isNotificationAdmin: false, + isExecEnvAdmin: false, +}; + +const filterPaths = sidebar => { + const visibleRoutes = []; + sidebar.forEach(({ routes }) => { + routes.forEach(route => { + visibleRoutes.push(route.path); + }); + }); + + return visibleRoutes; +}; +describe('getRouteConfig', () => { + test('routes for system admin', () => { + const sidebar = getRouteConfig({ ...userProfile, isSuperUser: true }); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/credential_types', + '/notification_templates', + '/management_jobs', + '/instance_groups', + '/applications', + '/execution_environments', + '/settings', + ]); + }); + + test('routes for system auditor', () => { + const sidebar = getRouteConfig({ ...userProfile, isSystemAuditor: true }); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/credential_types', + '/notification_templates', + '/management_jobs', + '/instance_groups', + '/applications', + '/execution_environments', + '/settings', + ]); + }); + + test('routes for org admin', () => { + const sidebar = getRouteConfig({ ...userProfile, isOrgAdmin: true }); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/notification_templates', + '/instance_groups', + '/applications', + '/execution_environments', + ]); + }); + + test('routes for notifications admin', () => { + const sidebar = getRouteConfig({ + ...userProfile, + isNotificationAdmin: true, + }); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/notification_templates', + '/execution_environments', + ]); + }); + + test('routes for execution environments admin', () => { + const sidebar = getRouteConfig({ ...userProfile, isExecEnvAdmin: true }); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/execution_environments', + ]); + }); + + test('routes for regular users', () => { + const sidebar = getRouteConfig(userProfile); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/execution_environments', + ]); + }); + + test('routes for execution environment admins and notification admin', () => { + const sidebar = getRouteConfig({ + ...userProfile, + isExecEnvAdmin: true, + isNotificationAdmin: true, + }); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/notification_templates', + '/execution_environments', + ]); + }); + + test('routes for execution environment admins and organization admins', () => { + const sidebar = getRouteConfig({ + ...userProfile, + isExecEnvAdmin: true, + isOrgAdmin: true, + }); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/notification_templates', + '/instance_groups', + '/applications', + '/execution_environments', + ]); + }); + + test('routes for notification admins and organization admins', () => { + const sidebar = getRouteConfig({ + ...userProfile, + isNotificationAdmin: true, + isOrgAdmin: true, + }); + const filteredPaths = filterPaths(sidebar); + expect(filteredPaths).toEqual([ + '/home', + '/jobs', + '/schedules', + '/activity_stream', + '/workflow_approvals', + '/templates', + '/credentials', + '/projects', + '/inventories', + '/hosts', + '/organizations', + '/users', + '/teams', + '/notification_templates', + '/instance_groups', + '/applications', + '/execution_environments', + ]); + }); +});