From a58b9b3ef65de7187cffb5f879e9088b3e1669ba Mon Sep 17 00:00:00 2001
From: Alexander Lesnenko <alxnddr@users.noreply.github.com>
Date: Mon, 4 Apr 2022 20:33:20 -0400
Subject: [PATCH] FE: Monitoring permission (#21407)

* monitoring permission

* [skip ci] fix types

* fix import order

* specs

* fix spec

* review
---
 .../metabase-enterprise/audit_app/index.js    |   6 +-
 .../metabase-enterprise/audit_app/routes.jsx  |   8 +-
 .../feature_level_permissions/index.ts        |  12 +-
 .../feature_level_permissions/types/user.ts   |   8 +
 .../feature_level_permissions/utils.ts        |  14 +-
 .../general_permissions/index.ts              |   6 +
 .../general_permissions/selectors.ts          |  31 +++-
 .../general_permissions/types/permissions.ts  |   2 +-
 .../general_permissions/utils.ts              |   4 +
 .../src/metabase-enterprise/tools/index.js    |   6 +-
 .../src/metabase-enterprise/tools/routes.jsx  |   7 +-
 frontend/src/metabase-types/api/mocks/user.ts |   2 -
 frontend/src/metabase-types/api/user.ts       |   2 -
 frontend/src/metabase/admin/routes.jsx        |  20 +-
 .../containers/RedirectToAllowedSettings.jsx  |  16 +-
 frontend/src/metabase/admin/utils.js          |  16 ++
 .../components/AdminNavbar/AdminNavbar.tsx    |  75 +-------
 .../metabase/nav/components/ProfileLink.jsx   |   7 +-
 .../metabase/nav/components/RecentsList.jsx   |   2 +-
 .../src/metabase/nav/utils/admin-paths.ts     |  70 +++++++
 frontend/src/metabase/nav/utils/index.js      |   2 +
 .../utils.js => utils/model-names.ts}         |   4 +-
 frontend/src/metabase/plugins/index.ts        |   3 -
 frontend/src/metabase/routes.jsx              |  41 +----
 .../metabase/search/components/InfoText.jsx   |   2 +-
 .../general-permissions.cy.spec.js            | 172 +++++++++++++-----
 26 files changed, 322 insertions(+), 216 deletions(-)
 create mode 100644 enterprise/frontend/src/metabase-enterprise/feature_level_permissions/types/user.ts
 create mode 100644 enterprise/frontend/src/metabase-enterprise/general_permissions/utils.ts
 create mode 100644 frontend/src/metabase/admin/utils.js
 create mode 100644 frontend/src/metabase/nav/utils/admin-paths.ts
 create mode 100644 frontend/src/metabase/nav/utils/index.js
 rename frontend/src/metabase/nav/{components/utils.js => utils/model-names.ts} (70%)

diff --git a/enterprise/frontend/src/metabase-enterprise/audit_app/index.js b/enterprise/frontend/src/metabase-enterprise/audit_app/index.js
index 77427810109..e7cf09827e4 100644
--- a/enterprise/frontend/src/metabase-enterprise/audit_app/index.js
+++ b/enterprise/frontend/src/metabase-enterprise/audit_app/index.js
@@ -9,7 +9,11 @@ import { hasPremiumFeature } from "metabase-enterprise/settings";
 import getAuditRoutes, { getUserMenuRotes } from "./routes";
 
 if (hasPremiumFeature("audit_app")) {
-  PLUGIN_ADMIN_NAV_ITEMS.push({ name: t`Audit`, path: "/admin/audit" });
+  PLUGIN_ADMIN_NAV_ITEMS.push({
+    name: t`Audit`,
+    path: "/admin/audit",
+    key: "audit",
+  });
   PLUGIN_ADMIN_ROUTES.push(getAuditRoutes);
 
   PLUGIN_ADMIN_USER_MENU_ITEMS.push(user => [
diff --git a/enterprise/frontend/src/metabase-enterprise/audit_app/routes.jsx b/enterprise/frontend/src/metabase-enterprise/audit_app/routes.jsx
index 1e38972da32..950e5f867d7 100644
--- a/enterprise/frontend/src/metabase-enterprise/audit_app/routes.jsx
+++ b/enterprise/frontend/src/metabase-enterprise/audit_app/routes.jsx
@@ -2,6 +2,7 @@ import React from "react";
 
 import { Route } from "metabase/hoc/Title";
 import { ModalRoute } from "metabase/hoc/ModalRoute";
+import { createAdminRouteGuard } from "metabase/admin/utils";
 import { IndexRoute, IndexRedirect } from "react-router";
 import { t } from "ttag";
 import _ from "underscore";
@@ -73,7 +74,12 @@ function getDefaultTab(page) {
 }
 
 const getRoutes = store => (
-  <Route key="audit" path="audit" title={t`Audit`} component={AuditApp}>
+  <Route
+    key="audit"
+    path="audit"
+    title={t`Audit`}
+    component={createAdminRouteGuard("audit", AuditApp)}
+  >
     {/* <IndexRedirect to="overview" /> */}
     <IndexRedirect to="members" />
 
diff --git a/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/index.ts b/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/index.ts
index 0869a4c7e1b..f7eca056046 100644
--- a/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/index.ts
+++ b/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/index.ts
@@ -1,10 +1,6 @@
 import { hasPremiumFeature } from "metabase-enterprise/settings";
 import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins";
-import {
-  canAccessSettings,
-  canAccessDataModel,
-  canAccessDatabaseManagement,
-} from "./utils";
+import { canAccessDataModel, canAccessDatabaseManagement } from "./utils";
 
 import { getFeatureLevelDataPermissions } from "./permissions";
 import { DATA_COLUMNS } from "./constants";
@@ -12,11 +8,11 @@ import {
   canDownloadResults,
   getDownloadWidgetMessageOverride,
 } from "./query-downloads";
+import { NAV_PERMISSION_GUARD } from "metabase/nav/utils";
 
 if (hasPremiumFeature("advanced_permissions")) {
-  PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessSettings = canAccessSettings;
-  PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessDataModel = canAccessDataModel;
-  PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessDatabaseManagement = canAccessDatabaseManagement;
+  NAV_PERMISSION_GUARD["data-model"] = canAccessDataModel;
+  NAV_PERMISSION_GUARD["databases"] = canAccessDatabaseManagement;
 
   PLUGIN_FEATURE_LEVEL_PERMISSIONS.getFeatureLevelDataPermissions = getFeatureLevelDataPermissions;
   PLUGIN_FEATURE_LEVEL_PERMISSIONS.dataColumns = DATA_COLUMNS;
diff --git a/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/types/user.ts b/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/types/user.ts
new file mode 100644
index 00000000000..55afc519867
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/types/user.ts
@@ -0,0 +1,8 @@
+import { User } from "metabase-types/api";
+
+export interface UserWithFeaturePermissions extends User {
+  permissions?: {
+    can_access_data_model: boolean;
+    can_access_database_management: boolean;
+  };
+}
diff --git a/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/utils.ts b/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/utils.ts
index 7f9fe00db54..99b5e103bb7 100644
--- a/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/utils.ts
+++ b/enterprise/frontend/src/metabase-enterprise/feature_level_permissions/utils.ts
@@ -1,10 +1,8 @@
-import { User } from "metabase-types/api";
+import { UserWithFeaturePermissions } from "./types/user";
 
-export const canAccessSettings = (user: User) =>
-  canAccessDataModel(user) || canAccessDatabaseManagement(user);
+export const canAccessDataModel = (user?: UserWithFeaturePermissions) =>
+  user?.permissions?.can_access_data_model ?? false;
 
-export const canAccessDataModel = (user?: User) =>
-  user?.can_access_data_model ?? false;
-
-export const canAccessDatabaseManagement = (user?: User) =>
-  user?.can_access_database_management ?? false;
+export const canAccessDatabaseManagement = (
+  user?: UserWithFeaturePermissions,
+) => user?.permissions?.can_access_database_management ?? false;
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/index.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/index.ts
index 31d77608753..6442604c2e0 100644
--- a/enterprise/frontend/src/metabase-enterprise/general_permissions/index.ts
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/index.ts
@@ -5,8 +5,14 @@ import getRoutes from "./routes";
 import { t } from "ttag";
 import { canManageSubscriptions } from "./selectors";
 import generalPermissionsReducer from "./reducer";
+import { NAV_PERMISSION_GUARD } from "metabase/nav/utils";
+import { canAccessMonitoringItems } from "./utils";
 
 if (hasPremiumFeature("advanced_permissions")) {
+  NAV_PERMISSION_GUARD["audit"] = canAccessMonitoringItems as any;
+  NAV_PERMISSION_GUARD["tools"] = canAccessMonitoringItems as any;
+  NAV_PERMISSION_GUARD["troubleshooting"] = canAccessMonitoringItems as any;
+
   PLUGIN_GENERAL_PERMISSIONS.getRoutes = getRoutes;
   PLUGIN_GENERAL_PERMISSIONS.tabs = [{ name: t`General`, value: `general` }];
   PLUGIN_GENERAL_PERMISSIONS.selectors = {
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/selectors.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/selectors.ts
index 393041b10c1..687523e6567 100644
--- a/enterprise/frontend/src/metabase-enterprise/general_permissions/selectors.ts
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/selectors.ts
@@ -8,10 +8,20 @@ import { getOrderedGroups } from "metabase/admin/permissions/selectors/data-perm
 import { GENERAL_PERMISSIONS_OPTIONS } from "./constants";
 import { getIn } from "icepick";
 import { GeneralPermissionsState } from "./types/state";
+import { GeneralPermissionKey, GeneralPermissions } from "./types/permissions";
 
 export const canManageSubscriptions = (state: GeneralPermissionsState) =>
   state.currentUser.permissions.can_access_subscription;
 
+export const canAccessMonitoring = (state: GeneralPermissionsState) =>
+  state.currentUser.permissions.can_access_monitoring;
+
+const getGeneralPermission = (
+  permissions: GeneralPermissions,
+  groupId: number,
+  permissionKey: GeneralPermissionKey,
+) => getIn(permissions, [groupId, permissionKey]) ?? "no";
+
 export const getIsDirty = createSelector(
   (state: GeneralPermissionsState) =>
     state.plugins.generalPermissionsPlugin?.generalPermissions,
@@ -32,9 +42,6 @@ export const getGeneralPermissionEditor = createSelector(
     const entities = groups.flat().map(group => {
       const isAdmin = isAdminGroup(group);
 
-      const subscriptionValue =
-        getIn(permissions, [group.id, "subscription"]) ?? "no";
-
       return {
         id: group.id,
         name: group.name,
@@ -45,7 +52,19 @@ export const getGeneralPermissionEditor = createSelector(
             disabledTooltip: isAdmin
               ? UNABLE_TO_CHANGE_ADMIN_PERMISSIONS
               : null,
-            value: subscriptionValue,
+            value: getGeneralPermission(permissions, group.id, "subscription"),
+            options: [
+              GENERAL_PERMISSIONS_OPTIONS.yes,
+              GENERAL_PERMISSIONS_OPTIONS.no,
+            ],
+          },
+          {
+            permission: "monitoring",
+            isDisabled: isAdmin,
+            disabledTooltip: isAdmin
+              ? UNABLE_TO_CHANGE_ADMIN_PERMISSIONS
+              : null,
+            value: getGeneralPermission(permissions, group.id, "monitoring"),
             options: [
               GENERAL_PERMISSIONS_OPTIONS.yes,
               GENERAL_PERMISSIONS_OPTIONS.no,
@@ -60,6 +79,10 @@ export const getGeneralPermissionEditor = createSelector(
       columns: [
         { name: `General settings access` },
         { name: `Subscriptions and Alerts` },
+        {
+          name: `Monitoring access`,
+          hint: t`This grants access to Tools, Audit, and Troubleshooting`,
+        },
       ],
       entities,
     };
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/types/permissions.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/types/permissions.ts
index 7a7fa218330..0ec9be60311 100644
--- a/enterprise/frontend/src/metabase-enterprise/general_permissions/types/permissions.ts
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/types/permissions.ts
@@ -1,6 +1,6 @@
 import { GroupId } from "metabase-types/api";
 
-export type GeneralPermissionKey = "subscription";
+export type GeneralPermissionKey = "subscription" | "monitoring";
 export type GeneralPermissionValue = "yes" | "no";
 
 export type GroupGeneralPermissions = {
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/utils.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/utils.ts
new file mode 100644
index 00000000000..19ad1fd8a88
--- /dev/null
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/utils.ts
@@ -0,0 +1,4 @@
+import { UserWithPermissions } from "./types/user";
+
+export const canAccessMonitoringItems = (user?: UserWithPermissions) =>
+  user?.permissions.can_access_monitoring ?? false;
diff --git a/enterprise/frontend/src/metabase-enterprise/tools/index.js b/enterprise/frontend/src/metabase-enterprise/tools/index.js
index 4220bb43792..765c23ab787 100644
--- a/enterprise/frontend/src/metabase-enterprise/tools/index.js
+++ b/enterprise/frontend/src/metabase-enterprise/tools/index.js
@@ -5,6 +5,10 @@ import { hasPremiumFeature } from "metabase-enterprise/settings";
 import getToolsRoutes from "./routes";
 
 if (hasPremiumFeature("audit_app")) {
-  PLUGIN_ADMIN_NAV_ITEMS.push({ name: t`Tools`, path: "/admin/tools" });
+  PLUGIN_ADMIN_NAV_ITEMS.push({
+    name: t`Tools`,
+    path: "/admin/tools",
+    key: "tools",
+  });
   PLUGIN_ADMIN_ROUTES.push(getToolsRoutes);
 }
diff --git a/enterprise/frontend/src/metabase-enterprise/tools/routes.jsx b/enterprise/frontend/src/metabase-enterprise/tools/routes.jsx
index 73bb293b173..7e24557d1d7 100644
--- a/enterprise/frontend/src/metabase-enterprise/tools/routes.jsx
+++ b/enterprise/frontend/src/metabase-enterprise/tools/routes.jsx
@@ -6,9 +6,14 @@ import { t } from "ttag";
 import ToolsApp from "./containers/ToolsApp";
 import ErrorOverview from "./containers/ErrorOverview";
 import ErrorDetail from "./containers/ErrorDetail";
+import { createAdminRouteGuard } from "metabase/admin/utils";
 
 const getRoutes = store => (
-  <Route path="tools" title={t`Tools`} component={ToolsApp}>
+  <Route
+    path="tools"
+    title={t`Tools`}
+    component={createAdminRouteGuard("tools", ToolsApp)}
+  >
     <IndexRedirect to="errors" />
     <Route
       path="errors"
diff --git a/frontend/src/metabase-types/api/mocks/user.ts b/frontend/src/metabase-types/api/mocks/user.ts
index 259fe264ad3..6f024d06629 100644
--- a/frontend/src/metabase-types/api/mocks/user.ts
+++ b/frontend/src/metabase-types/api/mocks/user.ts
@@ -16,7 +16,5 @@ export const createMockUser = (opts?: Partial<User>): User => ({
   personal_collection_id: 1,
   date_joined: new Date().toISOString(),
   last_login: new Date().toISOString(),
-  can_access_data_model: false,
-  can_access_database_management: false,
   ...opts,
 });
diff --git a/frontend/src/metabase-types/api/user.ts b/frontend/src/metabase-types/api/user.ts
index 10621690922..718feb28649 100644
--- a/frontend/src/metabase-types/api/user.ts
+++ b/frontend/src/metabase-types/api/user.ts
@@ -14,6 +14,4 @@ export interface User {
   date_joined: string;
   last_login: string;
   personal_collection_id: number;
-  can_access_data_model: boolean;
-  can_access_database_management: boolean;
 }
diff --git a/frontend/src/metabase/admin/routes.jsx b/frontend/src/metabase/admin/routes.jsx
index a148731eac5..e451ae24af1 100644
--- a/frontend/src/metabase/admin/routes.jsx
+++ b/frontend/src/metabase/admin/routes.jsx
@@ -1,8 +1,8 @@
 import React from "react";
-import { Route } from "metabase/hoc/Title";
 import { IndexRoute, IndexRedirect } from "react-router";
 import { t } from "ttag";
 
+import { Route } from "metabase/hoc/Title";
 import {
   PLUGIN_ADMIN_ROUTES,
   PLUGIN_ADMIN_USER_MENU_ROUTES,
@@ -10,6 +10,7 @@ import {
 
 import { withBackground } from "metabase/hoc/Background";
 import { ModalRoute } from "metabase/hoc/ModalRoute";
+import { createAdminRouteGuard } from "metabase/admin/utils";
 
 import RedirectToAllowedSettings from "./settings/containers/RedirectToAllowedSettings";
 import AdminApp from "metabase/admin/app/components/AdminApp";
@@ -55,13 +56,7 @@ import GroupDetailApp from "metabase/admin/people/containers/GroupDetailApp";
 // Permissions
 import getAdminPermissionsRoutes from "metabase/admin/permissions/routes";
 
-const getRoutes = (
-  store,
-  CanAccessSettings,
-  IsAdmin,
-  CanAccessDataModel,
-  CanAccessDatabaseManagement,
-) => (
+const getRoutes = (store, CanAccessSettings, IsAdmin) => (
   <Route
     path="/admin"
     component={withBackground("bg-white")(CanAccessSettings)}
@@ -72,14 +67,14 @@ const getRoutes = (
       <Route
         path="databases"
         title={t`Databases`}
-        component={CanAccessDatabaseManagement}
+        component={createAdminRouteGuard("databases")}
       >
         <IndexRoute component={DatabaseListApp} />
         <Route path="create" component={DatabaseEditApp} />
         <Route path=":databaseId" component={DatabaseEditApp} />
       </Route>
 
-      <Route path="datamodel" component={CanAccessDataModel}>
+      <Route path="datamodel" component={createAdminRouteGuard("data-model")}>
         <Route title={t`Data Model`} component={DataModelApp}>
           <IndexRedirect to="database" />
           <Route path="database" component={MetadataEditorApp} />
@@ -137,7 +132,10 @@ const getRoutes = (
       </Route>
 
       {/* Troubleshooting */}
-      <Route path="troubleshooting" component={IsAdmin}>
+      <Route
+        path="troubleshooting"
+        component={createAdminRouteGuard("troubleshooting")}
+      >
         <Route title={t`Troubleshooting`} component={TroubleshootingApp}>
           <IndexRedirect to="help" />
           <Route path="help" component={Help} />
diff --git a/frontend/src/metabase/admin/settings/containers/RedirectToAllowedSettings.jsx b/frontend/src/metabase/admin/settings/containers/RedirectToAllowedSettings.jsx
index 5a26c04e692..22daceec3d0 100644
--- a/frontend/src/metabase/admin/settings/containers/RedirectToAllowedSettings.jsx
+++ b/frontend/src/metabase/admin/settings/containers/RedirectToAllowedSettings.jsx
@@ -1,7 +1,7 @@
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 import { getUser } from "metabase/selectors/user";
-import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins";
+import { getAllowedMenuItems } from "metabase/nav/utils";
 
 const mapStateToProps = (state, props) => ({
   user: getUser(state),
@@ -13,16 +13,12 @@ const mapDispatchToProps = {
 };
 
 const RedirectToAllowedSettings = ({ user, push }) => {
-  if (user.is_superuser) {
-    push("/admin/settings");
-  } else if (PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessDataModel(user)) {
-    push("/admin/datamodel");
-  } else if (
-    PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessDatabaseManagement(user)
-  ) {
-    push("/admin/databases");
-  } else if (user != null) {
+  const allowedNavItems = getAllowedMenuItems(user);
+
+  if (allowedNavItems.length === 0) {
     push("/unauthorized");
+  } else {
+    push(allowedNavItems[0].path);
   }
 
   return null;
diff --git a/frontend/src/metabase/admin/utils.js b/frontend/src/metabase/admin/utils.js
new file mode 100644
index 00000000000..e83208555a7
--- /dev/null
+++ b/frontend/src/metabase/admin/utils.js
@@ -0,0 +1,16 @@
+import { UserAuthWrapper } from "redux-auth-wrapper";
+import { routerActions } from "react-router-redux";
+import { canAccessPath } from "metabase/nav/utils";
+
+export const createAdminRouteGuard = (routeKey, Component) => {
+  const Wrapper = UserAuthWrapper({
+    predicate: currentUser => canAccessPath(routeKey, currentUser),
+    failureRedirectPath: "/unauthorized",
+    authSelector: state => state.currentUser,
+    allowRedirectBack: false,
+    wrapperDisplayName: `CanAccess(${routeKey})`,
+    redirectAction: routerActions.replace,
+  });
+
+  return Wrapper(Component ?? (({ children }) => children));
+};
diff --git a/frontend/src/metabase/nav/components/AdminNavbar/AdminNavbar.tsx b/frontend/src/metabase/nav/components/AdminNavbar/AdminNavbar.tsx
index 5bb7f643c5b..6d2ca4692e3 100644
--- a/frontend/src/metabase/nav/components/AdminNavbar/AdminNavbar.tsx
+++ b/frontend/src/metabase/nav/components/AdminNavbar/AdminNavbar.tsx
@@ -1,9 +1,5 @@
 import React from "react";
 import { t } from "ttag";
-import {
-  PLUGIN_ADMIN_NAV_ITEMS,
-  PLUGIN_FEATURE_LEVEL_PERMISSIONS,
-} from "metabase/plugins";
 import MetabaseSettings from "metabase/lib/settings";
 import { AdminNavItem } from "./AdminNavItem";
 import StoreLink from "../StoreLink";
@@ -17,6 +13,7 @@ import {
   AdminNavbarRoot,
 } from "./AdminNavbar.styled";
 import { User } from "metabase-types/api";
+import { getAllowedMenuItems } from "metabase/nav/utils";
 
 interface AdminNavbarProps {
   path: string;
@@ -24,12 +21,7 @@ interface AdminNavbarProps {
 }
 
 export const AdminNavbar = ({ path: currentPath, user }: AdminNavbarProps) => {
-  const isAdmin = user.is_superuser;
-  const canAccessDataModel =
-    isAdmin || PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessDataModel(user);
-  const canAccessDatabaseManagement =
-    isAdmin ||
-    PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessDatabaseManagement(user);
+  const allowedMenuItems = getAllowedMenuItems(user);
 
   return (
     <AdminNavbarRoot className="Nav">
@@ -41,67 +33,14 @@ export const AdminNavbar = ({ path: currentPath, user }: AdminNavbarProps) => {
       </AdminLogoLink>
 
       <AdminNavbarItems>
-        {isAdmin && (
-          <>
-            <AdminNavItem
-              name={t`Settings`}
-              path="/admin/settings"
-              currentPath={currentPath}
-              key="admin-nav-settings"
-            />
-
-            <AdminNavItem
-              name={t`People`}
-              path="/admin/people"
-              currentPath={currentPath}
-              key="admin-nav-people"
-            />
-          </>
-        )}
-
-        {canAccessDataModel && (
+        {allowedMenuItems.map(({ name, key, path }) => (
           <AdminNavItem
-            name={t`Data Model`}
-            path="/admin/datamodel"
+            name={name}
+            path={path}
+            key={key}
             currentPath={currentPath}
-            key="admin-nav-datamodel"
           />
-        )}
-        {canAccessDatabaseManagement && (
-          <AdminNavItem
-            name={t`Databases`}
-            path="/admin/databases"
-            currentPath={currentPath}
-            key="admin-nav-databases"
-          />
-        )}
-
-        {isAdmin && (
-          <>
-            <AdminNavItem
-              name={t`Permissions`}
-              path="/admin/permissions"
-              currentPath={currentPath}
-              key="admin-nav-permissions"
-            />
-
-            {PLUGIN_ADMIN_NAV_ITEMS.map(({ name, path }) => (
-              <AdminNavItem
-                name={name}
-                path={path}
-                currentPath={currentPath}
-                key={`admin-nav-${name}`}
-              />
-            ))}
-
-            <AdminNavItem
-              name={t`Troubleshooting`}
-              path="/admin/troubleshooting"
-              currentPath={currentPath}
-              key="admin-nav-troubleshooting"
-            />
-          </>
-        )}
+        ))}
       </AdminNavbarItems>
 
       {!MetabaseSettings.isPaidPlan() && <StoreLink />}
diff --git a/frontend/src/metabase/nav/components/ProfileLink.jsx b/frontend/src/metabase/nav/components/ProfileLink.jsx
index 33bdc7497b8..f20f66595f7 100644
--- a/frontend/src/metabase/nav/components/ProfileLink.jsx
+++ b/frontend/src/metabase/nav/components/ProfileLink.jsx
@@ -5,7 +5,7 @@ import _ from "underscore";
 
 import { capitalize } from "metabase/lib/formatting";
 import { color } from "metabase/lib/colors";
-import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins";
+import { canAccessAdmin } from "metabase/nav/utils";
 
 import MetabaseSettings from "metabase/lib/settings";
 import * as Urls from "metabase/lib/urls";
@@ -38,8 +38,7 @@ export default class ProfileLink extends Component {
     const { tag } = MetabaseSettings.get("version");
     const { user, handleCloseNavbar } = this.props;
     const isAdmin = user.is_superuser;
-    const canAccessSettings =
-      isAdmin || PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessSettings(user);
+    const showAdminSettingsItem = canAccessAdmin(user);
 
     return [
       {
@@ -49,7 +48,7 @@ export default class ProfileLink extends Component {
         event: `Navbar;Profile Dropdown;Edit Profile`,
         onClose: handleCloseNavbar,
       },
-      canAccessSettings && {
+      showAdminSettingsItem && {
         title: t`Admin settings`,
         icon: null,
         link: "/admin",
diff --git a/frontend/src/metabase/nav/components/RecentsList.jsx b/frontend/src/metabase/nav/components/RecentsList.jsx
index 530500f183b..8d1708d52ec 100644
--- a/frontend/src/metabase/nav/components/RecentsList.jsx
+++ b/frontend/src/metabase/nav/components/RecentsList.jsx
@@ -17,7 +17,7 @@ import { ItemIcon } from "metabase/search/components/SearchResult";
 import EmptyState from "metabase/components/EmptyState";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
-import { getTranslatedEntityName } from "./utils";
+import { getTranslatedEntityName } from "../utils";
 import {
   EmptyStateContainer,
   Header,
diff --git a/frontend/src/metabase/nav/utils/admin-paths.ts b/frontend/src/metabase/nav/utils/admin-paths.ts
new file mode 100644
index 00000000000..34c57f88a85
--- /dev/null
+++ b/frontend/src/metabase/nav/utils/admin-paths.ts
@@ -0,0 +1,70 @@
+import { User } from "metabase-types/api";
+import { PLUGIN_ADMIN_NAV_ITEMS } from "metabase/plugins";
+import { t } from "ttag";
+
+type PathKeys =
+  | "data-model"
+  | "settings"
+  | "people"
+  | "databases"
+  | "permissions"
+  | "troubleshooting"
+  | "audit"
+  | "tools";
+
+export const NAV_PERMISSION_GUARD: {
+  [key in PathKeys]?: (user: User) => boolean;
+} = {};
+
+const defaultGuard = (user?: User) => user?.is_superuser;
+
+const canAccessMenuItem = (key: PathKeys, user: User) => {
+  return defaultGuard(user) || NAV_PERMISSION_GUARD[key]?.(user);
+};
+
+const getAllMenuItems: () => {
+  key: PathKeys;
+  name: string;
+  path: string;
+}[] = () => [
+  {
+    name: t`Settings`,
+    path: "/admin/settings",
+    key: "settings",
+  },
+  {
+    name: t`People`,
+    path: "/admin/people",
+    key: "people",
+  },
+  {
+    name: t`Data Model`,
+    path: "/admin/datamodel",
+    key: "data-model",
+  },
+  {
+    name: t`Databases`,
+    path: "/admin/databases",
+    key: "databases",
+  },
+  {
+    name: t`Permissions`,
+    path: "/admin/permissions",
+    key: "permissions",
+  },
+  ...PLUGIN_ADMIN_NAV_ITEMS,
+  {
+    name: t`Troubleshooting`,
+    path: "/admin/troubleshooting",
+    key: "troubleshooting",
+  },
+];
+
+export const getAllowedMenuItems = (user: User) =>
+  getAllMenuItems().filter(item => canAccessMenuItem(item.key, user));
+
+export const canAccessAdmin = (user: User) =>
+  getAllowedMenuItems(user).length > 0;
+
+export const canAccessPath = (key: string, user: User) =>
+  getAllowedMenuItems(user).some(item => item.key === key);
diff --git a/frontend/src/metabase/nav/utils/index.js b/frontend/src/metabase/nav/utils/index.js
new file mode 100644
index 00000000000..3f098bf64c2
--- /dev/null
+++ b/frontend/src/metabase/nav/utils/index.js
@@ -0,0 +1,2 @@
+export * from "./model-names";
+export * from "./admin-paths";
diff --git a/frontend/src/metabase/nav/components/utils.js b/frontend/src/metabase/nav/utils/model-names.ts
similarity index 70%
rename from frontend/src/metabase/nav/components/utils.js
rename to frontend/src/metabase/nav/utils/model-names.ts
index bbbdd7b5636..3bf286a8ce0 100644
--- a/frontend/src/metabase/nav/components/utils.js
+++ b/frontend/src/metabase/nav/utils/model-names.ts
@@ -1,6 +1,6 @@
 import { t } from "ttag";
 
-const TRANSLATED_NAME_BY_MODEL_TYPE = {
+const TRANSLATED_NAME_BY_MODEL_TYPE: Record<string, string> = {
   card: t`Question`,
   dataset: t`Dataset`,
   dashboard: t`Dashboard`,
@@ -12,5 +12,5 @@ const TRANSLATED_NAME_BY_MODEL_TYPE = {
   pulse: t`Pulse`,
 };
 
-export const getTranslatedEntityName = type =>
+export const getTranslatedEntityName = (type: string) =>
   TRANSLATED_NAME_BY_MODEL_TYPE[type] || null;
diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts
index 2f4761b3748..b8b269d3b6d 100644
--- a/frontend/src/metabase/plugins/index.ts
+++ b/frontend/src/metabase/plugins/index.ts
@@ -126,9 +126,6 @@ export const PLUGIN_ADVANCED_PERMISSIONS = {
 };
 
 export const PLUGIN_FEATURE_LEVEL_PERMISSIONS = {
-  canAccessSettings: (_user: User) => false,
-  canAccessDataModel: (_user: User) => false,
-  canAccessDatabaseManagement: (_user: User) => false,
   getFeatureLevelDataPermissions: (
     _entityId: DatabaseEntityId,
     _groupId: number,
diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx
index 93adbb51534..fedd580ce11 100644
--- a/frontend/src/metabase/routes.jsx
+++ b/frontend/src/metabase/routes.jsx
@@ -1,9 +1,6 @@
 import React from "react";
 
-import {
-  PLUGIN_LANDING_PAGE,
-  PLUGIN_FEATURE_LEVEL_PERMISSIONS,
-} from "metabase/plugins";
+import { PLUGIN_LANDING_PAGE } from "metabase/plugins";
 
 import { Route } from "metabase/hoc/Title";
 import { Redirect, IndexRedirect, IndexRoute } from "react-router";
@@ -85,6 +82,7 @@ import DashboardMoveModal from "metabase/dashboard/components/DashboardMoveModal
 import DashboardCopyModal from "metabase/dashboard/components/DashboardCopyModal";
 import DashboardDetailsModal from "metabase/dashboard/components/DashboardDetailsModal";
 import { ModalRoute } from "metabase/hoc/ModalRoute";
+import { canAccessAdmin } from "metabase/nav/utils";
 
 import HomePage from "metabase/home/homepage/containers/HomePage";
 import CollectionLanding from "metabase/components/CollectionLanding/CollectionLanding";
@@ -129,8 +127,7 @@ const UserIsNotAuthenticated = UserAuthWrapper({
 
 const UserCanAccessSettings = UserAuthWrapper({
   predicate: currentUser =>
-    currentUser?.is_superuser ||
-    PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessSettings(currentUser),
+    currentUser?.is_superuser || canAccessAdmin(currentUser),
   failureRedirectPath: "/unauthorized",
   authSelector: state => state.currentUser,
   allowRedirectBack: false,
@@ -138,20 +135,6 @@ const UserCanAccessSettings = UserAuthWrapper({
   redirectAction: routerActions.replace,
 });
 
-const createRouteGuard = (userPredicate, displayName) => {
-  const Wrapper = UserAuthWrapper({
-    predicate: currentUser =>
-      currentUser?.is_superuser || userPredicate(currentUser),
-    failureRedirectPath: "/unauthorized",
-    authSelector: state => state.currentUser,
-    allowRedirectBack: false,
-    wrapperDisplayName: displayName,
-    redirectAction: routerActions.replace,
-  });
-
-  return Wrapper(({ children }) => children);
-};
-
 const IsAuthenticated = MetabaseIsSetup(
   UserIsAuthenticated(({ children }) => children),
 );
@@ -167,16 +150,6 @@ const CanAccessSettings = MetabaseIsSetup(
   UserIsAuthenticated(UserCanAccessSettings(({ children }) => children)),
 );
 
-const CanAccessDataModel = createRouteGuard(
-  PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessDataModel,
-  "CanAccessDataModel",
-);
-
-const CanAccessDatabaseManagement = createRouteGuard(
-  PLUGIN_FEATURE_LEVEL_PERMISSIONS.canAccessDatabaseManagement,
-  "CanAccessDatabasesManagement",
-);
-
 export const getRoutes = store => (
   <Route title={t`Metabase`} component={App}>
     {/* SETUP */}
@@ -382,13 +355,7 @@ export const getRoutes = store => (
       {getAccountRoutes(store, IsAuthenticated)}
 
       {/* ADMIN */}
-      {getAdminRoutes(
-        store,
-        CanAccessSettings,
-        IsAdmin,
-        CanAccessDataModel,
-        CanAccessDatabaseManagement,
-      )}
+      {getAdminRoutes(store, CanAccessSettings, IsAdmin)}
     </Route>
 
     {/* INTERNAL */}
diff --git a/frontend/src/metabase/search/components/InfoText.jsx b/frontend/src/metabase/search/components/InfoText.jsx
index 826ebafbbcc..6bf953a6bb5 100644
--- a/frontend/src/metabase/search/components/InfoText.jsx
+++ b/frontend/src/metabase/search/components/InfoText.jsx
@@ -11,7 +11,7 @@ import Schema from "metabase/entities/schemas";
 import Database from "metabase/entities/databases";
 import Table from "metabase/entities/tables";
 import { PLUGIN_COLLECTIONS } from "metabase/plugins";
-import { getTranslatedEntityName } from "metabase/nav/components/utils";
+import { getTranslatedEntityName } from "metabase/nav/utils";
 import { CollectionBadge } from "./CollectionBadge";
 
 const searchResultPropTypes = {
diff --git a/frontend/test/metabase/scenarios/permissions/general-permissions.cy.spec.js b/frontend/test/metabase/scenarios/permissions/general-permissions.cy.spec.js
index fa4c201dd48..4717beddc3b 100644
--- a/frontend/test/metabase/scenarios/permissions/general-permissions.cy.spec.js
+++ b/frontend/test/metabase/scenarios/permissions/general-permissions.cy.spec.js
@@ -6,6 +6,7 @@ import {
 } from "__support__/e2e/cypress";
 
 const SUBSCRIPTIONS_INDEX = 0;
+const MONITORING_INDEX = 1;
 
 describeEE("scenarios > admin > permissions > general", () => {
   beforeEach(() => {
@@ -13,72 +14,143 @@ describeEE("scenarios > admin > permissions > general", () => {
     cy.signInAsAdmin();
   });
 
-  describe("revoked permission", () => {
-    beforeEach(() => {
-      cy.visit("/admin/permissions/general");
+  describe("subscriptions permission", () => {
+    describe("revoked", () => {
+      beforeEach(() => {
+        cy.visit("/admin/permissions/general");
 
-      modifyPermission("All Users", SUBSCRIPTIONS_INDEX, "No");
+        modifyPermission("All Users", SUBSCRIPTIONS_INDEX, "No");
 
-      cy.button("Save changes").click();
+        cy.button("Save changes").click();
 
-      modal().within(() => {
-        cy.findByText("Save permissions?");
-        cy.findByText("Are you sure you want to do this?");
-        cy.button("Yes").click();
+        modal().within(() => {
+          cy.findByText("Save permissions?");
+          cy.findByText("Are you sure you want to do this?");
+          cy.button("Yes").click();
+        });
+
+        cy.signInAsNormalUser();
       });
 
-      cy.signInAsNormalUser();
-    });
+      it("revokes ability to create dashboard subscriptions", () => {
+        cy.visit("/dashboard/1");
+        cy.icon("subscription")
+          .as("subscriptionsButton")
+          .realHover();
 
-    it("revokes ability to create dashboard subscriptions", () => {
-      cy.visit("/dashboard/1");
-      cy.icon("subscription")
-        .as("subscriptionsButton")
-        .realHover();
+        cy.findByText(
+          "You don't have permission to create a subscription for this dashboard",
+        );
 
-      cy.findByText(
-        "You don't have permission to create a subscription for this dashboard",
-      );
+        cy.get("@subscriptionsButton").click();
+        cy.findByText("Create a dashboard subscription").should("not.exist");
+      });
 
-      cy.get("@subscriptionsButton").click();
-      cy.findByText("Create a dashboard subscription").should("not.exist");
+      it("revokes ability to create question alerts", () => {
+        cy.visit("/question/1");
+        cy.icon("bell")
+          .as("subscriptionsButton")
+          .realHover();
+        cy.findByText(
+          "You don't have permission to share data from this saved question",
+        );
+
+        cy.findByText(
+          "To send alerts, an admin needs to set up email integration.",
+        ).should("not.exist");
+
+        cy.get("@subscriptionsButton").click();
+        cy.findByText("Create a dashboard subscription").should("not.exist");
+      });
     });
 
-    it("revokes ability to create question alerts", () => {
-      cy.visit("/question/1");
-      cy.icon("bell")
-        .as("subscriptionsButton")
-        .realHover();
-      cy.findByText(
-        "You don't have permission to share data from this saved question",
-      );
-
-      cy.findByText(
-        "To send alerts, an admin needs to set up email integration.",
-      ).should("not.exist");
-
-      cy.get("@subscriptionsButton").click();
-      cy.findByText("Create a dashboard subscription").should("not.exist");
+    describe("granted", () => {
+      beforeEach(() => {
+        cy.signInAsNormalUser();
+      });
+
+      it("gives ability to create dashboard subscriptions", () => {
+        cy.visit("/dashboard/1");
+        cy.icon("subscription").click();
+        cy.findByText("Create a dashboard subscription");
+      });
+
+      it("gives ability to create question alerts", () => {
+        cy.visit("/question/1");
+        cy.icon("bell").click();
+        cy.findByText(
+          "To send alerts, an admin needs to set up email integration.",
+        );
+      });
     });
   });
 
-  describe("granted permission", () => {
-    beforeEach(() => {
-      cy.signInAsNormalUser();
-    });
+  describe("monitoring permission", () => {
+    describe("granted", () => {
+      beforeEach(() => {
+        cy.visit("/admin/permissions/general");
+
+        modifyPermission("All Users", MONITORING_INDEX, "Yes");
+
+        cy.button("Save changes").click();
+
+        modal().within(() => {
+          cy.findByText("Save permissions?");
+          cy.findByText("Are you sure you want to do this?");
+          cy.button("Yes").click();
+        });
 
-    it("gives ability to create dashboard subscriptions", () => {
-      cy.visit("/dashboard/1");
-      cy.icon("subscription").click();
-      cy.findByText("Create a dashboard subscription");
+        cy.createNativeQuestion(
+          {
+            name: "broken_question",
+            native: { query: "select * from broken_question" },
+          },
+          { loadMetadata: true },
+        );
+
+        cy.signInAsNormalUser();
+      });
+
+      it("allows accessing tools, audit, and troubleshooting for non-admins", () => {
+        cy.visit("/");
+        cy.icon("gear").click();
+
+        cy.findByText("Admin settings").click();
+
+        // Tools smoke test
+        cy.url().should("include", "/admin/tools/errors");
+        cy.findByText("Questions that errored when last run");
+        cy.findByText("broken_question");
+
+        // Audit smoke test
+        cy.findByText("Audit").click();
+        cy.url().should("include", "/admin/audit/members/overview");
+        cy.findByText("All members").click();
+        cy.findByText("Bobby Tables");
+
+        // Troubleshooting smoke test
+        cy.findByText("Troubleshooting").click();
+        cy.findByText("Diagnostic Info");
+      });
     });
 
-    it("gives ability to create question alerts", () => {
-      cy.visit("/question/1");
-      cy.icon("bell").click();
-      cy.findByText(
-        "To send alerts, an admin needs to set up email integration.",
-      );
+    describe("revoked", () => {
+      it("does not allow accessing tools, audit, and troubleshooting for non-admins", () => {
+        cy.signInAsNormalUser();
+        cy.visit("/");
+        cy.icon("gear").click();
+
+        cy.findByText("Admin settings").should("not.exist");
+
+        cy.visit("/admin/tools/errors");
+        cy.findByText("Sorry, you don’t have permission to see that.");
+
+        cy.visit("/admin/tools/errors");
+        cy.findByText("Sorry, you don’t have permission to see that.");
+
+        cy.visit("/admin/troubleshooting/help");
+        cy.findByText("Sorry, you don’t have permission to see that.");
+      });
     });
   });
 });
-- 
GitLab