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

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


The following commit(s) were added to refs/heads/main by this push:
     new 2458360  SOLR-15527: Security admin screen for managing users, roles, 
and permissions (#209)
2458360 is described below

commit 245836005303df7bf3ff7f987595a6a3be23baf1
Author: Timothy Potter <thelabd...@gmail.com>
AuthorDate: Fri Jul 30 10:28:36 2021 -0600

    SOLR-15527: Security admin screen for managing users, roles, and 
permissions (#209)
---
 solr/CHANGES.txt                                   |    2 +
 .../solr/handler/admin/SystemInfoHandler.java      |    3 +
 .../src/java/org/apache/solr/util/SolrCLI.java     |   11 +-
 .../src/basic-authentication-plugin.adoc           |    2 +
 .../src/images/security-ui/add-permission.png      |  Bin 0 -> 112235 bytes
 .../src/images/security-ui/edit-user-dialog.png    |  Bin 0 -> 74147 bytes
 .../src/images/security-ui/filter-users.png        |  Bin 0 -> 24808 bytes
 .../src/images/security-ui/permissions.png         |  Bin 0 -> 117684 bytes
 .../src/images/security-ui/roles.png               |  Bin 0 -> 41003 bytes
 .../security-ui/security-not-enabled-warn.png      |  Bin 0 -> 112740 bytes
 .../src/images/security-ui/users.png               |  Bin 0 -> 39378 bytes
 .../src/images/solr-admin-ui/security.png          |  Bin 0 -> 294588 bytes
 solr/solr-ref-guide/src/securing-solr.adoc         |    5 +-
 solr/solr-ref-guide/src/security-ui.adoc           |  109 ++
 solr/solr-ref-guide/src/solr-admin-ui.adoc         |   10 +-
 solr/webapp/web/css/angular/menu.css               |    3 +-
 solr/webapp/web/css/angular/security.css           |  678 ++++++++++++
 solr/webapp/web/img/ico/key.png                    |  Bin 0 -> 689 bytes
 solr/webapp/web/img/ico/keyplus.png                |  Bin 0 -> 812 bytes
 solr/webapp/web/img/ico/lock.png                   |  Bin 0 -> 1535 bytes
 solr/webapp/web/img/ico/lockplus.png               |  Bin 0 -> 1640 bytes
 solr/webapp/web/img/ico/logout.png                 |  Bin 0 -> 660 bytes
 solr/webapp/web/img/ico/shield--exclamation.png    |  Bin 0 -> 813 bytes
 solr/webapp/web/img/ico/shield.png                 |  Bin 0 -> 782 bytes
 solr/webapp/web/img/ico/useradd.png                |  Bin 0 -> 785 bytes
 solr/webapp/web/index.html                         |    4 +
 solr/webapp/web/js/angular/app.js                  |    6 +-
 solr/webapp/web/js/angular/controllers/security.js | 1123 ++++++++++++++++++++
 solr/webapp/web/js/angular/services.js             |    6 +
 solr/webapp/web/partials/security.html             |  285 +++++
 30 files changed, 2241 insertions(+), 6 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 4c0f888..b58f1f4 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -359,6 +359,8 @@ New Features
 
 * SOLR-15208: Add the countDist aggregation to the stats, facet and timeseries 
Streaming Expressions (Joel Bernstein)
 
+* SOLR-15527: Security screen in Admin UI for managing users, roles, and 
permissions (Timothy Potter)
+
 Improvements
 ---------------------
 * SOLR-15460: Implement LIKE, IS NOT NULL, IS NULL, and support wildcard * in 
equals string literal for Parallel SQL (Timothy Potter, Houston Putman)
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 3ecb4fd..5a3c7b9 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
@@ -33,6 +33,7 @@ import java.util.Set;
 
 import com.codahale.metrics.Gauge;
 import org.apache.lucene.LucenePackage;
+import org.apache.solr.common.cloud.UrlScheme;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.SolrCore;
@@ -351,6 +352,8 @@ public class SystemInfoHandler extends RequestHandlerBase
       }
     }
 
+    info.add("tls", UrlScheme.HTTPS.equals(UrlScheme.INSTANCE.getUrlScheme()));
+
     return info;
   }
 
diff --git a/solr/core/src/java/org/apache/solr/util/SolrCLI.java 
b/solr/core/src/java/org/apache/solr/util/SolrCLI.java
index 5f30825..63fb737 100755
--- a/solr/core/src/java/org/apache/solr/util/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/util/SolrCLI.java
@@ -3977,8 +3977,15 @@ public class SolrCLI implements CLIO {
             password = credentials.split(":")[1];
           } else {
             Console console = System.console();
-            username = console.readLine("Enter username: ");
-            password = new String(console.readPassword("Enter password: "));
+            // keep prompting until they've entered a non-empty username & 
password
+            do {
+              username = console.readLine("Enter username: ");
+            } while (username == null || username.trim().length() == 0);
+            username = username.trim();
+
+            do {
+              password = new String(console.readPassword("Enter password: "));
+            } while (password.length() == 0);
           }
 
           boolean blockUnknown = 
Boolean.valueOf(cli.getOptionValue("blockUnknown", "true"));
diff --git a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc 
b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
index 5751f67..e3ba25e 100644
--- a/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/basic-authentication-plugin.adoc
@@ -26,6 +26,8 @@ To control user permissions, you may need to configure an 
authorization plugin a
 To use Basic authentication, you must first create a `security.json` file.
 This file and where to put it is described in detail in the section 
<<authentication-and-authorization-plugins.adoc#configuring-security-json,Configuring
 security.json>>.
 
+If running in cloud mode, you can use the `bin/solr auth` command-line utility 
to enable security for a new installation, see: `bin/solr auth --help` for more 
details.
+
 For Basic authentication, `security.json` must have an `authentication` block 
which defines the class being used for authentication.
 Usernames and passwords (as a sha256(password+salt) hash) could be added when 
the file is created, or can be added later with the Authentication API, 
described below.
 
diff --git a/solr/solr-ref-guide/src/images/security-ui/add-permission.png 
b/solr/solr-ref-guide/src/images/security-ui/add-permission.png
new file mode 100644
index 0000000..826022e
Binary files /dev/null and 
b/solr/solr-ref-guide/src/images/security-ui/add-permission.png differ
diff --git a/solr/solr-ref-guide/src/images/security-ui/edit-user-dialog.png 
b/solr/solr-ref-guide/src/images/security-ui/edit-user-dialog.png
new file mode 100644
index 0000000..5812215
Binary files /dev/null and 
b/solr/solr-ref-guide/src/images/security-ui/edit-user-dialog.png differ
diff --git a/solr/solr-ref-guide/src/images/security-ui/filter-users.png 
b/solr/solr-ref-guide/src/images/security-ui/filter-users.png
new file mode 100644
index 0000000..e2cc0ad
Binary files /dev/null and 
b/solr/solr-ref-guide/src/images/security-ui/filter-users.png differ
diff --git a/solr/solr-ref-guide/src/images/security-ui/permissions.png 
b/solr/solr-ref-guide/src/images/security-ui/permissions.png
new file mode 100644
index 0000000..9e9f447
Binary files /dev/null and 
b/solr/solr-ref-guide/src/images/security-ui/permissions.png differ
diff --git a/solr/solr-ref-guide/src/images/security-ui/roles.png 
b/solr/solr-ref-guide/src/images/security-ui/roles.png
new file mode 100644
index 0000000..ff6b66e
Binary files /dev/null and 
b/solr/solr-ref-guide/src/images/security-ui/roles.png differ
diff --git 
a/solr/solr-ref-guide/src/images/security-ui/security-not-enabled-warn.png 
b/solr/solr-ref-guide/src/images/security-ui/security-not-enabled-warn.png
new file mode 100644
index 0000000..ef913d5
Binary files /dev/null and 
b/solr/solr-ref-guide/src/images/security-ui/security-not-enabled-warn.png 
differ
diff --git a/solr/solr-ref-guide/src/images/security-ui/users.png 
b/solr/solr-ref-guide/src/images/security-ui/users.png
new file mode 100644
index 0000000..398bebb
Binary files /dev/null and 
b/solr/solr-ref-guide/src/images/security-ui/users.png differ
diff --git a/solr/solr-ref-guide/src/images/solr-admin-ui/security.png 
b/solr/solr-ref-guide/src/images/solr-admin-ui/security.png
new file mode 100644
index 0000000..49e4672
Binary files /dev/null and 
b/solr/solr-ref-guide/src/images/solr-admin-ui/security.png differ
diff --git a/solr/solr-ref-guide/src/securing-solr.adoc 
b/solr/solr-ref-guide/src/securing-solr.adoc
index 2499fc1..31fd7d5 100644
--- a/solr/solr-ref-guide/src/securing-solr.adoc
+++ b/solr/solr-ref-guide/src/securing-solr.adoc
@@ -2,7 +2,8 @@
 :page-children: authentication-and-authorization-plugins, \
     audit-logging, \
     enabling-ssl, \
-    zookeeper-access-control
+    zookeeper-access-control, \
+    security-ui
 // 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
@@ -39,6 +40,8 @@ See the section <<enabling-ssl.adoc#,Enabling TLS (SSL)>> for 
details.
 
 == Authentication and Authorization
 
+Use the <<security-ui.adoc#,Security>> screen in the Admin UI to manage users, 
roles, and permissions.
+
 See chapter <<authentication-and-authorization-plugins.adoc#,Configuring 
Authentication and Authorization>> to learn how to work with the 
`security.json` file.
 
 [#securing-solr-auth-plugins]
diff --git a/solr/solr-ref-guide/src/security-ui.adoc 
b/solr/solr-ref-guide/src/security-ui.adoc
new file mode 100644
index 0000000..1f8363b
--- /dev/null
+++ b/solr/solr-ref-guide/src/security-ui.adoc
@@ -0,0 +1,109 @@
+= Security UI
+:experimental:
+// 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.
+
+The Security screen allows administrators with the `security-edit` permission 
to manage users, roles, and permissions.
+The Security screen works with Solr running in cloud and standalone modes.
+
+.Security Screen
+image::images/solr-admin-ui/security.png[]
+
+== Getting Started
+
+The Security screen warns you if security is not enabled for Solr. You are 
strongly encouraged to enable security for Solr instances exposed on any 
network other than localhost.
+
+image::images/security-ui/security-not-enabled-warn.png[image,width=500]
+
+When first getting started with Solr, use the `bin/solr auth` command-line 
utility to enable security for your Solr installation (cloud mode only), see 
<<solr-control-script-reference.adoc#authentication,bin/solr auth>> for usage 
instructions.
+For example, the following command will enable *basic authentication* and 
prompt you for the username and password for the initial user with 
administrative access:
+[source,bash]
+----
+ bin/solr auth enable -type basicAuth -prompt true -z localhost:2181
+----
+_Note: The `auth` utility only works with Solr running in cloud mode and thus 
requires a Zookeeper connection string passed via the `-z` option._
+
+After enabling security, you'll need to refresh the Admin UI and login with 
the credentials you provided to the `auth` utility to see the updated Security 
panel.
+You do not need to restart Solr as the security configuration will be 
refreshed from Zookeeper automatically.
+
+The Security screen provides the following features:
+
+* Security Settings: Details about the configured authentication and 
authorization plugins.
+* Users: Read, create, update, and delete user accounts if using the 
<<basic-authentication-plugin.adoc#,Basic Authentication>> plugin; this panel 
is disabled for all other authentication plugins.
+* Roles: Read, create, and update roles if using the 
<<rule-based-authorization-plugin.adoc#,Rule-based Authorization>> plugin; this 
panel is disabled for all other authorization plugins.
+* Permissions: Read, create, update, and delete permissions if using the 
<<rule-based-authorization-plugin.adoc#,Rule-based Authorization>> plugin.
+
+== User Management
+
+Administrators can read, create, update, and delete user accounts when using 
the <<basic-authentication-plugin.adoc#,Basic Authentication>> plugin.
+
+image::images/security-ui/users.png[image,width=500]
+
+.Limited User Management Capabilities
+[NOTE]
+====
+Solr's user management is intended to be used by administrators to grant 
access to protected APIs and lacks common user account management facilities, 
like password expiration and password self-service (change / reset / recovery).
+Consequently, if a user account has been compromised, then an administrator 
needs to change the password or disable that account using the UI or API.
+====
+
+To edit a user account, click on the row in the table to open the edit user 
dialog. You can change a user's password and change their role membership.
+
+image::images/security-ui/edit-user-dialog.png[image,width=400]
+
+For systems with many user accounts, use the filter controls at the top of the 
user table to find users based on common properties.
+
+image::images/security-ui/filter-users.png[image,width=400]
+
+For other authentication plugins, such as the 
<<jwt-authentication-plugin.adoc#,JWT Authentication>> plugin, this panel will 
be disabled as users are managed by an external system.
+
+== Role Management
+
+<<rule-based-authorization-plugin.adoc#roles,Roles>> link users to 
permissions. If using the <<rule-based-authorization-plugin.adoc#,Rule-based 
Authorization>> plugin, administrators can read, create, and update roles. 
Deleting roles is not supported.
+
+image::images/security-ui/roles.png[image,width=500]
+
+To edit a role, simply click on the corresponding row in the table.
+
+If not using the Rule-based Authorization plugin, the Roles panel will be 
disabled as user role assignment is managed by an external system.
+
+== Permission Management
+
+The *Permissions* panel on the Security screen allows administrators to read, 
create, update, and delete permissions.
+
+image::images/security-ui/permissions.png[image,width=900]
+
+For detailed information about how permissions work in Solr, see: 
<<rule-based-authorization-plugin.adoc#permissions,Rule-based Authorization 
Permissions>>.
+
+=== Add Permission
+
+Click on the btn:[Add Permission] button to open the Add Permission dialog.
+
+image::images/security-ui/add-permission.png[image,width=600]
+
+You can _either_ select a *Predefined* permission from the drop-down select 
list or provide a unique name for a custom permission.
+Creating a new *Predefined* permission is simply a matter of mapping the 
permission to zero or more roles as the other settings, such as path, are 
immutable for predefined permissions.
+If you need fine-grained control over the path, request method, or collection, 
then create a custom permission.
+
+If you do not select any roles for a permission, then the permission is 
assigned the `null` role, which means grants the permission to anonymous users.
+However, if *Block anonymous requests* (`blockUnknown=true`) is checked, then 
anonymous users will not be allowed to make requests, thus permission with the 
`null` role are effectively inactive.
+
+To edit a permission, simply click on the corresponding row in the table. When 
editing a permission, the current index of the permission in the list of 
permissions is editable.
+This allows you to re-order permissions if needed; see 
<<rule-based-authorization-plugin.adoc#permission-ordering-and-resolution,Permission
 Ordering>>.
+
+
+
+
diff --git a/solr/solr-ref-guide/src/solr-admin-ui.adoc 
b/solr/solr-ref-guide/src/solr-admin-ui.adoc
index 446759e..6761ec4 100644
--- a/solr/solr-ref-guide/src/solr-admin-ui.adoc
+++ b/solr/solr-ref-guide/src/solr-admin-ui.adoc
@@ -85,6 +85,14 @@ This server resides at 
https://issues.apache.org/jira/browse/SOLR.
 
 These links cannot be modified without editing the `index.html` in the 
`server/solr/solr-webapp` directory that contains the Admin UI files.
 
+== Security
+
+Users with the `security-edit` permission can manage users, roles, and 
permissions using the <<security-ui.adoc#,Security>> panel in the Admin UI.
+Users with the `security-read` permission can view the Security panel but all 
update actions on the panel are disabled.
+
+.Security Screen
+image::images/solr-admin-ui/security.png[image,width=800]
+
 == Schema Designer
 
 The <<schema-designer.adoc#,Schema Designer>> screen provides an interactive 
experience to create a schema using sample data.
@@ -97,7 +105,6 @@ image::images/solr-admin-ui/schema-designer.png[image]
 The Schema Designer is only available on Solr instances running 
<<cluster-types.adoc#solrcloud-mode,SolrCloud>>.
 ====
 
-
 == Collection-Specific Tools
 
 In the left-hand navigation bar, you will see a pull-down menu titled 
Collection Selector that can be used to access collection specific 
administration screens.
@@ -139,6 +146,7 @@ Here are sections throughout the Guide describing each 
screen of the Admin UI:
 [cols="1,1",frame=none,grid=none,stripes=none]
 |===
 | <<configuring-logging.adoc#logging-screen,Logging Screen>>: Recent log 
messages and configuration of log levels.
+| <<security-ui.adoc#,Security>>: Manage users, roles, and permissions.
 | <<cloud-screens.adoc#,Cloud Screens>>: Access to SolrCloud node data and 
status.
 | <<schema-designer.adoc#,Schema Designer>>: Interactively create a schema 
using sample data.
 | <<collections-core-admin.adoc#,Collections / Core Admin>>: Collection or 
Core management tools.
diff --git a/solr/webapp/web/css/angular/menu.css 
b/solr/webapp/web/css/angular/menu.css
index c0d09ec..a89e7ca 100644
--- a/solr/webapp/web/css/angular/menu.css
+++ b/solr/webapp/web/css/angular/menu.css
@@ -253,7 +253,7 @@ limitations under the License.
 
 #menu #index.global p a { background-image: url( ../../img/ico/dashboard.png 
); }
 
-#menu #login.global p a { background-image: url( ../../img/ico/users.png ); }
+#menu #login.global p a { background-image: url( ../../img/ico/logout.png ); }
 
 #menu #logging.global p a { background-image: url( 
../../img/ico/inbox-document-text.png ); }
 #menu #logging.global .level a { background-image: url( ../../img/ico/gear.png 
); }
@@ -272,6 +272,7 @@ limitations under the License.
 #menu #cloud.global .graph a { background-image: url( 
../../img/ico/molecule.png ); }
 
 #menu #schema-designer.global p a { background-image: url( 
../../img/ico/book-open-text.png ); }
+#menu #security.global p a { background-image: url( ../../img/ico/users.png ); 
}
 
 .sub-menu .ping.error a
 {
diff --git a/solr/webapp/web/css/angular/security.css 
b/solr/webapp/web/css/angular/security.css
new file mode 100644
index 0000000..2f0a857
--- /dev/null
+++ b/solr/webapp/web/css/angular/security.css
@@ -0,0 +1,678 @@
+/*
+
+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.
+
+*/
+#securityPanel
+{
+  position: relative;
+}
+
+#securityPanel .main-col
+{
+  float: left;
+  padding: 7px;
+  min-width: 700px;
+}
+
+#securityPanel #users h2 { background-image: url( ../../img/ico/users.png ); }
+#securityPanel #roles h2 { background-image: url( ../../img/ico/key.png ); }
+#securityPanel #permissions h2 { background-image: url( ../../img/ico/lock.png 
); }
+#securityPanel #authn h2 { background-image: url( ../../img/ico/shield.png ); }
+
+#securityPanel .ref-guide-link
+{
+  cursor: pointer;
+  color: #003eff;
+  text-decoration-line: underline;
+  text-decoration-color: #003eff;
+}
+
+#securityPanel .warning-msg
+{
+  display: block;
+  font-weight: bold;
+  margin-bottom: 15px;
+}
+
+#securityPanel .external-msg
+{
+  display: block;
+  margin-bottom: 20px;
+  margin-top: 20px;
+  margin-left: 30px;
+}
+
+#securityPanel .error-msg
+{
+  display: block;
+  font-weight: bold;
+  color: #c00;
+  margin-bottom: 15px;
+}
+
+#securityPanel .table-hdr {
+  border-bottom: 1px solid #dddddd;
+  padding: 3px;
+  font-weight: bold;
+  text-align: left;
+  word-wrap: break-word;
+}
+
+#securityPanel .td-name {
+  width: 150px;
+}
+
+#securityPanel .td-role {
+  width: 100px;
+}
+
+#securityPanel .td-roles {
+  width: 200px;
+}
+
+#securityPanel .td-coll {
+  width: 120px;
+}
+
+#securityPanel .td-path {
+  width: 250px;
+}
+
+#securityPanel .td-method {
+  width: 100px;
+}
+
+#securityPanel .td-params {
+  width: 200px;
+}
+
+#securityPanel .table-data {
+  border-bottom: 1px solid #dddddd;
+  padding: 3px;
+  text-align: left;
+  word-wrap: break-word;
+  max-width: 450px;
+}
+
+#securityPanel tr.odd
+{
+  background-color: #f8f8f8;
+}
+
+#securityPanel #authn
+{
+  display: block;
+}
+
+#securityPanel #user-actions
+{
+  margin-right: 5px;
+  float: right;
+}
+
+#securityPanel #perm-actions
+{
+  margin-right: 5px;
+  float: right;
+}
+
+#securityPanel #authn form .buttons
+{
+  margin-top: 10px;
+  float: right;
+  width: 71%;
+}
+
+#securityPanel #authn form button.submit
+{
+  margin-right: 20px;
+}
+
+#securityPanel #authn form button.submit span
+{
+  background-image: url( ../../img/ico/tick.png );
+}
+
+#securityPanel #authn form button.reset span
+{
+  background-image: url( ../../img/ico/cross.png );
+}
+
+#securityPanel #authn #user-dialog
+{
+  z-index: 100;
+  background-color: #fff;
+  border: 1px solid #f0f0f0;
+  box-shadow: 5px 5px 10px #c0c0c0;
+  -moz-box-shadow: 5px 5px 10px #c0c0c0;
+  -webkit-box-shadow: 5px 5px 10px #c0c0c0;
+  position: absolute;
+  padding: 10px;
+  width: 350px;
+}
+
+#securityPanel #authn #role-dialog
+{
+  z-index: 100;
+  background-color: #fff;
+  border: 1px solid #f0f0f0;
+  box-shadow: 5px 5px 10px #c0c0c0;
+  -moz-box-shadow: 5px 5px 10px #c0c0c0;
+  -webkit-box-shadow: 5px 5px 10px #c0c0c0;
+  position: absolute;
+  padding: 10px;
+  width: 350px;
+}
+
+#securityPanel #authn #add-permission-dialog
+{
+  z-index: 100;
+  background-color: #fff;
+  border: 1px solid #f0f0f0;
+  box-shadow: 5px 5px 10px #c0c0c0;
+  -moz-box-shadow: 5px 5px 10px #c0c0c0;
+  -webkit-box-shadow: 5px 5px 10px #c0c0c0;
+  position: absolute;
+  top: 190px;
+  padding: 10px;
+  width: 530px;
+}
+
+#securityPanel #authn #add-permission-dialog form label
+{
+  float: left;
+  padding-top: 3px;
+  padding-bottom: 3px;
+  text-align: right;
+  width: 110px;
+  margin-right: 6px;
+}
+
+#securityPanel #authn #user-dialog form label
+{
+  float: left;
+  padding-top: 3px;
+  padding-bottom: 3px;
+  text-align: right;
+  width: 37%;
+  margin-right: 6px;
+}
+
+#securityPanel #authn form p {
+  padding-bottom: 8px;
+}
+
+#securityPanel #add-user span
+{
+  background-image: url( ../../img/ico/useradd.png );
+}
+
+#securityPanel #add-permission
+{
+  margin-left: 10px;
+}
+
+#securityPanel #add-permission span
+{
+  background-image: url( ../../img/ico/lockplus.png );
+}
+
+#securityPanel #authn .validate-error
+{
+  background-image: url( ../../img/ico/cross.png );
+}
+
+#securityPanel #authn .validate-error span
+{
+  color: #c00;
+  font-weight: bold;
+  margin-left: 18px;
+}
+
+#securityPanel #authn .formMessageHolder {
+  display: block;
+  margin-top: 7px;
+  height: 56px;
+  margin-bottom: 7px;
+  margin-left: 10px;
+}
+
+#securityPanel .form-field {
+  margin-top: 7px;
+}
+
+#securityPanel #authn .input-text {
+  width: 142px;
+}
+
+#securityPanel #authn .input-check {
+  margin-left: 0px;
+  text-align: left;
+  width: 25px;
+}
+
+#securityPanel #authn #add_perm_path {
+  width: 280px;
+}
+
+#securityPanel #authn #add_perm_params {
+  width: 300px;
+}
+
+#securityPanel #authn #add_user_roles {
+  width: 148px;
+}
+
+#securityPanel #authn #add_perm_roles {
+  width: 148px;
+}
+
+#securityPanel #authn #predefined {
+  width: 172px;
+}
+
+#securityPanel #users-content
+{
+  height: 230px;
+  margin-bottom: 20px;
+}
+
+#securityPanel #users-table
+{
+  height: 200px;
+  overflow: auto;
+}
+
+#securityPanel #roles-content
+{
+  height: 230px;
+  margin-bottom: 20px;
+}
+
+#securityPanel #roles-table
+{
+  height: 200px;
+  overflow: auto;
+}
+
+#securityPanel #perms-table
+{
+  max-height: 400px;
+  overflow: auto;
+}
+
+#securityPanel .help div.help-perm
+{
+  z-index: 200;
+  background-color: #FCF0AD;
+  border: 1px solid #f0f0f0;
+  box-shadow: 5px 5px 10px #c0c0c0;
+  -moz-box-shadow: 5px 5px 10px #c0c0c0;
+  -webkit-box-shadow: 5px 5px 10px #c0c0c0;
+  position: absolute;
+  left: 20px;
+  top: 90px;
+  padding: 6px;
+  width: 420px;
+}
+
+#securityPanel .help-anchor
+{
+  margin-top: 7px;
+  margin-left: 18px;
+  margin-bottom: 10px;
+}
+
+#securityPanel .help-anchor a
+{
+  color: #003eff;
+  text-decoration-line: underline;
+  text-decoration-color: #003eff;
+}
+
+#securityPanel .help-ico {
+  margin-left: 3px;
+  margin-top: 3px;
+}
+
+#securityPanel .heading {
+  display: block;
+  font-weight: bold;
+  font-size: 12px;
+  margin-left: 10px;
+  margin-bottom: 10px;
+}
+
+#securityPanel .users-left
+{
+  float: left;
+  width: 45%;
+  margin-right: 20px;
+}
+
+#securityPanel .roles-right
+{
+  overflow: auto;
+}
+
+#securityPanel #user-filters
+{
+  margin-top: 3px;
+  margin-bottom: 10px;
+}
+
+#securityPanel #user-filter-type
+{
+  margin-right: 5px;
+}
+
+#securityPanel #user-filter-text
+{
+  width: 100px;
+}
+
+#securityPanel #perm-filters
+{
+  margin-top: 3px;
+  margin-bottom: 10px;
+}
+
+#securityPanel #perm-filter-type
+{
+  margin-right: 5px;
+}
+
+#securityPanel #perm-filter-text
+{
+  width: 100px;
+}
+
+#securityPanel #delete-user
+{
+  margin-left: 15px;
+  float: right;
+}
+
+#securityPanel #delete-user span
+{
+  background-image: url( ../../img/ico/cross-button.png );
+}
+
+#securityPanel #user-heading {
+  display: block;
+  padding: 4px;
+  margin-bottom: 22px;
+}
+
+#securityPanel #role-heading {
+  display: block;
+  padding: 4px;
+  margin-bottom: 22px;
+}
+
+#securityPanel #perm-heading {
+  display: block;
+  padding: 4px;
+  margin-bottom: 22px;
+}
+
+#securityPanel #delete-perm
+{
+  margin-left: 15px;
+  float: right;
+}
+
+#securityPanel #delete-perm span
+{
+  background-image: url( ../../img/ico/cross-button.png );
+}
+
+#securityPanel #role-filters
+{
+  margin-top: 3px;
+  margin-bottom: 10px;
+}
+
+#securityPanel #role-filter-type
+{
+  margin-right: 5px;
+}
+
+#securityPanel #role-filter-text
+{
+  width: 100px;
+}
+
+#securityPanel #role-actions
+{
+  margin-right: 5px;
+  float: right;
+}
+
+#securityPanel #add-role span
+{
+  background-image: url( ../../img/ico/keyplus.png );
+}
+
+#securityPanel #authn-settings {
+  margin-top: 5px;
+  margin-bottom: 20px;
+}
+
+#securityPanel #plugins {
+  display: block;
+  margin-bottom: 10px;
+}
+
+#securityPanel #authzPlugin {
+  margin-left: 40px;
+}
+
+#securityPanel #realm-field {
+  display: inline;
+  width: 150px;
+  margin-right: 30px;
+}
+
+#securityPanel #block-field {
+  display: inline;
+  width: 220px;
+  margin-right: 30px;
+}
+
+#securityPanel #forward-field {
+  display: inline;
+  width: 220px;
+}
+
+#securityPanel #blockUnknownHelp
+{
+  z-index: 200;
+  background-color: #FCF0AD;
+  border: 1px solid #f0f0f0;
+  box-shadow: 5px 5px 10px #c0c0c0;
+  -moz-box-shadow: 5px 5px 10px #c0c0c0;
+  -webkit-box-shadow: 5px 5px 10px #c0c0c0;
+  position: absolute;
+  left: 210px;
+  top: 100px;
+  padding: 6px;
+  width: 380px;
+}
+
+#securityPanel #forwardCredsHelp
+{
+  z-index: 200;
+  background-color: #FCF0AD;
+  border: 1px solid #f0f0f0;
+  box-shadow: 5px 5px 10px #c0c0c0;
+  -moz-box-shadow: 5px 5px 10px #c0c0c0;
+  -webkit-box-shadow: 5px 5px 10px #c0c0c0;
+  position: absolute;
+  left: 450px;
+  top: 100px;
+  padding: 6px;
+  width: 380px;
+}
+
+#securityPanel #authn-content
+{
+  margin-top: 10px;
+  height: 80px;
+  overflow: auto;
+}
+
+#securityPanel #authn #role-dialog form label
+{
+  float: left;
+  padding-top: 3px;
+  padding-bottom: 3px;
+  text-align: right;
+  width: 37%;
+  margin-right: 6px;
+}
+
+#securityPanel #add_perm_custom
+{
+  margin-left: 5px;
+  display: inline;
+}
+
+#securityPanel #add_perm_name {
+  width: 125px;
+}
+
+#securityPanel #perm-select {
+  margin-top: 12px;
+  margin-bottom: 15px;
+}
+
+#securityPanel .help div.help-index
+{
+  z-index: 200;
+  background-color: #FCF0AD;
+  border: 1px solid #f0f0f0;
+  box-shadow: 5px 5px 10px #c0c0c0;
+  -moz-box-shadow: 5px 5px 10px #c0c0c0;
+  -webkit-box-shadow: 5px 5px 10px #c0c0c0;
+  position: absolute;
+  left: 20px;
+  top: 90px;
+  padding: 6px;
+  width: 380px;
+}
+
+#securityPanel #param-rows
+{
+  display: block;
+  height: 100px;
+  max-height: 100px;
+  overflow: auto;
+}
+
+#securityPanel .row
+{
+  display: block;
+  margin-bottom: 5px;
+  margin-right: 100px;
+}
+
+#securityPanel .row .param-name
+{
+  display: inline;
+  float: left;
+  width: 90px;
+}
+
+#securityPanel .row .param-value
+{
+  display: inline;
+  width: 140px;
+}
+
+#securityPanel .row .param-buttons
+{
+  float: right;
+  width: 40px;
+}
+
+#securityPanel .row a
+{
+  background-position: 50% 50%;
+  display: block;
+  height: 25px;
+  width: 49%;
+}
+
+#securityPanel .row a.add
+{
+  background-image: url( ../../img/ico/plus-button.png );
+  float: right;
+}
+
+#securityPanel .row a.rem
+{
+  background-image: url( ../../img/ico/minus-button.png );
+  float: left;
+}
+
+#securityPanel .error-dialog
+{
+  z-index: 200;
+  background-color: #f0f0f0;
+  border: 1px solid #c00;
+  box-shadow: 5px 5px 10px #c0c0c0;
+  -moz-box-shadow: 5px 5px 10px #c0c0c0;
+  -webkit-box-shadow: 5px 5px 10px #c0c0c0;
+  position: absolute;
+  left: 350px;
+  top: 95px;
+  padding: 20px;
+  width: 450px;
+}
+
+#securityPanel #error-dialog #error-dialog-buttons {
+  float: right;
+}
+
+#securityPanel #error-dialog .error-button {
+  margin-right: 15px;
+}
+
+#securityPanel #error-dialog .error-button span
+{
+  background-image: url( ../../img/ico/tick.png );
+}
+
+#securityPanel #error-dialog-note {
+  color: #c00;
+  font-weight: bold;
+  margin-bottom: 15px;
+}
+
+#securityPanel #error-dialog-details {
+  min-height: 80px;
+  margin-bottom: 15px;
+}
+
+#securityPanel #authnPlugin {
+  margin-left: 20px;
+}
+
+#securityPanel .editable {
+  cursor: pointer;
+}
\ No newline at end of file
diff --git a/solr/webapp/web/img/ico/key.png b/solr/webapp/web/img/ico/key.png
new file mode 100644
index 0000000..8284636
Binary files /dev/null and b/solr/webapp/web/img/ico/key.png differ
diff --git a/solr/webapp/web/img/ico/keyplus.png 
b/solr/webapp/web/img/ico/keyplus.png
new file mode 100644
index 0000000..9c3e361
Binary files /dev/null and b/solr/webapp/web/img/ico/keyplus.png differ
diff --git a/solr/webapp/web/img/ico/lock.png b/solr/webapp/web/img/ico/lock.png
new file mode 100644
index 0000000..571c16d
Binary files /dev/null and b/solr/webapp/web/img/ico/lock.png differ
diff --git a/solr/webapp/web/img/ico/lockplus.png 
b/solr/webapp/web/img/ico/lockplus.png
new file mode 100644
index 0000000..e3aa6e3
Binary files /dev/null and b/solr/webapp/web/img/ico/lockplus.png differ
diff --git a/solr/webapp/web/img/ico/logout.png 
b/solr/webapp/web/img/ico/logout.png
new file mode 100644
index 0000000..2bc51ac
Binary files /dev/null and b/solr/webapp/web/img/ico/logout.png differ
diff --git a/solr/webapp/web/img/ico/shield--exclamation.png 
b/solr/webapp/web/img/ico/shield--exclamation.png
new file mode 100644
index 0000000..37c57b9
Binary files /dev/null and b/solr/webapp/web/img/ico/shield--exclamation.png 
differ
diff --git a/solr/webapp/web/img/ico/shield.png 
b/solr/webapp/web/img/ico/shield.png
new file mode 100644
index 0000000..c41e5ab
Binary files /dev/null and b/solr/webapp/web/img/ico/shield.png differ
diff --git a/solr/webapp/web/img/ico/useradd.png 
b/solr/webapp/web/img/ico/useradd.png
new file mode 100644
index 0000000..9f6c0f5
Binary files /dev/null and b/solr/webapp/web/img/ico/useradd.png differ
diff --git a/solr/webapp/web/index.html b/solr/webapp/web/index.html
index 05e837f..cdf780c 100644
--- a/solr/webapp/web/index.html
+++ b/solr/webapp/web/index.html
@@ -27,6 +27,7 @@ limitations under the License.
   <link rel="stylesheet" type="text/css" 
href="css/angular/common.css?_=${version}">
   <link rel="stylesheet" type="text/css" 
href="css/angular/analysis.css?_=${version}">
   <link rel="stylesheet" type="text/css" 
href="css/angular/schema-designer.css?_=${version}">
+  <link rel="stylesheet" type="text/css" 
href="css/angular/security.css?_=${version}">
   <link rel="stylesheet" type="text/css" 
href="css/angular/cloud.css?_=${version}">
   <link rel="stylesheet" type="text/css" 
href="css/angular/cores.css?_=${version}">
   <link rel="stylesheet" type="text/css" 
href="css/angular/collections.css?_=${version}">
@@ -90,6 +91,7 @@ limitations under the License.
   <script src="js/angular/controllers/replication.js"></script>
   <script src="js/angular/controllers/schema.js"></script>
   <script src="js/angular/controllers/schema-designer.js"></script>
+  <script src="js/angular/controllers/security.js"></script>
   <script src="js/angular/controllers/segments.js"></script>
   <script src="js/angular/controllers/unknown.js"></script>
   <script src="js/angular/controllers/sqlquery.js"></script>
@@ -160,6 +162,8 @@ limitations under the License.
                 </ul>
               </li>
 
+              <li id="security" class="global" 
ng-class="{active:page=='security'}"><p><a 
href="#/~security">Security</a></p></li>
+
               <li id="cloud" class="global optional" ng-show="isCloudEnabled" 
ng-class="{active:showingCloud}"><p><a href="#/~cloud">Cloud</a></p>
                 <ul ng-show="showingCloud">
                   <li class="nodes" ng-class="{active:page=='cloud-nodes'}"><a 
href="#/~cloud?view=nodes">Nodes</a></li>
diff --git a/solr/webapp/web/js/angular/app.js 
b/solr/webapp/web/js/angular/app.js
index 47f0f31..28ba743 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -177,6 +177,10 @@ solrAdminApp.config([
         templateUrl: 'partials/schema-designer.html',
         controller: 'SchemaDesignerController'
       }).
+      when('/~security', {
+        templateUrl: 'partials/security.html',
+        controller: 'SecurityController'
+      }).
       otherwise({
         templateUrl: 'partials/unknown.html',
         controller: 'UnknownController'
@@ -436,7 +440,7 @@ solrAdminApp.config([
         $location.path('/login');
       }
     } else {
-      // schema designer prefers to handle errors itselft
+      // schema designer prefers to handle errors itself
       var isHandledBySchemaDesigner = rejection.config.url && 
rejection.config.url.startsWith("/api/schema-designer/");
       if (!isHandledBySchemaDesigner) {
         $rootScope.exceptions[rejection.config.url] = rejection.data.error;
diff --git a/solr/webapp/web/js/angular/controllers/security.js 
b/solr/webapp/web/js/angular/controllers/security.js
new file mode 100644
index 0000000..93a120e
--- /dev/null
+++ b/solr/webapp/web/js/angular/controllers/security.js
@@ -0,0 +1,1123 @@
+/*
+ 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.
+*/
+
+solrAdminApp.controller('SecurityController', function ($scope, $timeout, 
$cookies, $window, Constants, System, Security) {
+  $scope.resetMenu("security", Constants.IS_ROOT_PAGE);
+
+  $scope.params = [];
+
+  var strongPasswordRegex = 
/^(?=.*[0-9])(?=.*[!@#$%^&*\-_()[\]])[a-zA-Z0-9!@#$%^&*\-_()[\]]{8,30}$/;
+
+  function toList(str) {
+    if (Array.isArray(str)) {
+      return str; // already a list
+    }
+    return str.trim().split(",").map(s => s.trim()).filter(s => s !== "");
+  }
+
+  function asList(listOrStr) {
+    return Array.isArray(listOrStr) ? listOrStr : (listOrStr ? [listOrStr] : 
[]);
+  }
+
+  function transposeUserRoles(userRoles) {
+    var roleUsers = {};
+    for (var u in userRoles) {
+      var roleList = asList(userRoles[u]);
+      for (var i in roleList) {
+        var role = roleList[i];
+        if (!roleUsers[role]) roleUsers[role] = []
+        roleUsers[role].push(u);
+      }
+    }
+
+    var roles = [];
+    for (var r in roleUsers) {
+      roles.push({"name":r, "users":Array.from(new Set(roleUsers[r]))});
+    }
+    return roles.sort((a, b) => (a.name > b.name) ? 1 : -1);
+  }
+
+  function roleMatch(roles, rolesForUser) {
+    for (r in rolesForUser) {
+      if (roles.includes(rolesForUser[r]))
+        return true;
+    }
+    return false;
+  }
+
+  function permRow(perm, i) {
+    var roles = asList(perm.role);
+    var paths = asList(perm.path);
+
+    var collectionNames = "";
+    var collections = [];
+    if ("collection" in perm) {
+      if (perm["collection"] == null) {
+        collectionNames = "null";
+      } else {
+        collections = asList(perm.collection);
+        collectionNames = collections.sort().join(", ");
+      }
+    } else {
+      // no collection property on the perm, so the default "*" applies
+      collectionNames = "";
+      collections.push("*");
+    }
+
+    var method = asList(perm.method);
+
+    // perms don't always have an index ?!?
+    var index = "index" in perm ? perm["index"] : ""+i;
+
+    return { "index": index, "name": perm.name, "collectionNames": 
collectionNames, "collections": collections,
+      "roles": roles, "paths": paths, "method": method, "params": perm.params 
};
+  }
+
+  function checkError(data) {
+    var cause = null;
+    if ("errorMessages" in data && Array.isArray(data["errorMessages"]) && 
data["errorMessages"].length > 0) {
+      cause = "?";
+      if ("errorMessages" in data["errorMessages"][0] && 
Array.isArray(data["errorMessages"][0]["errorMessages"]) && 
data["errorMessages"][0]["errorMessages"].length > 0) {
+        cause = data["errorMessages"][0]["errorMessages"][0];
+      }
+    }
+    return cause;
+  }
+
+  function truncateTo(str, maxLen, delim) {
+    // allow for a little on either side of maxLen for better display
+    var varLen = Math.min(Math.round(maxLen * 0.1), 15);
+    if (str.length <= maxLen + varLen) {
+      return str;
+    }
+
+    var total = str.split(delim).length;
+    var at = str.indexOf(delim, maxLen - varLen);
+    str = (at !== -1 && at < maxLen + varLen) ? str.substring(0, at) : 
str.substring(0, maxLen);
+    var trimmed = str.split(delim).length;
+    var diff = total - trimmed;
+    str += " ... "+(diff > 1 ? "(+"+diff+" more)" : "");
+    return str;
+  }
+
+  $scope.closeErrorDialog = function () {
+    delete $scope.securityAPIError;
+    delete $scope.securityAPIErrorDetails;
+  };
+
+  $scope.displayList = function(listOrStr) {
+    if (!listOrStr) return "";
+    var str = Array.isArray(listOrStr) ? listOrStr.sort().join(", ") : 
(""+listOrStr).trim();
+    return truncateTo(str, 160, ", ");
+  };
+
+  $scope.displayParams = function(obj) {
+    if (!obj) return "";
+    if (Array.isArray(obj)) return obj.sort().join(", ");
+
+    var display = "";
+    for (const [key, value] of Object.entries(obj)) {
+      if (display.length > 0) display += "; ";
+      display += (key + "=" + 
(Array.isArray(value)?value.sort().join(","):value+""));
+    }
+    return truncateTo(display, 160, "; ");
+  };
+
+  $scope.displayRoles = function(obj) {
+    return (!obj || (Array.isArray(obj) && obj.length === 0)) ? "null" : 
$scope.displayList(obj);
+  };
+
+  $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", "filestore-read", "filestore-write", "package-edit", 
"package-read"].sort();
+
+  $scope.predefinedPermissionCollection = {"read":"*", "update":"*", 
"config-edit":"*", "config-read":"*", "schema-edit":"*", "schema-read":"*"};
+
+  $scope.errorHandler = function (e) {
+    var error = e.data && e.data.error ? e.data.error : null;
+    if (error && error.msg) {
+      $scope.securityAPIError = error.msg;
+      $scope.securityAPIErrorDetails = e.data.errorDetails;
+    } else if (e.data && e.data.message) {
+      $scope.securityAPIError = e.data.message;
+      $scope.securityAPIErrorDetails = JSON.stringify(e.data);
+    }
+  };
+
+  $scope.showHelp = function (id) {
+    if ($scope.helpId && ($scope.helpId === id || id === '')) {
+      delete $scope.helpId;
+    } else {
+      $scope.helpId = id;
+    }
+  };
+
+  $scope.refresh = function () {
+    $scope.hideAll();
+
+    $scope.blockUnknown = "false"; // default setting
+    $scope.realmName = "solr";
+    $scope.forwardCredentials = "false";
+
+    $scope.currentUser = sessionStorage.getItem("auth.username");
+
+    $scope.userFilter = "";
+    $scope.userFilterOption = "";
+    $scope.userFilterText = "";
+    $scope.userFilterOptions = [];
+
+    $scope.permFilter = "";
+    $scope.permFilterOption = "";
+    $scope.permFilterOptions = [];
+    $scope.permFilterTypes = ["", "name", "role", "path", "collection"];
+
+    System.get(function(data) {
+      // console.log(">> system: "+JSON.stringify(data));
+      $scope.authenticationPlugin = data.security ? 
data.security["authenticationPlugin"] : null;
+      $scope.authorizationPlugin = data.security ? 
data.security["authorizationPlugin"] : null;
+      $scope.myRoles = data.security ? data.security["roles"] : [];
+      $scope.isSecurityAdminEnabled = $scope.authenticationPlugin != null;
+      $scope.isCloudMode = data.mode.match( /solrcloud/i ) != null;
+      $scope.zkHost = $scope.isCloudMode ? data["zkHost"] : "";
+      $scope.solrHome = data["solr_home"];
+      $scope.refreshSecurityPanel();
+    }, function(e) {
+      if (e.status === 403) {
+        $scope.isSecurityAdminEnabled = true;
+        $scope.hasSecurityEditPerm = false;
+        $scope.hideAll();
+      }
+    });
+  };
+
+  $scope.hideAll = function () {
+    // add more dialogs here
+    delete $scope.validationError;
+    $scope.showUserDialog = false;
+    $scope.showPermDialog = false;
+    delete $scope.helpId;
+  };
+
+  $scope.getCurrentUserRoles = function() {
+    if ($scope.manageUserRolesEnabled) {
+      return Array.isArray($scope.userRoles[$scope.currentUser]) ? 
$scope.userRoles[$scope.currentUser] : [$scope.userRoles[$scope.currentUser]];
+    } else {
+      return $scope.myRoles;
+    }
+  };
+
+  $scope.hasPermission = function(permissionName) {
+    var rolesForPermission = $scope.permissionsTable.filter(p => 
permissionName === p.name).flatMap(p => p.roles);
+    return (rolesForPermission.length > 0 && roleMatch(rolesForPermission, 
$scope.getCurrentUserRoles()));
+  };
+
+  $scope.refreshSecurityPanel = function() {
+
+    // determine if the authorization plugin supports CRUD permissions
+    $scope.managePermissionsEnabled =
+        ($scope.authorizationPlugin === 
"org.apache.solr.security.RuleBasedAuthorizationPlugin" ||
+            $scope.authorizationPlugin === 
"org.apache.solr.security.ExternalRoleRuleBasedAuthorizationPlugin");
+
+    // don't allow CRUD on roles if using external
+    $scope.manageUserRolesEnabled = $scope.authorizationPlugin === 
"org.apache.solr.security.RuleBasedAuthorizationPlugin";
+
+    Security.get({path: "authorization"}, function (data) {
+      if (!data.authorization) {
+        $scope.isSecurityAdminEnabled = false;
+        $scope.hasSecurityEditPerm = false;
+        return;
+      }
+
+      if ($scope.manageUserRolesEnabled) {
+        $scope.userRoles = data.authorization["user-role"];
+        $scope.roles = transposeUserRoles($scope.userRoles);
+        $scope.filteredRoles = $scope.roles;
+        $scope.roleNames = $scope.roles.map(r => r.name).sort();
+        $scope.roleNamesWithWildcard = ["*"].concat($scope.roleNames);
+        if (!$scope.permFilterTypes.includes("user")) {
+          $scope.permFilterTypes.push("user"); // can only filter perms by 
user if we have a role to user mapping
+        }
+      } else {
+        $scope.userRoles = {};
+        $scope.roles = [];
+        $scope.filteredRoles = [];
+        $scope.roleNames = [];
+      }
+
+      $scope.permissions = data.authorization["permissions"];
+      $scope.permissionsTable = [];
+      for (p in $scope.permissions) {
+        $scope.permissionsTable.push(permRow($scope.permissions[p], 
parseInt(p)+1));
+      }
+      $scope.filteredPerms = $scope.permissionsTable;
+
+      $scope.hasSecurityEditPerm = $scope.hasPermission("security-edit");
+      $scope.hasSecurityReadPerm = $scope.hasSecurityEditPerm || 
$scope.hasPermission("security-read");
+
+      if ($scope.authenticationPlugin === 
"org.apache.solr.security.BasicAuthPlugin") {
+        $scope.manageUsersEnabled = true;
+
+        Security.get({path: "authentication"}, function (data) {
+          if (!data.authentication) {
+            // TODO: error msg
+            $scope.manageUsersEnabled = false;
+          }
+
+          $scope.blockUnknown = data.authentication["blockUnknown"] === true ? 
"true" : "false";
+          $scope.forwardCredentials = 
data.authentication["forwardCredentials"] === true ? "true" : "false";
+
+          if ("realm" in data.authentication) {
+            $scope.realmName = data.authentication["realm"];
+          }
+
+          var users = [];
+          if (data.authentication.credentials) {
+            for (var u in data.authentication.credentials) {
+              var roles = $scope.userRoles[u];
+              if (!roles) roles = [];
+              users.push({"username":u, "roles":roles});
+            }
+          }
+          $scope.users = users.sort((a, b) => (a.username > b.username) ? 1 : 
-1);
+          $scope.filteredUsers = $scope.users.slice(0,100); // only display 
first 100
+        }, $scope.errorHandler);
+      } else {
+        $scope.users = [];
+        $scope.filteredUsers = $scope.users;
+        $scope.manageUsersEnabled = false;
+      }
+    }, $scope.errorHandler);
+  };
+
+  $scope.validatePassword = function() {
+    var password = $scope.upsertUser.password.trim();
+    var password2 = $scope.upsertUser.password2 ? 
$scope.upsertUser.password2.trim() : "";
+    if (password !== password2) {
+      $scope.validationError = "Passwords do not match!";
+      return false;
+    }
+
+    if (!password.match(strongPasswordRegex)) {
+      $scope.validationError = "Password not strong enough! Must contain at 
least one lowercase letter, one uppercase letter, one digit, and one of these 
special characters: !@#$%^&*_-[]()";
+      return false;
+    }
+
+    return true;
+  };
+
+  $scope.updateUserRoles = function() {
+    var setUserRoles = {};
+    var roles = [];
+    if ($scope.upsertUser.selectedRoles) {
+      roles = roles.concat($scope.upsertUser.selectedRoles);
+    }
+    if ($scope.upsertUser.newRole && $scope.upsertUser.newRole.trim() !== "") {
+      var newRole = $scope.upsertUser.newRole.trim();
+      if (newRole !== "null" && newRole !== "*" && newRole.length <= 30) {
+        roles.push(newRole);
+      } // else, no new role for you!
+    }
+    var userRoles = Array.from(new Set(roles));
+    setUserRoles[$scope.upsertUser.username] = userRoles.length > 0 ? 
userRoles : null;
+    Security.post({path: "authorization"}, { "set-user-role": setUserRoles }, 
function (data) {
+      $scope.toggleUserDialog();
+      $scope.refreshSecurityPanel();
+    });
+  };
+
+  $scope.doUpsertUser = function() {
+    if (!$scope.upsertUser) {
+      delete $scope.validationError;
+      $scope.showUserDialog = false;
+      return;
+    }
+
+    if (!$scope.upsertUser.username || $scope.upsertUser.username.trim() === 
"") {
+      $scope.validationError = "Username is required!";
+      return;
+    }
+
+    // keep username to a reasonable length? but allow for email addresses
+    var username = $scope.upsertUser.username.trim();
+    if (username.length > 30) {
+      $scope.validationError = "Username must be 30 characters or less!";
+      return;
+    }
+
+    var doSetUser = false;
+    if ($scope.userDialogMode === 'add') {
+      if ($scope.users) {
+        var existing = $scope.users.find(u => u.username === username);
+        if (existing) {
+          $scope.validationError = "User '"+username+"' already exists!";
+          return;
+        }
+      }
+
+      if (!$scope.upsertUser.password) {
+        $scope.validationError = "Password is required!";
+        return;
+      }
+
+      if (!$scope.validatePassword()) {
+        return;
+      }
+      doSetUser = true;
+    } else {
+      if ($scope.upsertUser.password) {
+        if ($scope.validatePassword()) {
+          doSetUser = true;
+        } else {
+          return; // update to password is invalid
+        }
+      } // else no update to password
+    }
+
+    if ($scope.upsertUser.newRole && $scope.upsertUser.newRole.trim() !== "") {
+      var newRole = $scope.upsertUser.newRole.trim();
+      if (newRole === "null" || newRole === "*" || newRole.length > 30) {
+        $scope.validationError = "Invalid new role: "+newRole;
+        return;
+      }
+    }
+
+    delete $scope.validationError;
+
+    if (doSetUser) {
+      var setUserJson = {};
+      setUserJson[username] = $scope.upsertUser.password.trim();
+      Security.post({path: "authentication"}, { "set-user": setUserJson }, 
function (data) {
+
+        var errorCause = checkError(data);
+        if (errorCause != null) {
+          $scope.securityAPIError = "create user "+username+" failed due to: 
"+errorCause;
+          $scope.securityAPIErrorDetails = JSON.stringify(data);
+          return;
+        }
+
+        $scope.updateUserRoles();
+      });
+    } else {
+      $scope.updateUserRoles();
+    }
+  };
+
+  $scope.confirmDeleteUser = function() {
+    if (window.confirm("Confirm delete the '"+$scope.upsertUser.username+"' 
user?")) {
+      // remove all roles for the user and the delete the user
+      var removeRoles = {};
+      removeRoles[$scope.upsertUser.username] = null;
+      Security.post({path: "authorization"}, { "set-user-role": removeRoles }, 
function (data) {
+        Security.post({path: "authentication"}, {"delete-user": 
[$scope.upsertUser.username]}, function (data2) {
+          $scope.toggleUserDialog();
+          $scope.refreshSecurityPanel();
+        });
+      });
+    }
+  };
+
+  $scope.showAddUserDialog = function() {
+    $scope.userDialogMode = "add";
+    $scope.userDialogHeader = "Add New User";
+    $scope.userDialogAction = "Add User";
+    $scope.upsertUser = {};
+    $scope.toggleUserDialog();
+  };
+
+  $scope.toggleUserDialog = function() {
+    if ($scope.showUserDialog) {
+      delete $scope.upsertUser;
+      delete $scope.validationError;
+      $scope.showUserDialog = false;
+      return;
+    }
+
+    $scope.hideAll();
+    $('#user-dialog').css({left: 132, top: 132});
+    $scope.showUserDialog = true;
+  };
+
+  $scope.onPredefinedChanged = function() {
+    if (!$scope.upsertPerm) {
+      return;
+    }
+
+    if ($scope.upsertPerm.name && $scope.upsertPerm.name.trim() !== "") {
+      delete $scope.selectedPredefinedPermission;
+    } else {
+      $scope.upsertPerm.name = "";
+    }
+
+    if ($scope.selectedPredefinedPermission && 
$scope.selectedPredefinedPermission in $scope.predefinedPermissionCollection) {
+      $scope.upsertPerm.collection = 
$scope.predefinedPermissionCollection[$scope.selectedPredefinedPermission];
+    }
+
+    $scope.isPermFieldDisabled = ($scope.upsertPerm.name === "" && 
$scope.selectedPredefinedPermission);
+  };
+
+  $scope.showAddPermDialog = function() {
+    $scope.permDialogMode = "add";
+    $scope.permDialogHeader = "Add New Permission";
+    $scope.permDialogAction = "Add Permission";
+    $scope.upsertPerm = {};
+    $scope.upsertPerm.name = "";
+    $scope.upsertPerm.index = "";
+    $scope.upsertPerm["method"] = {"get":"true", "post":"true", "put":"true", 
"delete":"true"};
+    $scope.isPermFieldDisabled = false;
+    delete $scope.selectedPredefinedPermission;
+
+    $scope.params = [{"name":"", "value":""}];
+
+    var permissionNames = $scope.permissions.map(p => p.name);
+    $scope.filteredPredefinedPermissions = 
$scope.predefinedPermissions.filter(p => !permissionNames.includes(p));
+
+    $scope.togglePermDialog();
+  };
+
+  $scope.togglePermDialog = function() {
+    if ($scope.showPermDialog) {
+      delete $scope.upsertPerm;
+      delete $scope.validationError;
+      $scope.showPermDialog = false;
+      $scope.isPermFieldDisabled = false;
+      delete $scope.selectedPredefinedPermission;
+      return;
+    }
+
+    $scope.hideAll();
+
+    var leftPos = $scope.permDialogMode === "add" ? 500 : 100;
+    var topPos = $('#permissions').offset().top - 320;
+    if (topPos < 0) topPos = 0;
+    $('#add-permission-dialog').css({left: leftPos, top: topPos});
+
+    $scope.showPermDialog = true;
+  };
+
+  $scope.getMethods = function() {
+    var methods = [];
+    if ($scope.upsertPerm.method.get === "true") {
+      methods.push("GET");
+    }
+    if ($scope.upsertPerm.method.put === "true") {
+      methods.push("PUT");
+    }
+    if ($scope.upsertPerm.method.post === "true") {
+      methods.push("POST");
+    }
+    if ($scope.upsertPerm.method.delete === "true") {
+      methods.push("DELETE");
+    }
+    return methods;
+  };
+
+  $scope.confirmDeletePerm = function() {
+    var permName = $scope.selectedPredefinedPermission ? 
$scope.selectedPredefinedPermission : $scope.upsertPerm.name.trim();
+    if (window.confirm("Confirm delete the '"+permName+"' permission?")) {
+      var index = parseInt($scope.upsertPerm.index);
+      Security.post({path: "authorization"}, { "delete-permission": index }, 
function (data) {
+        $scope.togglePermDialog();
+        $scope.refreshSecurityPanel();
+      });
+    }
+  };
+
+  $scope.doUpsertPermission = function() {
+    if (!$scope.upsertPerm) {
+      $scope.upsertPerm = {};
+    }
+
+    var isAdd = $scope.permDialogMode === "add";
+    var name = $scope.selectedPredefinedPermission ? 
$scope.selectedPredefinedPermission : $scope.upsertPerm.name.trim();
+
+    if (isAdd) {
+      if (!name) {
+        $scope.validationError = "Either select a predefined permission or 
provide a name for a custom permission";
+        return;
+      }
+      var permissionNames = $scope.permissions.map(p => p.name);
+      if (permissionNames.includes(name)) {
+        $scope.validationError = "Permission '"+name+"' already exists!";
+        return;
+      }
+
+      if (name === "*") {
+        $scope.validationError = "Invalid permission name!";
+        return;
+      }
+    }
+
+    var role = null;
+    if ($scope.manageUserRolesEnabled) {
+      role = $scope.upsertPerm.selectedRoles;
+      if (!role || role.length === 0) {
+        role = null;
+      } else if (role.includes("*")) {
+        role = ["*"];
+      }
+    } else if ($scope.upsertPerm.manualRoles && 
$scope.upsertPerm.manualRoles.trim() !== "") {
+      var manualRoles = $scope.upsertPerm.manualRoles.trim();
+      role = (manualRoles === "null") ? null : toList(manualRoles);
+    }
+
+    var setPermJson = {"name": name, "role": role };
+
+    if ($scope.selectedPredefinedPermission) {
+      $scope.params = [{"name":"","value":""}];
+    } else {
+      // collection
+      var coll = null;
+      if ($scope.upsertPerm.collection != null && $scope.upsertPerm.collection 
!== "null") {
+        if ($scope.upsertPerm.collection === "*") {
+          coll = "*";
+        } else {
+          coll = $scope.upsertPerm.collection && 
$scope.upsertPerm.collection.trim() !== "" ? 
toList($scope.upsertPerm.collection) : "";
+        }
+      }
+      setPermJson["collection"] = coll;
+
+      // path
+      if (!$scope.upsertPerm.path || (Array.isArray($scope.upsertPerm.path) && 
$scope.upsertPerm.path.length === 0)) {
+        $scope.validationError = "Path is required for custom permissions!";
+        return;
+      }
+
+      setPermJson["path"] = toList($scope.upsertPerm.path);
+
+      if ($scope.upsertPerm.method) {
+        var methods = $scope.getMethods();
+        if (methods.length === 0) {
+          $scope.validationError = "Must specify at least one request method 
for a custom permission!";
+          return;
+        }
+
+        if (methods.length < 4) {
+          setPermJson["method"] = methods;
+        } // else no need to specify, rule applies to all methods
+      }
+
+      // params
+      var params = {};
+      if ($scope.params && $scope.params.length > 0) {
+        for (i in $scope.params) {
+          var p = $scope.params[i];
+          var name = p.name.trim();
+          if (name !== "" && p.value) {
+            if (name in params) {
+              params[name].push(p.value);
+            } else {
+              params[name] = [p.value];
+            }
+          }
+        }
+      }
+      setPermJson["params"] = params;
+    }
+
+    var indexUpdated = false;
+    if ($scope.upsertPerm.index) {
+      var indexOrBefore = isAdd ? "before" : "index";
+      var indexInt = parseInt($scope.upsertPerm.index);
+      if (indexInt < 1) indexInt = 1;
+      if (indexInt >= $scope.permissions.length) indexInt = null;
+      if (indexInt != null) {
+        setPermJson[indexOrBefore] = indexInt;
+      }
+      indexUpdated = (!isAdd && indexInt !== 
parseInt($scope.upsertPerm.originalIndex));
+    }
+
+    if (indexUpdated) {
+      // changing position is a delete + re-add in new position
+      Security.post({path: "authorization"}, { "delete-permission": 
parseInt($scope.upsertPerm.originalIndex) }, function (remData) {
+        if (setPermJson.index) {
+          var before = setPermJson.index;
+          delete setPermJson.index;
+          setPermJson["before"] = before;
+        }
+
+        // add perm back in new position
+        Security.post({path: "authorization"}, { "set-permission": setPermJson 
}, function (data) {
+          var errorCause = checkError(data);
+          if (errorCause != null) {
+            $scope.securityAPIError = "set-permission "+name+" failed due to: 
"+errorCause;
+            $scope.securityAPIErrorDetails = JSON.stringify(data);
+            return;
+          }
+          $scope.togglePermDialog();
+          $scope.refreshSecurityPanel();
+        });
+      });
+    } else {
+      var action = isAdd ? "set-permission" : "update-permission";
+      var postBody = {};
+      postBody[action] = setPermJson;
+      Security.post({path: "authorization"}, postBody, function (data) {
+        var errorCause = checkError(data);
+        if (errorCause != null) {
+          $scope.securityAPIError = action+" "+name+" failed due to: 
"+errorCause;
+          $scope.securityAPIErrorDetails = JSON.stringify(data);
+          return;
+        }
+
+        $scope.togglePermDialog();
+        $scope.refreshSecurityPanel();
+      });
+    }
+  };
+
+  $scope.applyUserFilter = function() {
+    $scope.userFilterText = "";
+    $scope.userFilterOption = "";
+    $scope.userFilterOptions = [];
+    $scope.filteredUsers = $scope.users; // reset the filtered when the filter 
type changes
+
+    if ($scope.userFilter === "name" || $scope.userFilter === "path") {
+      // no-op: filter is text input
+    } else if ($scope.userFilter === "role") {
+      $scope.userFilterOptions = $scope.roleNames;
+    } else if ($scope.userFilter === "perm") {
+      $scope.userFilterOptions = $scope.permissions.map(p => p.name).sort();
+    } else {
+      $scope.userFilter = "";
+    }
+  };
+
+  $scope.onUserFilterTextChanged = function() {
+    // don't fire until we have at least 2 chars ...
+    if ($scope.userFilterText && $scope.userFilterText.trim().length >= 2) {
+      $scope.userFilterOption = $scope.userFilterText.toLowerCase();
+      $scope.onUserFilterOptionChanged();
+    } else {
+      $scope.filteredUsers = $scope.users;
+    }
+  };
+
+  function pathMatch(paths, filter) {
+    for (p in paths) {
+      if (paths[p].includes(filter)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  $scope.onUserFilterOptionChanged = function() {
+    var filter = $scope.userFilterOption ? $scope.userFilterOption.trim() : "";
+    if (filter.length === 0) {
+      $scope.filteredUsers = $scope.users;
+      return;
+    }
+
+    if ($scope.userFilter === "name") {
+      $scope.filteredUsers = $scope.users.filter(u => 
u.username.toLowerCase().includes(filter));
+    } else if ($scope.userFilter === "role") {
+      $scope.filteredUsers = $scope.users.filter(u => 
u.roles.includes(filter));
+    } else if ($scope.userFilter === "path") {
+      var rolesForPath = Array.from(new Set($scope.permissionsTable.filter(p 
=> p.roles && pathMatch(p.paths, filter)).flatMap(p => p.roles)));
+      var usersForPath = Array.from(new Set($scope.roles.filter(r => r.users 
&& r.users.length > 0 && rolesForPath.includes(r.name)).flatMap(r => r.users)));
+      $scope.filteredUsers = $scope.users.filter(u => 
usersForPath.includes(u.username));
+    } else if ($scope.userFilter === "perm") {
+      var rolesForPerm = Array.from(new Set($scope.permissionsTable.filter(p 
=> p.name === filter).flatMap(p => p.roles)));
+      var usersForPerm = Array.from(new Set($scope.roles.filter(r => r.users 
&& r.users.length > 0 && rolesForPerm.includes(r.name)).flatMap(r => r.users)));
+      $scope.filteredUsers = $scope.users.filter(u => 
usersForPerm.includes(u.username));
+    } else {
+      // reset
+      $scope.userFilter = "";
+      $scope.userFilterOption = "";
+      $scope.userFilterText = "";
+      $scope.filteredUsers = $scope.users;
+    }
+  };
+
+  $scope.applyPermFilter = function() {
+    $scope.permFilterText = "";
+    $scope.permFilterOption = "";
+    $scope.permFilterOptions = [];
+    $scope.filteredPerms = $scope.permissionsTable;
+
+    if ($scope.permFilter === "name" || $scope.permFilter === "path") {
+      // no-op: filter is text input
+    } else if ($scope.permFilter === "role") {
+      var roles = $scope.manageUserRolesEnabled ? $scope.roleNames : 
Array.from(new Set($scope.permissionsTable.flatMap(p => p.roles))).sort();
+      $scope.permFilterOptions = ["*", "null"].concat(roles);
+    } else if ($scope.permFilter === "user") {
+      $scope.permFilterOptions = Array.from(new Set($scope.roles.flatMap(r => 
r.users))).sort();
+    } else if ($scope.permFilter === "collection") {
+      $scope.permFilterOptions = Array.from(new 
Set($scope.permissionsTable.flatMap(p => p.collections))).sort();
+      $scope.permFilterOptions.push("null");
+    } else {
+      // no perm filtering
+      $scope.permFilter = "";
+    }
+  };
+
+  $scope.onPermFilterTextChanged = function() {
+    // don't fire until we have at least 2 chars ...
+    if ($scope.permFilterText && $scope.permFilterText.trim().length >= 2) {
+      $scope.permFilterOption = $scope.permFilterText.trim().toLowerCase();
+      $scope.onPermFilterOptionChanged();
+    } else {
+      $scope.filteredPerms = $scope.permissionsTable;
+    }
+  };
+
+  $scope.onPermFilterOptionChanged = function() {
+    var filterCriteria = $scope.permFilterOption ? 
$scope.permFilterOption.trim() : "";
+    if (filterCriteria.length === 0) {
+      $scope.filteredPerms = $scope.permissionsTable;
+      return;
+    }
+
+    if ($scope.permFilter === "name") {
+      $scope.filteredPerms = $scope.permissionsTable.filter(p => 
p.name.toLowerCase().includes(filterCriteria));
+    } else if ($scope.permFilter === "role") {
+      if (filterCriteria === "null") {
+        $scope.filteredPerms = $scope.permissionsTable.filter(p => 
p.roles.length === 0);
+      } else {
+        $scope.filteredPerms = $scope.permissionsTable.filter(p => 
p.roles.includes(filterCriteria));
+      }
+    } else if ($scope.permFilter === "path") {
+      $scope.filteredPerms = $scope.permissionsTable.filter(p => 
pathMatch(p.paths, filterCriteria));
+    } else if ($scope.permFilter === "user") {
+      // get the user's roles and then find all the permissions mapped to each 
role
+      var rolesForUser = $scope.roles.filter(r => 
r.users.includes(filterCriteria)).map(r => r.name);
+      $scope.filteredPerms = $scope.permissionsTable.filter(p => 
p.roles.length > 0 && roleMatch(p.roles, rolesForUser));
+    } else if ($scope.permFilter === "collection") {
+      function collectionMatch(collNames, colls, filter) {
+        return (filter === "null") ?collNames === "null" : 
colls.includes(filter);
+      }
+      $scope.filteredPerms = $scope.permissionsTable.filter(p => 
collectionMatch(p.collectionNames, p.collections, filterCriteria));
+    } else {
+      // reset
+      $scope.permFilter = "";
+      $scope.permFilterOption = "";
+      $scope.permFilterText = "";
+      $scope.filteredPerms = $scope.permissionsTable;
+    }
+  };
+
+  $scope.editUser = function(row) {
+    if (!row || !$scope.hasSecurityEditPerm) {
+      return;
+    }
+
+    var userId = row.username;
+    $scope.userDialogMode = "edit";
+    $scope.userDialogHeader = "Edit User: "+userId;
+    $scope.userDialogAction = "Update";
+    var userRoles = userId in $scope.userRoles ? $scope.userRoles[userId] : [];
+    if (!Array.isArray(userRoles)) {
+      userRoles = [userRoles];
+    }
+
+    $scope.upsertUser = { username: userId, selectedRoles: userRoles };
+    $scope.toggleUserDialog();
+  };
+
+  function buildMethods(m) {
+    return {"get":""+m.includes("GET"), "post":""+m.includes("POST"), 
"put":""+m.includes("PUT"), "delete":""+m.includes("DELETE")};
+  }
+
+  $scope.editPerm = function(row) {
+    if (!$scope.managePermissionsEnabled || !$scope.hasSecurityEditPerm || 
!row) {
+      return;
+    }
+
+    var name = row.name;
+    $scope.permDialogMode = "edit";
+    $scope.permDialogHeader = "Edit Permission: "+name;
+    $scope.permDialogAction = "Update";
+
+    var perm = $scope.permissionsTable.find(p => p.name === name);
+    var isPredefined = $scope.predefinedPermissions.includes(name);
+    if (isPredefined) {
+      $scope.selectedPredefinedPermission = name;
+      $scope.upsertPerm = { };
+      $scope.filteredPredefinedPermissions = [];
+      $scope.filteredPredefinedPermissions.push(name);
+      if ($scope.selectedPredefinedPermission && 
$scope.selectedPredefinedPermission in $scope.predefinedPermissionCollection) {
+        $scope.upsertPerm.collection = 
$scope.predefinedPermissionCollection[$scope.selectedPredefinedPermission];
+      }
+      $scope.isPermFieldDisabled = true;
+    } else {
+      $scope.upsertPerm = { name: name, collection: perm.collectionNames, 
path: perm.paths };
+      $scope.params = [];
+      if (perm.params) {
+        for (const [key, value] of Object.entries(perm.params)) {
+          if (Array.isArray(value)) {
+            for (i in value) {
+              $scope.params.push({"name":key, "value":value[i]});
+            }
+          } else {
+            $scope.params.push({"name":key, "value":value});
+          }
+        }
+      }
+      if ($scope.params.length === 0) {
+        $scope.params = [{"name":"","value":""}];
+      }
+
+      $scope.upsertPerm["method"] = perm.method.length === 0 ? {"get":"true", 
"post":"true", "put":"true", "delete":"true"} : buildMethods(perm.method);
+      $scope.isPermFieldDisabled = false;
+      delete $scope.selectedPredefinedPermission;
+    }
+
+    $scope.upsertPerm.index = perm["index"];
+    $scope.upsertPerm.originalIndex = perm["index"];
+
+    // roles depending on authz plugin support
+    if ($scope.manageUserRolesEnabled) {
+      $scope.upsertPerm["selectedRoles"] = asList(perm.roles);
+    } else {
+      $scope.upsertPerm["manualRoles"] = asList(perm.roles).sort().join(", ");
+    }
+
+    $scope.togglePermDialog();
+  };
+
+  $scope.applyRoleFilter = function() {
+    $scope.roleFilterText = "";
+    $scope.roleFilterOption = "";
+    $scope.roleFilterOptions = [];
+    $scope.filteredRoles = $scope.roles; // reset the filtered when the filter 
type changes
+
+    if ($scope.roleFilter === "name" || $scope.roleFilter === "path") {
+      // no-op: filter is text input
+    } else if ($scope.roleFilter === "user") {
+      $scope.roleFilterOptions = Array.from(new Set($scope.roles.flatMap(r => 
r.users))).sort();
+    } else if ($scope.roleFilter === "perm") {
+      $scope.roleFilterOptions = $scope.permissions.map(p => p.name).sort();
+    } else {
+      $scope.roleFilter = "";
+    }
+  };
+
+  $scope.onRoleFilterTextChanged = function() {
+    // don't fire until we have at least 2 chars ...
+    if ($scope.roleFilterText && $scope.roleFilterText.trim().length >= 2) {
+      $scope.roleFilterOption = $scope.roleFilterText.toLowerCase();
+      $scope.onRoleFilterOptionChanged();
+    } else {
+      $scope.filteredRoles = $scope.roles;
+    }
+  };
+
+  $scope.onRoleFilterOptionChanged = function() {
+    var filter = $scope.roleFilterOption ? $scope.roleFilterOption.trim() : "";
+    if (filter.length === 0) {
+      $scope.filteredRoles = $scope.roles;
+      return;
+    }
+
+    if ($scope.roleFilter === "name") {
+      $scope.filteredRoles = $scope.roles.filter(r => 
r.name.toLowerCase().includes(filter));
+    } else if ($scope.roleFilter === "user") {
+      $scope.filteredRoles = $scope.roles.filter(r => 
r.users.includes(filter));
+    } else if ($scope.roleFilter === "path") {
+      var rolesForPath = Array.from(new Set($scope.permissionsTable.filter(p 
=> p.roles && pathMatch(p.paths, filter)).flatMap(p => p.roles)));
+      $scope.filteredRoles = $scope.roles.filter(r => 
rolesForPath.includes(r.name));
+    } else if ($scope.roleFilter === "perm") {
+      var rolesForPerm = Array.from(new Set($scope.permissionsTable.filter(p 
=> p.name === filter).flatMap(p => p.roles)));
+      $scope.filteredRoles = $scope.roles.filter(r => 
rolesForPerm.includes(r.name));
+    } else {
+      // reset
+      $scope.roleFilter = "";
+      $scope.roleFilterOption = "";
+      $scope.roleFilterText = "";
+      $scope.filteredRoles = $scope.roles;
+    }
+  };
+
+  $scope.showAddRoleDialog = function() {
+    $scope.roleDialogMode = "add";
+    $scope.roleDialogHeader = "Add New Role";
+    $scope.roleDialogAction = "Add Role";
+    $scope.upsertRole = {};
+    $scope.userNames = $scope.users.map(u => u.username);
+    $scope.grantPermissionNames = Array.from(new 
Set($scope.predefinedPermissions.concat($scope.permissions.map(p => 
p.name)))).sort();
+    $scope.toggleRoleDialog();
+  };
+
+  $scope.toggleRoleDialog = function() {
+    if ($scope.showRoleDialog) {
+      delete $scope.upsertRole;
+      delete $scope.validationError;
+      delete $scope.userNames;
+      $scope.showRoleDialog = false;
+      return;
+    }
+    $scope.hideAll();
+    $('#role-dialog').css({left: 680, top: 139});
+    $scope.showRoleDialog = true;
+  };
+
+  $scope.doUpsertRole = function() {
+    if (!$scope.upsertRole) {
+      delete $scope.validationError;
+      $scope.showRoleDialog = false;
+      return;
+    }
+
+    if (!$scope.upsertRole.name || $scope.upsertRole.name.trim() === "") {
+      $scope.validationError = "Role name is required!";
+      return;
+    }
+
+    // keep role name to a reasonable length? but allow for email addresses
+    var name = $scope.upsertRole.name.trim();
+    if (name.length > 30) {
+      $scope.validationError = "Role name must be 30 characters or less!";
+      return;
+    }
+
+    if (name === "null" || name === "*") {
+      $scope.validationError = "Role name '"+name+"' is invalid!";
+      return;
+    }
+
+    if ($scope.roleDialogMode === "add") {
+      if ($scope.roleNames.includes(name)) {
+        $scope.validationError = "Role '"+name+"' already exists!";
+        return;
+      }
+    }
+
+    var usersForRole = [];
+    if ($scope.upsertRole.selectedUsers && 
$scope.upsertRole.selectedUsers.length > 0) {
+      usersForRole = usersForRole.concat($scope.upsertRole.selectedUsers);
+    }
+    usersForRole = Array.from(new Set(usersForRole));
+    if (usersForRole.length === 0) {
+      $scope.validationError = "Must assign new role '"+name+"' to at least 
one user.";
+      return;
+    }
+
+    var perms = [];
+    if ($scope.upsertRole.grantedPerms && 
Array.isArray($scope.upsertRole.grantedPerms) && 
$scope.upsertRole.grantedPerms.length > 0) {
+      perms = $scope.upsertRole.grantedPerms;
+    }
+
+    // go get the latest role mappings ...
+    Security.get({path: "authorization"}, function (data) {
+      var userRoles = data.authorization["user-role"];
+      var setUserRoles = {};
+      for (u in usersForRole) {
+        var user = usersForRole[u];
+        var currentRoles = user in userRoles ? asList(userRoles[user]) : [];
+        // add the new role for this user if needed
+        if (!currentRoles.includes(name)) {
+          currentRoles.push(name);
+        }
+        setUserRoles[user] = currentRoles;
+      }
+
+      Security.post({path: "authorization"}, { "set-user-role": setUserRoles 
}, function (data2) {
+
+        var errorCause = checkError(data2);
+        if (errorCause != null) {
+          $scope.securityAPIError = "set-user-role for "+username+" failed due 
to: "+errorCause;
+          $scope.securityAPIErrorDetails = JSON.stringify(data2);
+          return;
+        }
+
+        if (perms.length === 0) {
+          // close dialog and refresh the tables ...
+          $scope.toggleRoleDialog();
+          $scope.refreshSecurityPanel();
+          return;
+        }
+
+        var currentPerms = data.authorization["permissions"];
+        for (i in perms) {
+          var permName = perms[i];
+          var existingPerm = currentPerms.find(p => p.name === permName);
+
+          if (existingPerm) {
+            var roleList = [];
+            if (existingPerm.role) {
+              if (Array.isArray(existingPerm.role)) {
+                roleList = existingPerm.role;
+              } else {
+                roleList.push(existingPerm.role);
+              }
+            }
+            if (!roleList.includes(name)) {
+              roleList.push(name);
+            }
+            existingPerm.role = roleList;
+            Security.post({path: "authorization"}, { "update-permission": 
existingPerm }, function (data3) {
+              $scope.refreshSecurityPanel();
+            });
+          } else {
+            // new perm ... must be a predefined ...
+            if ($scope.predefinedPermissions.includes(permName)) {
+              var setPermission = {name: permName, role:[name]};
+              Security.post({path: "authorization"}, { "set-permission": 
setPermission }, function (data3) {
+                $scope.refreshSecurityPanel();
+              });
+            } // else ignore it
+          }
+        }
+        $scope.toggleRoleDialog();
+      });
+    });
+
+  };
+
+  $scope.editRole = function(row) {
+    if (!row || !$scope.hasSecurityEditPerm) {
+      return;
+    }
+
+    var roleName = row.name;
+    $scope.roleDialogMode = "edit";
+    $scope.roleDialogHeader = "Edit Role: "+roleName;
+    $scope.roleDialogAction = "Update";
+    var role = $scope.roles.find(r => r.name === roleName);
+    var perms = $scope.permissionsTable.filter(p => 
p.roles.includes(roleName)).map(p => p.name);
+    $scope.upsertRole = { name: roleName, selectedUsers: role.users, 
grantedPerms: perms };
+    $scope.userNames = $scope.users.map(u => u.username);
+    $scope.grantPermissionNames = Array.from(new 
Set($scope.predefinedPermissions.concat($scope.permissions.map(p => 
p.name)))).sort();
+    $scope.toggleRoleDialog();
+  };
+
+  $scope.onBlockUnknownChange = function() {
+    Security.post({path: "authentication"}, { "set-property": { 
"blockUnknown": $scope.blockUnknown === "true" } }, function (data) {
+      $scope.refreshSecurityPanel();
+    });
+  };
+
+  $scope.onForwardCredsChange = function() {
+    Security.post({path: "authentication"}, { "set-property": { 
"forwardCredentials": $scope.forwardCredentials === "true" } }, function (data) 
{
+      $scope.refreshSecurityPanel();
+    });
+  };
+
+  $scope.removeParam= function(index) {
+    if ($scope.params.length === 1) {
+      $scope.params = [{"name":"","value":""}];
+    } else {
+      $scope.params.splice(index, 1);
+    }
+  };
+  
+  $scope.addParam = function(index) {
+    $scope.params.splice(index+1, 0, {"name":"","value":""});
+  };
+
+  $scope.refresh();
+})
diff --git a/solr/webapp/web/js/angular/services.js 
b/solr/webapp/web/js/angular/services.js
index 4d2c3c3..6da98c2 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -270,6 +270,12 @@ solrAdminServices.factory('System',
        upload: {method: "POST", transformRequest: angular.identity, headers: 
{'Content-Type': undefined}, timeout: 90000}
      })
 }])
+.factory('Security',
+    ['$resource', function($resource) {
+          return $resource('/api/cluster/security/:path', {wt: 'json', path: 
'@path', _:Date.now()}, {
+            get: {method: "GET"}, post: {method: "POST", timeout: 90000}
+        })
+}])
 .factory('AuthenticationService',
     ['base64', function (base64) {
         var service = {};
diff --git a/solr/webapp/web/partials/security.html 
b/solr/webapp/web/partials/security.html
new file mode 100644
index 0000000..b3b7b8a
--- /dev/null
+++ b/solr/webapp/web/partials/security.html
@@ -0,0 +1,285 @@
+<!--
+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.
+-->
+<div id="securityPanel" class="clearfix">
+  <div ng-show="isSecurityAdminEnabled && !currentUser">
+    <p class="error-msg"><img src="img/ico/prohibition.png"/>&nbsp;Current 
user is not authenticated! Security panel is disabled.</p>
+  </div>
+
+  <div ng-show="isSecurityAdminEnabled && !hasSecurityReadPerm">
+    <p class="error-msg"><img src="img/ico/prohibition.png"/>&nbsp;You do not 
have permission to view the security panel.</p>
+  </div>
+
+  <div ng-show="!isSecurityAdminEnabled">
+    <p class="warning-msg"><img 
src="img/ico/shield--exclamation.png"/>&nbsp;WARNING: Security is not enabled 
for this server!</p>
+    <div ng-show="isCloudMode">
+      <p class="clearfix">Use the <b>bin/solr auth</b> command-line tool to 
enable security and then reload this panel. For more information, see: <a 
target="_blank" 
href="https://solr.apache.org/guide/authentication-and-authorization-plugins.html#using-security-json-with-solr";
 class="ref-guide-link">Using security.json with Solr</a></p>
+      <p class="clearfix"><br/>Example usage of <b>bin/solr auth</b> to enable 
basic authentication:</p>
+      <pre>
+
+
+        bin/solr auth enable -type basicAuth -prompt true -z {{zkHost}}
+
+      </pre>
+    </div>
+    <div ng-show="!isCloudMode">
+      <p class="clearfix">Create a <b>security.json</b> config file in your 
Solr home directory and then restart Solr (on all nodes). For more information, 
see: <a target="_blank" 
href="https://solr.apache.org/guide/authentication-and-authorization-plugins.html#using-security-json-with-solr";
 class="ref-guide-link">Using security.json with Solr</a></p>
+    </div>
+  </div>
+
+  <div class="main-col" ng-show="isSecurityAdminEnabled && 
hasSecurityReadPerm">
+
+    <div class="block">
+      <div id="authn">
+        <h2><span>Security Settings</span></h2>
+        <div id="authn-content">
+          <div id="plugins"><span id="tls">TLS enabled? <img ng-show="tls" 
src="img/ico/tick.png"/><img ng-show="!tls" 
src="img/ico/cross.png"/></span><span id="authnPlugin">Authentication Plugin: 
<b>{{authenticationPlugin}}</b></span><span id="authzPlugin">Authorization 
Plugin: <b>{{authorizationPlugin}}</b></span></div>
+          <form>
+            <span ng-show="manageUsersEnabled" id="realm-field">
+              <label for="realmName">Realm:&nbsp;</label><input disabled 
class="input-text" type="text" id="realmName" ng-model="realmName">
+            </span>
+            <span id="block-field"><label for="block_unknown">Block anonymous 
requests?</label><input class="input-check" type="checkbox" id="block_unknown" 
ng-model="blockUnknown" ng-change="onBlockUnknownChange()" 
ng-true-value="'true'" ng-false-value="'false'"/><a 
ng-click="showHelp('blockUnknownHelp')"><img class="help-ico" 
src="img/ico/question-white.png"/></a>
+              <div id="blockUnknownHelp" class="help" ng-show="helpId === 
'blockUnknownHelp'">
+                <div class="help-top">
+                  <p>If checked, un-authenticated requests to any Solr 
endpoint are blocked. If un-checked, then any endpoint that is not protected 
with a permission will be accessible by anonymous users. Only disable this 
check if you want to allow un-authenticated access to specific endpoints that 
are configured with <b>role: null</b>. For more information, see:
+                  <div class="help-anchor"><a target="_blank" 
href="https://solr.apache.org/guide/basic-authentication-plugin.html#enable-basic-authentication";>Basic
 Authentication</a></div></p>
+                </div>
+              </div>
+            </span>
+            <span ng-show="manageUsersEnabled" id="forward-field"><label 
for="forward_creds">Forward credentials?</label><input class="input-check" 
type="checkbox" id="forward_creds" ng-model="forwardCredentials" 
ng-change="onForwardCredsChange()" ng-true-value="'true'" 
ng-false-value="'false'"/><a ng-click="showHelp('forwardCredsHelp')"><img 
class="help-ico" src="img/ico/question-white.png"/></a>
+              <div id="forwardCredsHelp" class="help" ng-show="helpId === 
'forwardCredsHelp'">
+                <div class="help-top">
+                  <p>If checked, Solr forwards user credentials when making 
distributed requests to other nodes in the cluster. If un-checked (the 
default), Solr will use the internal PKI authentication mechanism for 
distributed requests. For more information, see:
+                  <div class="help-anchor"><a target="_blank" 
href="https://solr.apache.org/guide/authentication-and-authorization-plugins.html#pkiauthenticationplugin";>PKIAuthenticationPlugin</a></div></p>
+                </div>
+              </div>
+            </span>
+          </form>
+        </div>
+
+        <div id="error-dialog" class="error-dialog" ng-show="securityAPIError">
+          <div id="error-dialog-note"><p class="clearfix"><img 
src="img/ico/prohibition.png"/>&nbsp;{{securityAPIError}}</p></div>
+          <div id="error-dialog-details"><div 
ng-show="securityAPIErrorDetails"><textarea rows="10" 
cols="55">{{securityAPIErrorDetails}}</textarea></div></div>
+          <div id="error-dialog-buttons" class="clearfix">
+            <button type="reset" class="error-button" 
ng-click="closeErrorDialog()"><span>OK</span></button>
+          </div>
+        </div>
+
+        <div id="user-dialog" class="dialog" ng-show="showUserDialog" 
escape-pressed="hideAll()">
+          <div id="user-heading" class="heading">{{userDialogHeader}}<button 
id="delete-user" ng-show="userDialogMode === 'edit'" class="submit" 
ng-click="confirmDeleteUser()"><span>Delete</span></button></div>
+          <form autocomplete="off">
+            <p class="clearfix"><label for="add_user">Username:</label><input 
autocomplete="off" ng-disabled="userDialogMode === 'edit'" class="input-text" 
type="text" id="add_user" ng-model="upsertUser.username" 
focus-when="showAddField" placeholder="enter a username"></p>
+            <p class="clearfix"><label 
for="add_user_roles">Roles:</label><select multiple 
ng-model="upsertUser.selectedRoles" id="add_user_roles" size="5" ng-options="r 
for r in roleNames"></select></p>
+            <p class="clearfix"><label for="add_user">Add New 
Role:</label><input autocomplete="off" ng-disabled="userDialogMode === 'edit'" 
class="input-text" type="text" id="add_user_new_role" 
ng-model="upsertUser.newRole"></p>
+            <p class="clearfix"><label 
for="add_user_password">Password:</label><input autocomplete="off" 
class="input-text" type="password" id="add_user_password" 
ng-model="upsertUser.password" placeholder="enter a strong password"></p>
+            <p class="clearfix"><label for="add_user_password2">Confirm 
password:</label><input autocomplete="off" class="input-text" type="password" 
id="add_user_password2" ng-model="upsertUser.password2" placeholder="re-enter 
password"></p>
+            <div class="formMessageHolder">
+              <p class="validate-error" 
ng-show="validationError"><span>{{validationError}}</span></p>
+            </div>
+            <p class="clearfix buttons">
+              <button type="submit" class="submit" 
ng-click="doUpsertUser()"><span>{{userDialogAction}}</span></button>
+              <button type="reset" class="reset" 
ng-click="toggleUserDialog()"><span>Cancel</span></button>
+            </p>
+          </form>
+        </div>
+
+        <div id="role-dialog" class="dialog" ng-show="showRoleDialog" 
escape-pressed="hideAll()">
+          <div id="role-heading" class="heading">{{roleDialogHeader}}</div>
+          <form autocomplete="off">
+            <p class="clearfix"><label for="add_role">Name:</label><input 
autocomplete="off" ng-disabled="roleDialogMode === 'edit'" class="input-text" 
type="text" id="add_role" ng-model="upsertRole.name" 
focus-when="showRoleDialog" placeholder="enter role name"></p>
+            <p class="clearfix"><label 
for="add_role_users">Users:</label><select multiple 
ng-model="upsertRole.selectedUsers" id="add_role_users" size="8" ng-options="u 
for u in userNames"></select></p>
+            <!-- <p class="clearfix"><label for="add_role_new_user">Add New 
User:</label><input class="input-text" type="text" id="add_role_new_user" 
ng-model="upsertRole.newUser"></p> -->
+            <p class="clearfix"><label for="add_role_perms">Grant 
Permissions:</label><select multiple ng-model="upsertRole.grantedPerms" 
id="add_role_perms" size="8" ng-options="p for p in 
grantPermissionNames"></select></p>
+            <div class="formMessageHolder">
+              <p class="validate-error" 
ng-show="validationError"><span>{{validationError}}</span></p>
+            </div>
+            <p class="clearfix buttons">
+              <button type="submit" class="submit" 
ng-click="doUpsertRole()"><span>{{roleDialogAction}}</span></button>
+              <button type="reset" class="reset" 
ng-click="toggleRoleDialog()"><span>Cancel</span></button>
+            </p>
+          </form>
+        </div>        
+
+        <div id="add-permission-dialog" class="dialog" 
ng-show="showPermDialog" escape-pressed="hideAll()">
+          <div id="perm-heading" class="heading">{{permDialogHeader}}<button 
id="delete-perm" ng-show="permDialogMode === 'edit'" class="submit" 
ng-click="confirmDeletePerm()"><span>Delete</span></button></div>
+          <form>
+            <div class="form-field" ng-show="permDialogMode === 'edit'"><label 
for="add_perm_index">Index:</label><input class="input-text" type="text" 
id="add_perm_index" ng-model="upsertPerm.index"><a 
ng-click="showHelp('permIndexHelp')"><img class="help-ico" 
src="img/ico/question-white.png"/></a>
+              <div id="permIndexHelp" class="help" ng-show="helpId === 
'permIndexHelp'">
+                <div class="help-index">
+                  <p>For requests where multiple permissions match, Solr 
applies the first permission that matches based on a complex ordering logic. In 
general, more specific permissions should be listed earlier in the 
configuration. The permission index (1-based) governs its position in the 
configuration. To re-order a permission, change the index to desired position.
+                  <div class="help-anchor"><a target="_blank" 
href="https://solr.apache.org/guide/rule-based-authorization-plugin.html#permission-ordering-and-resolution";>Permission
 Ordering and Resolution</a></div></p>
+                </div>
+              </div>
+            </div>
+            <div id="perm-select"><label 
for="predefined">Predefined:</label><select id="predefined"
+                chosen
+                ng-change="onPredefinedChanged()"
+                ng-model="selectedPredefinedPermission"
+                ng-disabled="permDialogMode === 'edit'"
+                ng-options="p for p in 
filteredPredefinedPermissions"></select><span id="add_perm_custom">or Custom: 
<input ng-disabled="permDialogMode === 'edit'" 
ng-change="onPredefinedChanged()" type="text" id="add_perm_name" 
ng-model="upsertPerm.name"><a ng-click="showHelp('permDialogHelp')"><img 
class="help-ico" src="img/ico/question-white.png"/></a>
+              <div id="permDialogHelp" class="help" ng-show="helpId === 
'permDialogHelp'">
+                <div class="help-perm">
+                  <p>Permissions allow you to grant access to protected 
resources to one or more roles. Solr provides a list of <b>predefined</b> 
permissions to cover common use cases, such as collection administration. 
Otherwise, you can define a <b>custom permission</b> for fine-grained control 
over the API path(s), collection(s), request method(s) and params.
+                  <div class="help-anchor"><a target="_blank" 
href="https://solr.apache.org/guide/rule-based-authorization-plugin.html#permissions-2";>Rule-based
 Authorization :: Permissions</a></div></p>
+                </div>
+              </div></span>
+            </div>
+            <p class="form-field"><label>Roles:</label><select 
ng-show="manageUserRolesEnabled" multiple ng-model="upsertPerm.selectedRoles" 
id="add_perm_roles" size="5" ng-options="r for r in 
roleNamesWithWildcard"></select><input ng-show="!manageUserRolesEnabled" 
class="input-text" type="text" ng-model="upsertPerm.manualRoles"></p>
+            <p class="form-field"><label 
for="add_perm_collection">Collection:</label><input 
ng-disabled="isPermFieldDisabled" class="input-text" type="text" 
id="add_perm_collection" ng-model="upsertPerm.collection"></p>
+            <p class="form-field"><label 
for="add_perm_path">Path:</label><input ng-disabled="isPermFieldDisabled" 
type="text" id="add_perm_path" ng-model="upsertPerm.path"></p>
+            <div class="form-field"><label>Request Method:</label>
+              <table>
+                <tr><td><input ng-disabled="isPermFieldDisabled" 
class="input-check" type="checkbox" id="add_perm_method_get" 
ng-model="upsertPerm.method.get" ng-true-value="'true'" 
ng-false-value="'false'"/>GET</td></tr>
+                <tr><td><input ng-disabled="isPermFieldDisabled" 
class="input-check" type="checkbox" id="add_perm_method_post" 
ng-model="upsertPerm.method.post" ng-true-value="'true'" 
ng-false-value="'false'"/>POST</td></tr>
+                <tr><td><input ng-disabled="isPermFieldDisabled" 
class="input-check" type="checkbox" id="add_perm_method_put" 
ng-model="upsertPerm.method.put" ng-true-value="'true'" 
ng-false-value="'false'"/>PUT</td></tr>
+                <tr><td><input ng-disabled="isPermFieldDisabled" 
class="input-check" type="checkbox" id="add_perm_method_delete" 
ng-model="upsertPerm.method.delete" ng-true-value="'true'" 
ng-false-value="'false'"/>DELETE</td></tr>
+              </table>
+            </div>
+            <div class="form-field"><label>Request Params:</label>
+              <div id="param-rows">
+                <div class="row clearfix" ng-repeat="p in params">
+                  <input class="param-name" type="text" ng-model="p.name" 
name="paramName" ng-disabled="isPermFieldDisabled">&nbsp;=&nbsp;<input 
class="param-value" type="text" ng-model="p.value" name="paramValue" 
ng-disabled="isPermFieldDisabled">
+                  <div class="param-buttons" ng-show="!isPermFieldDisabled">
+                    <a class="rem" 
ng-click="removeParam($index)"><span></span></a>
+                    <a class="add" 
ng-click="addParam($index)"><span></span></a>
+                  </div>
+                </div>
+              </div>
+            </div>
+            <div class="formMessageHolder">
+              <p class="validate-error" 
ng-show="validationError"><span>{{validationError}}</span></p>
+            </div>
+            <p class="clearfix buttons">
+              <button type="submit" class="submit" 
ng-click="doUpsertPermission()"><span>{{permDialogAction}}</span></button>
+              <button type="reset" class="reset" 
ng-click="togglePermDialog()"><span>Cancel</span></button>
+            </p>
+          </form>
+        </div>
+      </div>
+    </div>
+
+    <div class="block">
+      <div class="users-left" id="users">
+        <h2><span>Users</span></h2>
+        <p ng-show="!manageUsersEnabled" class="external-msg"><img 
src="img/ico/prohibition.png"/>&nbsp;Users are managed by an external 
provider.</p>
+        <div id="users-content" ng-show="manageUsersEnabled">
+          <div id="user-filters">
+            Filter users by:&nbsp;<select id="user-filter-type" 
ng-model="userFilter" ng-change="applyUserFilter()">
+            <option value=""></option>
+            <option value="name">name</option>
+            <option value="role">role</option>
+            <option value="path">path</option>
+            <option value="perm">permission</option>
+            </select>
+            <select ng-show="userFilter==='role'||userFilter==='perm'" 
id="user-filter-options" ng-model="userFilterOption" 
ng-change="onUserFilterOptionChanged()" ng-options="option for option in 
userFilterOptions"></select>
+            <input ng-show="userFilter==='name'||userFilter==='path'" 
type="text" ng-model="userFilterText" id="user-filter-text" 
ng-change="onUserFilterTextChanged()"/>
+            <div id="user-actions">
+              <button id="add-user" class="action" 
ng-click="showAddUserDialog()" ng-show="hasSecurityEditPerm"><span>Add 
User</span></button>
+            </div>
+          </div>
+
+          <div id="users-table">
+          <table border="0" cellspacing="0" cellpadding="0">
+            <tbody>
+            <tr ng-class="{odd:$odd}">
+              <th class="table-hdr td-name">Username</th>
+              <th class="table-hdr td-roles">Roles</th>
+            </tr>
+            <tr class="editable" ng-repeat="u in filteredUsers" 
ng-class="{odd:$odd}" ng-click="editUser(u)">
+              <td class="table-data">{{u.username}}</td>
+              <td class="table-data">{{displayList(u.roles)}}</td>
+            </tr>
+            </tbody>
+          </table>
+          </div>
+        </div>
+      </div>
+
+      <div class="roles-right" id="roles">
+        <h2><span>Roles</span></h2>
+        <p ng-show="!manageUserRolesEnabled" class="external-msg"><img 
src="img/ico/prohibition.png"/>&nbsp;Roles are managed by an external 
provider.</p>
+        <div id="roles-content" ng-show="manageUserRolesEnabled">
+          <div id="role-filters">
+            Filter roles by:&nbsp;<select id="role-filter-type" 
ng-model="roleFilter" ng-change="applyRoleFilter()">
+            <option value=""></option>
+            <option value="name">name</option>
+            <option value="user">user</option>
+            <option value="path">path</option>
+            <option value="perm">permission</option>
+          </select>
+            <select ng-show="roleFilter==='user'||roleFilter==='perm'" 
id="role-filter-options" ng-model="roleFilterOption" 
ng-change="onRoleFilterOptionChanged()" ng-options="option for option in 
roleFilterOptions"></select>
+            <input ng-show="roleFilter==='name'||roleFilter==='path'" 
type="text" ng-model="roleFilterText" id="role-filter-text" 
ng-change="onRoleFilterTextChanged()"/>
+            <div id="role-actions">
+              <button id="add-role" class="action" 
ng-click="showAddRoleDialog()" ng-show="hasSecurityEditPerm"><span>Add 
Role</span></button>
+            </div>
+          </div>
+
+          <div id="roles-table">
+          <table border="0" cellspacing="0" cellpadding="0">
+            <tbody>
+            <tr ng-class="{odd:$odd}">
+              <th class="table-hdr td-role">Role</th>
+              <th class="table-hdr td-roles">Users</th>
+            </tr>
+            <tr class="editable" ng-repeat="r in filteredRoles" 
ng-class="{odd:$odd}" ng-click="editRole(r)">
+              <td class="table-data">{{r.name}}</td>
+              <td class="table-data">{{displayList(r.users)}}</td>
+            </tr>
+            </tbody>
+          </table>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="block" id="permissions">
+      <h2><span>Permissions</span></h2>
+      <div id="permissions-content">
+        <div id="perm-filters">
+          Filter permissions by:&nbsp;<select id="perm-filter-type" 
ng-model="permFilter" ng-change="applyPermFilter()" ng-options="opt for opt in 
permFilterTypes"></select>
+          <select 
ng-show="permFilter==='role'||permFilter==='user'||permFilter==='collection'" 
id="perm-filter-options" ng-model="permFilterOption" 
ng-change="onPermFilterOptionChanged()" ng-options="option for option in 
permFilterOptions"></select>
+          <input ng-show="permFilter==='name'||permFilter==='path'" 
type="text" ng-model="permFilterText" id="perm-filter-text" 
ng-change="onPermFilterTextChanged()"/>
+        <div id="perm-actions">
+          <button id="add-permission" class="action" 
ng-click="showAddPermDialog()" ng-show="hasSecurityEditPerm && 
managePermissionsEnabled"><span>Add Permission</span></button>
+        </div>
+        </div>
+
+        <div id="perms-table">
+        <table border="0" cellspacing="0" cellpadding="0">
+          <tbody>
+          <tr ng-class="{odd:$odd}">
+            <th class="table-hdr td-name">Name</th>
+            <th class="table-hdr td-roles">Roles</th>
+            <th class="table-hdr td-coll">Collection</th>
+            <th class="table-hdr td-path">Path</th>
+            <th class="table-hdr td-method">Method</th>
+            <th class="table-hdr td-params">Params</th>
+          </tr>
+          <tr class="editable" ng-repeat="p in filteredPerms" 
ng-class="{odd:$odd}" ng-click="editPerm(p)">
+            <td class="table-data">{{p.name}}</td>
+            <td class="table-data">{{displayRoles(p.roles)}}</td>
+            <td class="table-data">{{p.collectionNames}}</td>
+            <td class="table-data">{{displayList(p.paths)}}</td>
+            <td class="table-data">{{displayList(p.method)}}</td>
+            <td class="table-data">{{displayParams(p.params)}}</td>
+          </tr>
+          </tbody>
+        </table>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

Reply via email to