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 <[email protected]>
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"/> 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"/> 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"/> 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: </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"/> {{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"> = <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"/> Users are managed by an external
provider.</p>
+ <div id="users-content" ng-show="manageUsersEnabled">
+ <div id="user-filters">
+ Filter users by: <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"/> Roles are managed by an external
provider.</p>
+ <div id="roles-content" ng-show="manageUserRolesEnabled">
+ <div id="role-filters">
+ Filter roles by: <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: <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>