Skip to content

Commit

Permalink
SOLR-15776 Make Admin UI play well with Authorization (#399)
Browse files Browse the repository at this point in the history
  • Loading branch information
janhoy committed Jan 19, 2022
1 parent aa8e3c6 commit a140684
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 28 deletions.
2 changes: 1 addition & 1 deletion solr/CHANGES.txt
Expand Up @@ -50,7 +50,7 @@ Bug Fixes

Other Changes
---------------------
(No changes)
* SOLR-15776: Admin UI is now aware of logged-in user's permissions and can adapt accordingly (janhoy)

================== 9.0.0 ==================

Expand Down
Expand Up @@ -65,7 +65,6 @@
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();

Expand Down Expand Up @@ -354,6 +353,7 @@ public SimpleOrderedMap<Object> getSecurityInfo(SolrQueryRequest req)
RuleBasedAuthorizationPluginBase rbap = (RuleBasedAuthorizationPluginBase) auth;
Set<String> roles = rbap.getUserRoles(req.getUserPrincipal());
info.add("roles", roles);
info.add("permissions", rbap.getPermissionNamesForRoles(roles));
}
}

Expand Down
Expand Up @@ -27,6 +27,7 @@
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;
Expand All @@ -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 {
Expand Down Expand Up @@ -110,6 +112,16 @@ public AuthorizationResponse authorize(AuthorizationContext context) {
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;

Expand Down Expand Up @@ -286,6 +298,12 @@ private void add2Mapping(Permission permission) {
perms.add(permission);
}
}
if (permission.role != null) {
for (String r : permission.role) {
Set<Permission> rm = roleToPermissionsMap.computeIfAbsent(r, k -> new HashSet<>());
rm.add(permission);
}
}
}

/**
Expand Down
Expand Up @@ -25,6 +25,7 @@
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;
Expand All @@ -48,9 +49,7 @@
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;
Expand Down Expand Up @@ -449,6 +448,20 @@ public void testShortNameResolvesPermissions() {
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));
}
Expand Down
1 change: 1 addition & 0 deletions solr/webapp/web/index.html
Expand Up @@ -71,6 +71,7 @@
<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>
Expand Down
22 changes: 13 additions & 9 deletions solr/webapp/web/js/angular/app.js
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
});
});
});
}
Expand Down
28 changes: 17 additions & 11 deletions solr/webapp/web/js/angular/controllers/cloud.js
Expand Up @@ -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() {
Expand Down
6 changes: 6 additions & 0 deletions solr/webapp/web/js/angular/controllers/logging.js
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion solr/webapp/web/js/angular/controllers/security.js
Expand Up @@ -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();
Expand Down
56 changes: 56 additions & 0 deletions 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;
}
}
4 changes: 2 additions & 2 deletions solr/webapp/web/partials/collections.html
Expand Up @@ -61,7 +61,7 @@
</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>

Expand Down Expand Up @@ -372,7 +372,7 @@ <h2>
</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>
Expand Down

0 comments on commit a140684

Please sign in to comment.