Skip to content
Snippets Groups Projects
Unverified Commit a58b9b3e authored by Alexander Lesnenko's avatar Alexander Lesnenko Committed by GitHub
Browse files

FE: Monitoring permission (#21407)

* monitoring permission

* [skip ci] fix types

* fix import order

* specs

* fix spec

* review
parent 0454c0d7
No related branches found
No related tags found
No related merge requests found
Showing
with 191 additions and 123 deletions
......@@ -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 => [
......
......@@ -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" />
......
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;
......
import { User } from "metabase-types/api";
export interface UserWithFeaturePermissions extends User {
permissions?: {
can_access_data_model: boolean;
can_access_database_management: boolean;
};
}
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;
......@@ -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 = {
......
......@@ -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,
};
......
import { GroupId } from "metabase-types/api";
export type GeneralPermissionKey = "subscription";
export type GeneralPermissionKey = "subscription" | "monitoring";
export type GeneralPermissionValue = "yes" | "no";
export type GroupGeneralPermissions = {
......
import { UserWithPermissions } from "./types/user";
export const canAccessMonitoringItems = (user?: UserWithPermissions) =>
user?.permissions.can_access_monitoring ?? false;
......@@ -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);
}
......@@ -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"
......
......@@ -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,
});
......@@ -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;
}
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} />
......
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;
......
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));
};
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 />}
......
......@@ -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",
......
......@@ -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,
......
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);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment