From ae2557c17f27ba1c7e2120e1195ba758f61266ca Mon Sep 17 00:00:00 2001
From: Alexander Lesnenko <alxnddr@users.noreply.github.com>
Date: Fri, 8 Apr 2022 13:35:46 -0400
Subject: [PATCH] FE: Settings access global permission + tweaks (#21460)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* settings global permission

* fix specs

* Enforce Setting permissions (cont) (#21464)

* settings global permission

* more api permissions enforcement

* only admin could call token checks

* address Noah's comments

* clean ns

* clean ns 🤦‍♂️

Co-authored-by: Aleksandr Lesnenko <alxnddr@gmail.com>

* hide subscriptions buttons for users with no permissions

* fix specs

* review fixes

* update spec

Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
---
 .../advanced_permissions/api/setting_test.clj | 84 ++++++++++++++++-
 .../general_permissions/index.ts              |  9 +-
 .../general_permissions/selectors.ts          | 50 ++++------
 .../general_permissions/types/permissions.ts  |  2 +-
 .../general_permissions/types/state.ts        |  4 +-
 .../general_permissions/types/user.ts         |  6 +-
 .../general_permissions/utils.ts              |  9 +-
 .../src/metabase-enterprise/license/index.js  |  1 +
 frontend/src/metabase/admin/routes.jsx        |  9 +-
 .../src/metabase/admin/settings/selectors.js  | 57 +++++++-----
 frontend/src/metabase/admin/utils.js          | 27 +++++-
 .../dashboard/components/DashboardActions.jsx | 11 +--
 .../components/view/QuestionAlertWidget.jsx   |  8 +-
 .../general-permissions.cy.spec.js            | 69 +++++++++-----
 src/metabase/api/geojson.clj                  |  3 +-
 src/metabase/api/ldap.clj                     |  3 +-
 src/metabase/api/permissions.clj              |  3 +-
 src/metabase/api/setup.clj                    |  3 +-
 test/metabase/api/geojson_test.clj            |  2 +-
 test/metabase/api/ldap_test.clj               | 46 ++++++----
 test/metabase/api/permissions_test.clj        |  8 +-
 test/metabase/api/premium_features_test.clj   | 24 +++++
 test/metabase/api/setup_test.clj              | 91 ++++++++++---------
 .../public_settings/premium_features_test.clj |  2 +-
 24 files changed, 351 insertions(+), 180 deletions(-)
 create mode 100644 test/metabase/api/premium_features_test.clj

diff --git a/enterprise/backend/test/metabase_enterprise/advanced_permissions/api/setting_test.clj b/enterprise/backend/test/metabase_enterprise/advanced_permissions/api/setting_test.clj
index 2a4d04aaa0f..2edff32c469 100644
--- a/enterprise/backend/test/metabase_enterprise/advanced_permissions/api/setting_test.clj
+++ b/enterprise/backend/test/metabase_enterprise/advanced_permissions/api/setting_test.clj
@@ -1,13 +1,16 @@
 (ns metabase-enterprise.advanced-permissions.api.setting-test
   "Permisisons tests for API that needs to be enforced by General Permissions to access Admin/Setting pages."
   (:require [clojure.test :refer :all]
+            [metabase.api.geojson-test :as geojson-test]
+            [metabase.api.ldap-test :as ldap-test]
             [metabase.email :as email]
             [metabase.integrations.slack :as slack]
             [metabase.models.permissions :as perms]
             [metabase.models.setting-test :refer [test-setting-1 test-setting-2]]
             [metabase.public-settings.premium-features-test :as premium-features-test]
             [metabase.test :as mt]
-            [metabase.test.fixtures :as fixtures]))
+            [metabase.test.fixtures :as fixtures]
+            [metabase.test.integrations.ldap :as ldap.test]))
 
 (use-fixtures :once (fixtures/initialize :db))
 
@@ -156,3 +159,82 @@
               (get-manifest user 200)
               (set-slack-settings :crowberto 200)
               (get-manifest :crowberto 200))))))))
