You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ja...@apache.org on 2022/01/19 14:46:37 UTC

[solr] branch branch_9x updated: SOLR-15776 Make Admin UI play well with Authorization (#399)

This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 20bcad7  SOLR-15776 Make Admin UI play well with Authorization (#399)
20bcad7 is described below

commit 20bcad761fb8a830e58c0894ac66d8645573540e
Author: Jan Høydahl <ja...@users.noreply.github.com>
AuthorDate: Wed Jan 19 15:38:01 2022 +0100

    SOLR-15776 Make Admin UI play well with Authorization (#399)
    
    (cherry picked from commit a1406847ba256965e636e05b11e53c0ac696e2a9)
    
    # Conflicts:
    #	solr/CHANGES.txt
---
 solr/CHANGES.txt                                   |  2 +
 .../solr/handler/admin/SystemInfoHandler.java      |  2 +-
 .../security/RuleBasedAuthorizationPluginBase.java | 18 +++++++
 .../BaseTestRuleBasedAuthorizationPlugin.java      | 19 ++++++--
 solr/webapp/web/index.html                         |  1 +
 solr/webapp/web/js/angular/app.js                  | 22 +++++----
 solr/webapp/web/js/angular/controllers/cloud.js    | 28 ++++++-----
 solr/webapp/web/js/angular/controllers/logging.js  |  6 +++
 solr/webapp/web/js/angular/controllers/security.js |  2 +-
 solr/webapp/web/js/angular/permissions.js          | 56 ++++++++++++++++++++++
 solr/webapp/web/partials/collections.html          |  4 +-
 11 files changed, 133 insertions(+), 27 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index d347b03..7f13a02 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -28,6 +28,8 @@ Other Changes
 ---------------------
 * SOLR-15897: Remove <jmx/> from all unit test solrconfig.xml files. (Eric Pugh)
 
+* SOLR-15776: Admin UI is now aware of logged-in user's permissions and can adapt accordingly (janhoy)
+
 ==================  9.0.0 ==================
 
 Docker and contrib modules have separate CHANGES.md files.
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
index 87c5d48..7589e8d 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
@@ -65,7 +65,6 @@ import static org.apache.solr.common.params.CommonParams.NAME;
 public class SystemInfoHandler extends RequestHandlerBase 
 {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-  private static final String PARAM_NODE = "node";
 
   public static String REDACT_STRING = RedactionUtils.getRedactString();
 
@@ -354,6 +353,7 @@ public class SystemInfoHandler extends RequestHandlerBase
         RuleBasedAuthorizationPluginBase rbap = (RuleBasedAuthorizationPluginBase) auth;
         Set<String> roles = rbap.getUserRoles(req.getUserPrincipal());
         info.add("roles", roles);
+        info.add("permissions", rbap.getPermissionNamesForRoles(roles));
       }
     }
 
diff --git a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java
index cdcee91..9045de6 100644
--- a/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java
+++ b/solr/core/src/java/org/apache/solr/security/RuleBasedAuthorizationPluginBase.java
@@ -27,6 +27,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import org.apache.solr.common.SpecProvider;
 import org.apache.solr.common.util.Utils;
@@ -46,6 +47,7 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   private final Map<String, WildCardSupportMap> mapping = new HashMap<>();
+  private final Map<String, Set<Permission>> roleToPermissionsMap = new HashMap<>();
 
   // Doesn't implement Map because we violate the contracts of put() and get()
   private static class WildCardSupportMap {
@@ -110,6 +112,16 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
     return flag.rsp;
   }
 
+  /**
+   * Retrieves permission names for a given set of roles
+   */
+  public Set<String> getPermissionNamesForRoles(Set<String> roles) {
+    return roles.stream().filter(roleToPermissionsMap::containsKey)
+        .flatMap(r -> roleToPermissionsMap.get(r).stream())
+        .map(p -> p.name)
+        .collect(Collectors.toSet());
+  }
+
   private MatchStatus checkCollPerm(WildCardSupportMap pathVsPerms, AuthorizationContext context) {
     if (pathVsPerms == null) return MatchStatus.NO_PERMISSIONS_FOUND;
 
@@ -286,6 +298,12 @@ public abstract class RuleBasedAuthorizationPluginBase implements AuthorizationP
         perms.add(permission);
       }
     }
+    if (permission.role != null) {
+      for (String r : permission.role) {
+        Set<Permission> rm = roleToPermissionsMap.computeIfAbsent(r, k -> new HashSet<>());
+        rm.add(permission);
+      }
+    }
   }
 
   /**
diff --git a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java
index 57c2001..f1598ac 100644
--- a/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java
+++ b/solr/core/src/test/org/apache/solr/security/BaseTestRuleBasedAuthorizationPlugin.java
@@ -25,6 +25,7 @@ import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.http.auth.BasicUserPrincipal;
 import org.apache.solr.SolrTestCaseJ4;
@@ -48,9 +49,7 @@ import org.hamcrest.core.IsInstanceOf;
 import org.junit.Before;
 import org.junit.Test;
 
-import static java.util.Collections.emptyMap;
-import static java.util.Collections.singletonList;
-import static java.util.Collections.singletonMap;
+import static java.util.Collections.*;
 import static org.apache.solr.common.util.CommandOperation.captureErrors;
 import static org.apache.solr.common.util.Utils.getObjectByPath;
 import static org.hamcrest.CoreMatchers.instanceOf;
@@ -449,6 +448,20 @@ public class BaseTestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
     checkRules(values, STATUS_OK);
   }
 
+  @Test
+  public void testGetPermissionNamesForRoles() {
+    // Tests the method that maps role(s) to permissions, used by SystemInfoHandler to provide UI with logged in user's permissions
+    try (RuleBasedAuthorizationPluginBase plugin = createPlugin()) {
+      plugin.init(rules);
+      assertEquals(Set.of("mycoll_update", "read"), plugin.getPermissionNamesForRoles(Set.of("dev")));
+      assertEquals(emptySet(), plugin.getPermissionNamesForRoles(Set.of("user")));
+      assertEquals(Set.of("schema-edit", "collection-admin-edit", "mycoll_update"), plugin.getPermissionNamesForRoles(Set.of("admin")));
+      assertEquals(Set.of("schema-edit", "collection-admin-edit", "mycoll_update", "read"), plugin.getPermissionNamesForRoles(Set.of("admin", "dev")));
+    } catch (IOException e) {
+      ; // swallow error, otherwise a you have to add a _lot_ of exceptions to methods.
+    }
+  }
+
   void addPermission(String permissionName, String role, String path, Map<String, Object> params) {
     ((List)rules.get("permissions")).add(Map.of("name", permissionName, "role", role, "path", path, "params", params));
   }
diff --git a/solr/webapp/web/index.html b/solr/webapp/web/index.html
index 64a5f8e..8fce6df 100644
--- a/solr/webapp/web/index.html
+++ b/solr/webapp/web/index.html
@@ -71,6 +71,7 @@ limitations under the License.
   <script src="libs/angular-utf8-base64.min.js?_=${version}"></script>
   <script src="js/angular/app.js?_=${version}"></script>
   <script src="js/angular/services.js?_=${version}"></script>
+  <script src="js/angular/permissions.js?_=${version}"></script>
   <script src="js/angular/controllers/index.js?_=${version}"></script>
   <script src="js/angular/controllers/login.js?_=${version}"></script>
   <script src="js/angular/controllers/logging.js?_=${version}"></script>
diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js
index 437a7e2..d0de77a 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -492,6 +492,11 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
       $scope.aliases = [];
   }
 
+  $scope.permissions = permissions;
+  $scope.isPermitted = function (permissions) {
+    return hasAllRequiredPermissions(permissions, $scope.usersPermissions);
+  }
+
   $scope.refresh();
   $scope.resetMenu = function(page, pageType) {
     Cores.list(function(data) {
@@ -512,9 +517,16 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
       $scope.initFailures = data.initFailures;
     });
 
-    $scope.isSchemaDesignerEnabled = true;
     System.get(function(data) {
       $scope.isCloudEnabled = data.mode.match( /solrcloud/i );
+      $scope.usersPermissions = data.security.permissions;
+
+      $scope.isSchemaDesignerEnabled = $scope.isPermitted([
+        permissions.CONFIG_EDIT_PERM,
+        permissions.SCHEMA_EDIT_PERM,
+        permissions.READ_PERM,
+        permissions.UPDATE_PERM
+      ]);
 
       var currentCollectionName = $route.current.params.core;
       delete $scope.currentCollection;
@@ -550,14 +562,6 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
               $scope.aliases_and_collections = $scope.aliases_and_collections.concat({name:'-----'});
             }
             $scope.aliases_and_collections = $scope.aliases_and_collections.concat($scope.collections);
-
-            SchemaDesigner.get({path: "configs"}, function (ignore) {
-              // no-op, just checking if we have access to this path
-            }, function(e) {
-              if (e.status === 401 || e.status === 403) {
-                $scope.isSchemaDesignerEnabled = false;
-              }
-            });
           });
         });
       }
diff --git a/solr/webapp/web/js/angular/controllers/cloud.js b/solr/webapp/web/js/angular/controllers/cloud.js
index 2d62989..2769b3a 100644
--- a/solr/webapp/web/js/angular/controllers/cloud.js
+++ b/solr/webapp/web/js/angular/controllers/cloud.js
@@ -560,17 +560,23 @@ var treeSubController = function($scope, Zookeeper) {
 
     $scope.showTreeLink = function(link) {
         var path = decodeURIComponent(link.replace(/.*[\\?&]path=([^&#]*).*/, "$1"));