+
+(deftest ldap-api-test
+  (testing "/api/ldap"
+    (mt/with-user-in-groups
+      [group {:name "New Group"}
+       user  [group]]
+      (letfn [(update-ldap-settings [user status]
+                (testing (format "update ldap settings with %s user" (user->name user))
+                  (ldap.test/with-ldap-server
+                    (mt/user-http-request user :put status "ldap/settings"
+                                          (ldap-test/ldap-test-details)))))]
+
+        (testing "if `advanced-permissions` is disabled, require admins"
+          (premium-features-test/with-premium-features #{}
+            (update-ldap-settings user 403)
+            (update-ldap-settings :crowberto 200)))
+
+        (testing "if `advanced-permissions` is enabled"
+          (premium-features-test/with-premium-features #{:advanced-permissions}
+            (testing "still fail if user's group doesn't have `setting` permission"
+              (update-ldap-settings user 403)
+              (update-ldap-settings :crowberto 200))
+
+            (testing "succeed if user's group has `setting` permission"
+              (perms/grant-general-permissions! group :setting)
+              (update-ldap-settings user 200)
+              (update-ldap-settings :crowberto 200))))))))
+
+
+(deftest geojson-api-test
+  (testing "/api/geojson"
+    (mt/with-user-in-groups
+      [group {:name "New Group"}
+       user  [group]]
+      (letfn [(get-geojson [user status]
+                (testing (format "get geojson with %s user" (user->name user))
+                  (mt/user-http-request user :get status "geojson"
+                                        :url geojson-test/test-geojson-url)))]
+
+        (testing "if `advanced-permissions` is disabled, require admins"
+          (premium-features-test/with-premium-features #{}
+            (get-geojson user 403)
+            (get-geojson :crowberto 200)))
+
+        (testing "if `advanced-permissions` is enabled"
+          (premium-features-test/with-premium-features #{:advanced-permissions}
+            (testing "still fail if user's group doesn't have `setting` permission"
+              (get-geojson user 403)
+              (get-geojson :crowberto 200))
+
+            (testing "succeed if user's group has `setting` permission"
+              (perms/grant-general-permissions! group :setting)
+              (get-geojson user 200)
+              (get-geojson :crowberto 200))))))))
+
+(deftest permissions-group-api-test
+  (testing "/api/permissions"
+    (mt/with-user-in-groups
+      [group {:name "New Group"}
+       user  [group]]
+      (letfn [(get-permission-groups [user status]
+                (testing (format "get permission groups with %s user" (user->name user))
+                  (mt/user-http-request user :get status "permissions/group")))]
+
+        (testing "if `advanced-permissions` is disabled, require admins"
+          (premium-features-test/with-premium-features #{}
+            (get-permission-groups user 403)
+            (get-permission-groups :crowberto 200)))
+
+        (testing "if `advanced-permissions` is enabled"
+          (premium-features-test/with-premium-features #{:advanced-permissions}
+            (testing "still fail if user's group doesn't have `setting` permission"
+              (get-permission-groups user 403)
+              (get-permission-groups :crowberto 200))
+
+            (testing "succeed if user's group has `setting` permission"
+              (perms/grant-general-permissions! group :setting)
+              (get-permission-groups user 200)
+              (get-permission-groups :crowberto 200))))))))
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/index.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/index.ts
index 6442604c2e0..cd8ba72dc6a 100644
--- a/enterprise/frontend/src/metabase-enterprise/general_permissions/index.ts
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/index.ts
@@ -6,12 +6,13 @@ import { t } from "ttag";
 import { canManageSubscriptions } from "./selectors";
 import generalPermissionsReducer from "./reducer";
 import { NAV_PERMISSION_GUARD } from "metabase/nav/utils";
-import { canAccessMonitoringItems } from "./utils";
+import { canAccessMonitoringItems, canAccessSettings } 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;
+  NAV_PERMISSION_GUARD["audit"] = canAccessMonitoringItems;
+  NAV_PERMISSION_GUARD["tools"] = canAccessMonitoringItems;
+  NAV_PERMISSION_GUARD["troubleshooting"] = canAccessMonitoringItems;
+  NAV_PERMISSION_GUARD["settings"] = canAccessSettings;
 
   PLUGIN_GENERAL_PERMISSIONS.getRoutes = getRoutes;
   PLUGIN_GENERAL_PERMISSIONS.tabs = [{ name: t`General`, value: `general` }];
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/selectors.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/selectors.ts
index 687523e6567..4f71ff226e1 100644
--- a/enterprise/frontend/src/metabase-enterprise/general_permissions/selectors.ts
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/selectors.ts
@@ -11,10 +11,7 @@ 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;
+  state.currentUser.permissions?.can_access_subscription ?? false;
 
 const getGeneralPermission = (
   permissions: GeneralPermissions,
@@ -30,6 +27,19 @@ export const getIsDirty = createSelector(
     !_.isEqual(permissions, originalPermissions),
 );
 
+const getPermission = (
+  permissions: GeneralPermissions,
+  isAdmin: boolean,
+  groupId: number,
+  permissionKey: GeneralPermissionKey,
+) => ({
+  permission: permissionKey,
+  isDisabled: isAdmin,
+  disabledTooltip: isAdmin ? UNABLE_TO_CHANGE_ADMIN_PERMISSIONS : null,
+  value: getGeneralPermission(permissions, groupId, permissionKey),
+  options: [GENERAL_PERMISSIONS_OPTIONS.yes, GENERAL_PERMISSIONS_OPTIONS.no],
+});
+
 export const getGeneralPermissionEditor = createSelector(
   (state: GeneralPermissionsState) =>
     state.plugins.generalPermissionsPlugin?.generalPermissions,
@@ -46,30 +56,9 @@ export const getGeneralPermissionEditor = createSelector(
         id: group.id,
         name: group.name,
         permissions: [
-          {
-            permission: "subscription",
-            isDisabled: isAdmin,
-            disabledTooltip: isAdmin
-              ? UNABLE_TO_CHANGE_ADMIN_PERMISSIONS
-              : null,
-            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,
-            ],
-          },
+          getPermission(permissions, isAdmin, group.id, "setting"),
+          getPermission(permissions, isAdmin, group.id, "monitoring"),
+          getPermission(permissions, isAdmin, group.id, "subscription"),
         ],
       };
     });
@@ -77,12 +66,13 @@ export const getGeneralPermissionEditor = createSelector(
     return {
       filterPlaceholder: t`Search for a group`,
       columns: [
-        { name: `General settings access` },
-        { name: `Subscriptions and Alerts` },
+        { name: t`Group name` },
+        { name: t`General settings access` },
         {
           name: `Monitoring access`,
           hint: t`This grants access to Tools, Audit, and Troubleshooting`,
         },
+        { name: t`Subscriptions and Alerts` },
       ],
       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 0ec9be60311..73e3bcd6d30 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" | "monitoring";
+export type GeneralPermissionKey = "subscription" | "monitoring" | "setting";
 export type GeneralPermissionValue = "yes" | "no";
 
 export type GroupGeneralPermissions = {
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/types/state.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/types/state.ts
index 397e161a687..b882d796241 100644
--- a/enterprise/frontend/src/metabase-enterprise/general_permissions/types/state.ts
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/types/state.ts
@@ -1,9 +1,9 @@
 import { State } from "metabase-types/store";
 import { GeneralPermissions } from "./permissions";
-import { UserWithPermissions } from "./user";
+import { UserWithGeneralPermissions } from "./user";
 
 export interface GeneralPermissionsState extends State {
-  currentUser: UserWithPermissions;
+  currentUser: UserWithGeneralPermissions;
   plugins: {
     generalPermissionsPlugin: {
       generalPermissions: GeneralPermissions;
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/types/user.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/types/user.ts
index bee42b5934a..c0bd7197d9d 100644
--- a/enterprise/frontend/src/metabase-enterprise/general_permissions/types/user.ts
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/types/user.ts
@@ -1,9 +1,9 @@
 import { User } from "metabase-types/api";
 
-export interface UserWithPermissions extends User {
-  permissions: {
+export interface UserWithGeneralPermissions extends User {
+  permissions?: {
     can_access_monitoring: boolean;
-    can_access_settings: boolean;
+    can_access_setting: boolean;
     can_access_subscription: boolean;
   };
 }
diff --git a/enterprise/frontend/src/metabase-enterprise/general_permissions/utils.ts b/enterprise/frontend/src/metabase-enterprise/general_permissions/utils.ts
index 19ad1fd8a88..43c7580c640 100644
--- a/enterprise/frontend/src/metabase-enterprise/general_permissions/utils.ts
+++ b/enterprise/frontend/src/metabase-enterprise/general_permissions/utils.ts
@@ -1,4 +1,7 @@
-import { UserWithPermissions } from "./types/user";
+import { UserWithGeneralPermissions } from "./types/user";
 
-export const canAccessMonitoringItems = (user?: UserWithPermissions) =>
-  user?.permissions.can_access_monitoring ?? false;
+export const canAccessMonitoringItems = (user?: UserWithGeneralPermissions) =>
+  user?.permissions?.can_access_monitoring ?? false;
+
+export const canAccessSettings = (user?: UserWithGeneralPermissions) =>
+  user?.permissions?.can_access_setting ?? false;
diff --git a/enterprise/frontend/src/metabase-enterprise/license/index.js b/enterprise/frontend/src/metabase-enterprise/license/index.js
index 8a19890ea92..d3ab7f75252 100644
--- a/enterprise/frontend/src/metabase-enterprise/license/index.js
+++ b/enterprise/frontend/src/metabase-enterprise/license/index.js
@@ -10,6 +10,7 @@ PLUGIN_ADMIN_SETTINGS_UPDATES.push(sections =>
       ...license,
       component: LicenseAndBillingSettings,
       name: t`License and Billing`,
+      adminOnly: true,
     };
   }),
 );
diff --git a/frontend/src/metabase/admin/routes.jsx b/frontend/src/metabase/admin/routes.jsx
index e451ae24af1..97a9db7620e 100644
--- a/frontend/src/metabase/admin/routes.jsx
+++ b/frontend/src/metabase/admin/routes.jsx
@@ -10,7 +10,10 @@ import {
 
 import { withBackground } from "metabase/hoc/Background";
 import { ModalRoute } from "metabase/hoc/ModalRoute";
-import { createAdminRouteGuard } from "metabase/admin/utils";
+import {
+  createAdminRouteGuard,
+  createAdminRedirect,
+} from "metabase/admin/utils";
 
 import RedirectToAllowedSettings from "./settings/containers/RedirectToAllowedSettings";
 import AdminApp from "metabase/admin/app/components/AdminApp";
@@ -154,9 +157,9 @@ const getRoutes = (store, CanAccessSettings, IsAdmin) => (
       </Route>
 
       {/* SETTINGS */}
-      <Route path="settings" component={IsAdmin}>
+      <Route path="settings" component={createAdminRouteGuard("settings")}>
+        <IndexRoute component={createAdminRedirect("setup", "general")} />
         <Route title={t`Settings`}>
-          <IndexRedirect to="setup" />
           <Route
             path="premium-embedding-license"
             component={PremiumEmbeddingLicensePage}
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index b5cc3550333..c219d4df425 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -25,6 +25,7 @@ import { trackTrackingPermissionChanged } from "./analytics";
 
 import { UtilApi } from "metabase/services";
 import { PLUGIN_ADMIN_SETTINGS_UPDATES } from "metabase/plugins";
+import { getUserIsAdmin } from "metabase/selectors/user";
 
 // This allows plugins to update the settings sections
 function updateSectionsWithPlugins(sections) {
@@ -54,6 +55,7 @@ const SECTIONS = updateSectionsWithPlugins({
     order: 1,
     settings: [],
     component: SettingsSetupList,
+    adminOnly: true,
   },
   general: {
     name: t`General`,
@@ -131,6 +133,7 @@ const SECTIONS = updateSectionsWithPlugins({
         type: "boolean",
       },
     ],
+    adminOnly: true,
   },
   email: {
     name: t`Email`,
@@ -418,31 +421,39 @@ export const getNewVersionAvailable = createSelector(getSettings, settings => {
   return MetabaseSettings.newVersionAvailable(settings);
 });
 
-export const getSections = createSelector(getSettings, settings => {
-  if (!settings || _.isEmpty(settings)) {
-    return {};
-  }
+export const getSections = createSelector(
+  getSettings,
+  getUserIsAdmin,
+  (settings, isAdmin) => {
+    if (!settings || _.isEmpty(settings)) {
+      return {};
+    }
 
-  const settingsByKey = _.groupBy(settings, "key");
-  const sectionsWithAPISettings = {};
-  for (const [slug, section] of Object.entries(SECTIONS)) {
-    const settings = section.settings.map(function(setting) {
-      const apiSetting =
-        settingsByKey[setting.key] && settingsByKey[setting.key][0];
-      if (apiSetting) {
-        return {
-          placeholder: apiSetting.default,
-          ...apiSetting,
-          ...setting,
-        };
-      } else {
-        return setting;
+    const settingsByKey = _.groupBy(settings, "key");
+    const sectionsWithAPISettings = {};
+    for (const [slug, section] of Object.entries(SECTIONS)) {
+      if (section.adminOnly && !isAdmin) {
+        continue;
       }
-    });
-    sectionsWithAPISettings[slug] = { ...section, settings };
-  }
-  return sectionsWithAPISettings;
-});
+
+      const settings = section.settings.map(function(setting) {
+        const apiSetting =
+          settingsByKey[setting.key] && settingsByKey[setting.key][0];
+        if (apiSetting) {
+          return {
+            placeholder: apiSetting.default,
+            ...apiSetting,
+            ...setting,
+          };
+        } else {
+          return setting;
+        }
+      });
+      sectionsWithAPISettings[slug] = { ...section, settings };
+    }
+    return sectionsWithAPISettings;
+  },
+);
 
 export const getActiveSectionName = (state, props) => props.params.splat;
 
diff --git a/frontend/src/metabase/admin/utils.js b/frontend/src/metabase/admin/utils.js
index e83208555a7..21e7a3fb0fa 100644
--- a/frontend/src/metabase/admin/utils.js
+++ b/frontend/src/metabase/admin/utils.js
@@ -1,6 +1,8 @@
+import { connect } from "react-redux";
 import { UserAuthWrapper } from "redux-auth-wrapper";
-import { routerActions } from "react-router-redux";
+import { routerActions, push } from "react-router-redux";
 import { canAccessPath } from "metabase/nav/utils";
+import { getUser } from "metabase/selectors/user";
 
 export const createAdminRouteGuard = (routeKey, Component) => {
   const Wrapper = UserAuthWrapper({
@@ -14,3 +16,26 @@ export const createAdminRouteGuard = (routeKey, Component) => {
 
   return Wrapper(Component ?? (({ children }) => children));
 };
+
+const mapStateToProps = state => ({
+  user: getUser(state),
+});
+
+const mapDispatchToProps = {
+  push,
+};
+
+export const createAdminRedirect = (adminPath, nonAdminPath) => {
+  const NonAdminRedirectComponent = connect(
+    mapStateToProps,
+    mapDispatchToProps,
+  )(({ user, push, location }) => {
+    const path = `${location.pathname}/${
+      user.is_superuser ? adminPath : nonAdminPath
+    }`;
+    push(path);
+    return null;
+  });
+
+  return NonAdminRedirectComponent;
+};
diff --git a/frontend/src/metabase/dashboard/components/DashboardActions.jsx b/frontend/src/metabase/dashboard/components/DashboardActions.jsx
index 0c7f66ba41a..72a7e5928ed 100644
--- a/frontend/src/metabase/dashboard/components/DashboardActions.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardActions.jsx
@@ -46,18 +46,13 @@ export const getDashboardActions = (
     dashboard.ordered_cards.some(dashCard => dashCard.card.display !== "text");
 
   const canShareDashboard = hasCards;
+  const canCreateSubscription = hasDataCards && canManageSubscriptions;
 
   if (!isEditing && !isEmpty && !isPublic) {
     // Getting notifications with static text-only cards doesn't make a lot of sense
-    if (hasDataCards) {
+    if (canCreateSubscription) {
       buttons.push(
-        <Tooltip
-          tooltip={
-            canManageSubscriptions
-              ? t`Subscriptions`
-              : t`You don't have permission to create a subscription for this dashboard`
-          }
-        >
+        <Tooltip tooltip={t`Subscriptions`}>
           <span>
             <DashboardHeaderButton
               disabled={!canManageSubscriptions}
diff --git a/frontend/src/metabase/query_builder/components/view/QuestionAlertWidget.jsx b/frontend/src/metabase/query_builder/components/view/QuestionAlertWidget.jsx
index 3242c5edf5d..41b3ed23690 100644
--- a/frontend/src/metabase/query_builder/components/view/QuestionAlertWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/view/QuestionAlertWidget.jsx
@@ -36,13 +36,7 @@ export default class QuestionAlertWidget extends React.Component {
     const { isOpen, isFrozen } = this.state;
 
     if (!canManageSubscriptions) {
-      return (
-        <Icon
-          name="bell"
-          tooltip={t`You don't have permission to share data from this saved question`}
-          className={cx(className, "text-light")}
-        />
-      );
+      return null;
     }
 
     if (question.isSaved() && Object.values(questionAlerts).length > 0) {
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 4717beddc3b..e5c9f7a3ff9 100644
--- a/frontend/test/metabase/scenarios/permissions/general-permissions.cy.spec.js
+++ b/frontend/test/metabase/scenarios/permissions/general-permissions.cy.spec.js
@@ -5,8 +5,9 @@ import {
   modifyPermission,
 } from "__support__/e2e/cypress";
 
-const SUBSCRIPTIONS_INDEX = 0;
+const SETTINGS_INDEX = 0;
 const MONITORING_INDEX = 1;
+const SUBSCRIPTIONS_INDEX = 2;
 
 describeEE("scenarios > admin > permissions > general", () => {
   beforeEach(() => {
@@ -34,33 +35,12 @@ describeEE("scenarios > admin > permissions > general", () => {
 
       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.get("@subscriptionsButton").click();
-        cy.findByText("Create a dashboard subscription").should("not.exist");
+        cy.icon("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");
+        cy.icon("bell").should("not.exist");
       });
     });
 
@@ -153,4 +133,45 @@ describeEE("scenarios > admin > permissions > general", () => {
       });
     });
   });
+
+  describe("settings permission", () => {
+    describe("granted", () => {
+      beforeEach(() => {
+        cy.visit("/admin/permissions/general");
+
+        modifyPermission("All Users", SETTINGS_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();
+        });
+
+        cy.signInAsNormalUser();
+      });
+
+      it("allows editing settings as a non-admin user", () => {
+        cy.visit("/");
+        cy.icon("gear").click();
+
+        cy.findByText("Admin settings").click();
+
+        cy.url().should("include", "/admin/settings/general");
+
+        cy.findByText("License and Billing").should("not.exist");
+        cy.findByText("Setup").should("not.exist");
+        cy.findByText("Updates").should("not.exist");
+
+        // General smoke test
+        cy.get("#setting-site-name")
+          .clear()
+          .type("new name")
+          .blur();
+
+        cy.findByText("Saved");
+      });
+    });
+  });
 });
diff --git a/src/metabase/api/geojson.clj b/src/metabase/api/geojson.clj
index 0bbfb81872b..79c9f35d7f3 100644
--- a/src/metabase/api/geojson.clj
+++ b/src/metabase/api/geojson.clj
@@ -2,6 +2,7 @@
   (:require [clojure.java.io :as io]
             [compojure.core :refer [GET]]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.models.setting :as setting :refer [defsetting]]
             [metabase.util.i18n :as ui18n :refer [deferred-tru tru]]
             [metabase.util.schema :as su]
@@ -114,7 +115,7 @@
   This behaves similarly to /api/geojson/:key but doesn't require the custom map to be saved to the DB first."
   [{{:keys [url]} :params} respond raise]
   {url su/NonBlankString}
-  (api/check-superuser)
+  (validation/check-has-general-permission :setting)
   (let [decoded-url (rc/url-decode url)]
     (try
       (when-not (valid-geojson-url? decoded-url)
diff --git a/src/metabase/api/ldap.clj b/src/metabase/api/ldap.clj
index 05ddb388859..b5dd74c5617 100644
--- a/src/metabase/api/ldap.clj
+++ b/src/metabase/api/ldap.clj
@@ -4,6 +4,7 @@
             [clojure.tools.logging :as log]
             [compojure.core :refer [PUT]]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.integrations.ldap :as ldap]
             [metabase.models.setting :as setting]
             [metabase.util.schema :as su]))
@@ -89,7 +90,7 @@
   "Update LDAP related settings. You must be a superuser to do this."
   [:as {settings :body}]
   {settings su/Map}
-  (api/check-superuser)
+  (validation/check-has-general-permission :setting)
   (let [ldap-settings (-> settings
                           (select-keys (keys mb-settings->ldap-details))
                           (assoc :ldap-port (when-let [^String ldap-port (not-empty (str (:ldap-port settings)))]
diff --git a/src/metabase/api/permissions.clj b/src/metabase/api/permissions.clj
index 7cc1143dfcd..db393549395 100644
--- a/src/metabase/api/permissions.clj
+++ b/src/metabase/api/permissions.clj
@@ -4,6 +4,7 @@
             [compojure.core :refer [DELETE GET POST PUT]]
             [honeysql.helpers :as hh]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.api.permission-graph :as pg]
             [metabase.models.permissions :as perms]
             [metabase.models.permissions-group :as group :refer [PermissionsGroup]]
@@ -86,7 +87,7 @@
 (api/defendpoint GET "/group"
   "Fetch all `PermissionsGroups`, including a count of the number of `:members` in that group."
   []
-  (api/check-superuser)
+  (validation/check-has-general-permission :setting)
   (-> (ordered-groups offset-paging/*limit* offset-paging/*offset*)
       (hydrate :member_count)))
 
diff --git a/src/metabase/api/setup.clj b/src/metabase/api/setup.clj
index 946ac51c7cd..810d5bab712 100644
--- a/src/metabase/api/setup.clj
+++ b/src/metabase/api/setup.clj
@@ -3,6 +3,7 @@
             [compojure.core :refer [GET POST]]
             [metabase.analytics.snowplow :as snowplow]
             [metabase.api.common :as api]
+            [metabase.api.common.validation :as validation]
             [metabase.api.database :as database-api :refer [DBEngineString]]
             [metabase.config :as config]
             [metabase.driver :as driver]
@@ -300,7 +301,7 @@
 (api/defendpoint GET "/admin_checklist"
   "Return various \"admin checklist\" steps and whether they've been completed. You must be a superuser to see this!"
   []
-  (api/check-superuser)
+  (validation/check-has-general-permission :setting)
   (admin-checklist))
 
 ;; User defaults endpoint
diff --git a/test/metabase/api/geojson_test.clj b/test/metabase/api/geojson_test.clj
index 2baae4336f5..54d28907153 100644
--- a/test/metabase/api/geojson_test.clj
+++ b/test/metabase/api/geojson_test.clj
@@ -10,7 +10,7 @@
             [metabase.util.schema :as su]
             [schema.core :as s]))
 
-(def ^:private ^String test-geojson-url
+(def ^String test-geojson-url
   "URL of a GeoJSON file used for test purposes."
   "https://raw.githubusercontent.com/metabase/metabase/master/test_resources/test.geojson")
 
diff --git a/test/metabase/api/ldap_test.clj b/test/metabase/api/ldap_test.clj
index 157743a3059..4ed6c834638 100644
--- a/test/metabase/api/ldap_test.clj
+++ b/test/metabase/api/ldap_test.clj
@@ -6,29 +6,37 @@
             [metabase.test :as mt]
             [metabase.test.integrations.ldap :as ldap.test]))
 
+(defn ldap-test-details
+  []
+  (-> (ldap.test/get-ldap-details)
+      (set/rename-keys (set/map-invert @#'ldap-api/mb-settings->ldap-details))
+      (assoc :ldap-enabled true)))
+
 (deftest ldap-settings-test
   (testing "PUT /api/ldap/settings"
     (ldap.test/with-ldap-server
-      (let [ldap-test-details (-> (ldap.test/get-ldap-details)
-                                  (set/rename-keys (set/map-invert @#'ldap-api/mb-settings->ldap-details))
-                                  (assoc :ldap-enabled true))]
-        (testing "Valid LDAP settings can be saved via an API call"
-          (mt/user-http-request :crowberto :put 200 "ldap/settings" ldap-test-details))
+      (testing "Valid LDAP settings can be saved via an API call"
+        (mt/user-http-request :crowberto :put 200 "ldap/settings" (ldap-test-details)))
+
+      (testing "Invalid LDAP settings return a server error"
+        (mt/user-http-request :crowberto :put 500 "ldap/settings"
+                              (assoc (ldap-test-details) :ldap-password "wrong-password")))
 
-        (testing "Invalid LDAP settings return a server error"
-          (mt/user-http-request :crowberto :put 500 "ldap/settings"
-                                (assoc ldap-test-details :ldap-password "wrong-password")))
+      (testing "Invalid LDAP settings can be saved if LDAP is disabled"
+        (mt/user-http-request :crowberto :put 200 "ldap/settings"
+                              (assoc (ldap-test-details) :ldap-password "wrong-password" :ldap-enabled false)))
 
-        (testing "Invalid LDAP settings can be saved if LDAP is disabled"
-          (mt/user-http-request :crowberto :put 200 "ldap/settings"
-                                (assoc ldap-test-details :ldap-password "wrong-password" :ldap-enabled false)))
+      (testing "Valid LDAP settings can still be saved if port is a integer (#18936)"
+        (mt/user-http-request :crowberto :put 200 "ldap/settings"
+                              (assoc (ldap-test-details)
+                                     :ldap-port (Integer. (ldap.test/get-ldap-port)))))
 
-        (testing "Valid LDAP settings can still be saved if port is a integer (#18936)"
-          (mt/user-http-request :crowberto :put 200 "ldap/settings"
-                                (assoc ldap-test-details
-                                       :ldap-port (Integer. (ldap.test/get-ldap-port)))))
+      (testing "LDAP port is saved as default value if passed as an empty string (#18936)"
+        (mt/user-http-request :crowberto :put 200 "ldap/settings"
+                              (assoc (ldap-test-details) :ldap-port "" :ldap-enabled false))
+        (is (= 389 (ldap/ldap-port))))
 
-        (testing "LDAP port is saved as default value if passed as an empty string (#18936)"
-          (mt/user-http-request :crowberto :put 200 "ldap/settings"
-                                (assoc ldap-test-details :ldap-port "" :ldap-enabled false))
-          (is (= 389 (ldap/ldap-port))))))))
+      (testing "Requires superusers"
+        (is (= "You don't have permissions to do that."
+               (mt/user-http-request :rasta :put 403 "ldap/settings"
+                                     (assoc (ldap-test-details) :ldap-port "" :ldap-enabled false))))))))
diff --git a/test/metabase/api/permissions_test.clj b/test/metabase/api/permissions_test.clj
index 361528622c0..371df8e6b8a 100644
--- a/test/metabase/api/permissions_test.clj
+++ b/test/metabase/api/permissions_test.clj
@@ -36,7 +36,7 @@
                 (is (schema= {:id           (s/eq (:id (group/admin)))
                               :name         (s/eq "Administrators")
                               :member_count su/IntGreaterThanZero}
-                       (get id->group (:id (group/admin)))))))]
+                             (get id->group (:id (group/admin)))))))]
       (let [id->group (u/key-by :id (fetch-groups))]
         (check-default-groups-returned id->group))
 
@@ -48,7 +48,11 @@
               (is (schema= {:id           su/IntGreaterThanZero
                             :name         su/NonBlankString
                             :member_count (s/eq 0)}
-                           (get id->group (:id group)))))))))))
+                           (get id->group (:id group))))))))
+
+      (testing "require superusers"
+        (is (= "You don't have permissions to do that."
+               (mt/user-http-request :rasta :get 403 "permissions/group")))))))
 
 (deftest groups-list-limit-test
   (testing "GET /api/permissions/group?limit=1&offset=1"
diff --git a/test/metabase/api/premium_features_test.clj b/test/metabase/api/premium_features_test.clj
new file mode 100644
index 00000000000..e13f085bf3b
--- /dev/null
+++ b/test/metabase/api/premium_features_test.clj
@@ -0,0 +1,24 @@
+(ns metabase.api.premium-features-test
+  (:require [clojure.test :refer :all]
+            [metabase.public-settings.premium-features :as premium-features]
+            [metabase.public-settings.premium-features-test :as premium-features-test]
+            [metabase.test :as mt]))
+
+(deftest get-token-status-test
+  (testing "GET /api/premium-features/token/status"
+    (with-redefs [premium-features/fetch-token-status (fn [_x]
+                                                        {:valid    true
+                                                         :status   "fake"
+                                                         :features ["test" "fixture"]
+                                                         :trial    false})]
+      (mt/with-temporary-setting-values [:premium-embedding-token premium-features-test/random-fake-token]
+        (testing "returns correctly"
+          (is (= {:valid    true
+                  :status   "fake"
+                  :features ["test" "fixture"]
+                  :trial    false}
+                 (mt/user-http-request :crowberto :get 200 "premium-features/token/status"))))
+
+        (testing "requires superusers"
+          (is (= "You don't have permissions to do that."
+                 (mt/user-http-request :rasta :get 403 "premium-features/token/status"))))))))
diff --git a/test/metabase/api/setup_test.clj b/test/metabase/api/setup_test.clj
index 9b4c645d8ac..2e0d10bb311 100644
--- a/test/metabase/api/setup_test.clj
+++ b/test/metabase/api/setup_test.clj
@@ -391,49 +391,54 @@
 
 ;; basic sanity check
 (deftest admin-checklist-test
-  (with-redefs [db/exists?              (constantly true)
-                db/count                (constantly 5)
-                email/email-configured? (constantly true)
-                slack/slack-configured? (constantly false)]
-    (is (= [{:name  "Get connected"
-             :tasks [{:title        "Add a database"
-                      :completed    true
-                      :triggered    true
-                      :is_next_step false}
-                     {:title        "Set up email"
-                      :completed    true
-                      :triggered    true
-                      :is_next_step false}
-                     {:title        "Set Slack credentials"
-                      :completed    false
-                      :triggered    true
-                      :is_next_step true}
-                     {:title        "Invite team members"
-                      :completed    true
-                      :triggered    true
-                      :is_next_step false}]}
-            {:name  "Curate your data"
-             :tasks [{:title        "Hide irrelevant tables"
-                      :completed    true
-                      :triggered    false
-                      :is_next_step false}
-                     {:title        "Organize questions"
-                      :completed    true
-                      :triggered    false
-                      :is_next_step false}
-                     {:title        "Create metrics"
-                      :completed    true
-                      :triggered    false
-                      :is_next_step false}
-                     {:title        "Create segments"
-                      :completed    true
-                      :triggered    false
-                      :is_next_step false}]}]
-           (for [{group-name :name, tasks :tasks} (#'setup-api/admin-checklist)]
-             {:name  (str group-name)
-              :tasks (for [task tasks]
-                       (-> (select-keys task [:title :completed :triggered :is_next_step])
-                           (update :title str)))})))))
+  (testing "GET /api/setup/admin_checklist"
+    (with-redefs [db/exists?              (constantly true)
+                  db/count                (constantly 5)
+                  email/email-configured? (constantly true)
+                  slack/slack-configured? (constantly false)]
+      (is (= [{:name  "Get connected"
+               :tasks [{:title        "Add a database"
+                        :completed    true
+                        :triggered    true
+                        :is_next_step false}
+                       {:title        "Set up email"
+                        :completed    true
+                        :triggered    true
+                        :is_next_step false}
+                       {:title        "Set Slack credentials"
+                        :completed    false
+                        :triggered    true
+                        :is_next_step true}
+                       {:title        "Invite team members"
+                        :completed    true
+                        :triggered    true
+                        :is_next_step false}]}
+              {:name  "Curate your data"
+               :tasks [{:title        "Hide irrelevant tables"
+                        :completed    true
+                        :triggered    false
+                        :is_next_step false}
+                       {:title        "Organize questions"
+                        :completed    true
+                        :triggered    false
+                        :is_next_step false}
+                       {:title        "Create metrics"
+                        :completed    true
+                        :triggered    false
+                        :is_next_step false}
+                       {:title        "Create segments"
+                        :completed    true
+                        :triggered    false
+                        :is_next_step false}]}]
+             (for [{group-name :name, tasks :tasks} (mt/user-http-request :crowberto :get 200 "setup/admin_checklist")]
+               {:name  (str group-name)
+                :tasks (for [task tasks]
+                         (-> (select-keys task [:title :completed :triggered :is_next_step])
+                             (update :title str)))}))))
+
+    (testing "require superusers"
+      (is (= "You don't have permissions to do that."
+             (mt/user-http-request :rasta :get 403 "setup/admin_checklist"))))))
 
 (deftest user-defaults-test
   (testing "with no user defaults configured"
diff --git a/test/metabase/public_settings/premium_features_test.clj b/test/metabase/public_settings/premium_features_test.clj
index da76f52c34e..926006f9624 100644
--- a/test/metabase/public_settings/premium_features_test.clj
+++ b/test/metabase/public_settings/premium_features_test.clj
@@ -43,7 +43,7 @@
                 :features ["test" "fixture"]
                 :trial    false}))
 
-(def ^:private random-fake-token
+(def random-fake-token
   "d7ad0b5f9ddfd1953b1b427b75d620e4ba91d38e7bcbc09d8982480863dbc611")
 
 (deftest fetch-token-status-test
-- 
GitLab