-        Zookeeper.detail({path: path}, function(data) {
-            $scope.znode = data.znode;
-            if (data.znode.path.endsWith("/managed-schema") || data.znode.path.endsWith(".xml.bak")) {
-              $scope.lang = "xml";
-            } else {
-              var lastPathElement = data.znode.path.split( '/' ).pop();
-              var lastDotAt = lastPathElement ? lastPathElement.lastIndexOf('.') : -1;
-              $scope.lang = lastDotAt != -1 ? lastPathElement.substring(lastDotAt+1) : "txt";
-            }
-            $scope.showData = true;
-        });
+        if (path === '/security.json' && !$scope.isPermitted(permissions.SECURITY_READ_PERM)) {
+          // TODO: Set proper data here to display a warning in right panel "You lack the required role to see this file"
+          $scope.znode = {};
+          $scope.showData = false;
+        } else {
+          Zookeeper.detail({path: path}, function(data) {
+              $scope.znode = data.znode;
+              if (data.znode.path.endsWith("/managed-schema") || data.znode.path.endsWith(".xml.bak")) {
+                $scope.lang = "xml";
+              } else {
+                var lastPathElement = data.znode.path.split( '/' ).pop();
+                var lastDotAt = lastPathElement ? lastPathElement.lastIndexOf('.') : -1;
+                $scope.lang = lastDotAt != -1 ? lastPathElement.substring(lastDotAt+1) : "txt";
+              }
+              $scope.showData = true;
+          });
+        }
     };
 
     $scope.hideData = function() {
diff --git a/solr/webapp/web/js/angular/controllers/logging.js b/solr/webapp/web/js/angular/controllers/logging.js
index 585c90a..6b38afe 100644
--- a/solr/webapp/web/js/angular/controllers/logging.js
+++ b/solr/webapp/web/js/angular/controllers/logging.js
@@ -134,6 +134,9 @@ solrAdminApp.controller('LoggingController',
     };
 
     $scope.toggleOptions = function(logger) {
+      if (!$scope.isPermitted(permissions.CONFIG_EDIT_PERM)) {
+        return;
+      }
       if (logger.showOptions) {
         logger.showOptions = false;
         delete $scope.currentLogger;
@@ -147,6 +150,9 @@ solrAdminApp.controller('LoggingController',
     };
 
     $scope.setLevel = function(logger, newLevel) {
+      if (!$scope.isPermitted(permissions.CONFIG_EDIT_PERM)) {
+        return;
+      }
       var setString = logger.name + ":" + newLevel;
       logger.showOptions = false;
       Logging.setLevel({set: setString}, function(data) {
diff --git a/solr/webapp/web/js/angular/controllers/security.js b/solr/webapp/web/js/angular/controllers/security.js
index c95469c..6c7223d 100644
--- a/solr/webapp/web/js/angular/controllers/security.js
+++ b/solr/webapp/web/js/angular/controllers/security.js
@@ -142,7 +142,7 @@ solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cooki
     return (!obj || (Array.isArray(obj) && obj.length === 0)) ? "null" : $scope.displayList(obj);
   };
 
-  // TODO: Read this list from Solr to avoid duplication
+  // TODO: Use new permissions.js
   $scope.predefinedPermissions = ["collection-admin-edit", "collection-admin-read", "core-admin-read", "core-admin-edit", "zk-read",
     "read", "update", "all", "config-edit", "config-read", "schema-read", "schema-edit", "security-edit", "security-read",
     "metrics-read", "health", "filestore-read", "filestore-write", "package-edit", "package-read"].sort();
diff --git a/solr/webapp/web/js/angular/permissions.js b/solr/webapp/web/js/angular/permissions.js
new file mode 100644
index 0000000..2c28a49
--- /dev/null
+++ b/solr/webapp/web/js/angular/permissions.js
@@ -0,0 +1,56 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+const permissions = {
+  COLL_EDIT_PERM: "collection-admin-edit",
+  COLL_READ_PERM: "collection-admin-read",
+  CORE_READ_PERM: "core-admin-read",
+  CORE_EDIT_PERM: "core-admin-edit",
+  ZK_READ_PERM: "zk-read",
+  READ_PERM: "read",
+  UPDATE_PERM: "update",
+  CONFIG_EDIT_PERM: "config-edit",
+  CONFIG_READ_PERM: "config-read",
+  SCHEMA_READ_PERM: "schema-read",
+  SCHEMA_EDIT_PERM: "schema-edit",
+  SECURITY_EDIT_PERM: "security-edit",
+  SECURITY_READ_PERM: "security-read",
+  METRICS_READ_PERM: "metrics-read",
+  FILESTORE_READ_PERM: "filestore-read",
+  FILESTORE_WRITE_PERM: "filestore-write",
+  PACKAGE_EDIT_PERM: "package-edit",
+  PACKAGE_READ_PERM: "package-read",
+  ALL_PERM: "all"
+}
+
+/**
+ * Returns true if all required permissions are available. Also returns true if RBAC is not enabled
+ * @param requiredPermissions the permission(s) to check for, can be a single or array
+ * @param userPermissions the actual permissions of current user
+ * @returns {boolean}
+ */
+var hasAllRequiredPermissions = function (requiredPermissions, userPermissions) {
+  if (!Array.isArray(requiredPermissions)) {
+    requiredPermissions = [requiredPermissions];
+  }
+  if (userPermissions !== undefined) {
+    return requiredPermissions.every(elem => userPermissions.indexOf(elem) > -1);
+  } else {
+    // RBAC not enabled, always return true
+    return true;
+  }
+}
\ No newline at end of file
diff --git a/solr/webapp/web/partials/collections.html b/solr/webapp/web/partials/collections.html
index 59104f0..b4d4c2b 100644
--- a/solr/webapp/web/partials/collections.html
+++ b/solr/webapp/web/partials/collections.html
@@ -61,7 +61,7 @@ limitations under the License.
         </p>
 
         <p class="clearfix buttons">
-          <button type="submit" class="submit" ng-click="addCollection()"><span>Add Collection</span></button>
+          <button type="submit" class="submit" ng-disabled="!isPermitted(permissions.COLL_EDIT_PERM)" ng-click="addCollection()"><span>Add Collection</span></button>
           <button type="reset" class="reset" ng-click="cancelAddCollection()"><span>Cancel</span></button>
         </p>
 
@@ -372,7 +372,7 @@ limitations under the License.
   </div>
 
   <div id="navigation" class="requires-core clearfix">
-    <button id="add" class="action" ng-click="showAddCollection()"><span>Add Collection</span></button>
+    <button id="add" class="action" ng-disabled="!isPermitted(permissions.COLL_EDIT_PERM)" ng-click="showAddCollection()"><span>Add Collection</span></button>
     <ul>
       <li ng-repeat="c in collections" ng-class="{current: collection.name == c.name && collection.type === 'collection'}"><a href="#~collections/{{c.name}}">{{c.name}}</a></li>
     </ul>