SOLR-7896: Add a login page to Admin UI, with initial support for Basic Auth

This commit is contained in:
Jan Høydahl 2018-12-12 10:35:15 +01:00
parent 5affe7421f
commit 280f67927e
17 changed files with 859 additions and 118 deletions

View File

@ -155,6 +155,8 @@ New Features
* SOLR-12839: JSON 'terms' Faceting now supports a 'prelim_sort' option to use when initially selecting * SOLR-12839: JSON 'terms' Faceting now supports a 'prelim_sort' option to use when initially selecting
the top ranking buckets, prior to the final 'sort' option used after refinement. (hossman) the top ranking buckets, prior to the final 'sort' option used after refinement. (hossman)
* SOLR-7896: Add a login page to Admin UI, with initial support for Basic Auth (janhoy)
Bug Fixes Bug Fixes
---------------------- ----------------------

View File

@ -46,6 +46,12 @@ Copyright (c) 2008-2014, Ryan McGeary, https://github.com/rmm5t/jquery-timeago
This product includes require.js Javascript library created by James Burke This product includes require.js Javascript library created by James Burke
Copyright (C) 2010-2014 James Burke, https://github.com/jrburke/requirejs Copyright (C) 2010-2014 James Burke, https://github.com/jrburke/requirejs
This product includes angular-utf8-base64.js Javascript library created by Andrey Bezyazychniy
Copyright (c) 2014 Andrey Bezyazychniy, https://github.com/stranger82/angular-utf8-base64
This product includes code copied and modified from the www-authenticate Javascript library
Copyright (c) 2013 Randy McLaughlin, MIT-license, https://github.com/randymized/www-authenticate
This product includes fugue icons created by Yusuke Kamiyamane This product includes fugue icons created by Yusuke Kamiyamane
Copyright (C) 2013-2014 Yusuke Kamiyamane, https://github.com/yusukekamiyamane/fugue-icons Copyright (C) 2013-2014 Yusuke Kamiyamane, https://github.com/yusukekamiyamane/fugue-icons

View File

@ -34,12 +34,13 @@ import java.util.StringTokenizer;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.auth.BasicUserPrincipal; import org.apache.http.auth.BasicUserPrincipal;
import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHeader;
import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.ValidatingJsonMap;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.SpecProvider; import org.apache.solr.common.SpecProvider;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.ValidatingJsonMap;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -47,6 +48,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private AuthenticationProvider authenticationProvider; private AuthenticationProvider authenticationProvider;
private final static ThreadLocal<Header> authHeader = new ThreadLocal<>(); private final static ThreadLocal<Header> authHeader = new ThreadLocal<>();
private static final String X_REQUESTED_WITH_HEADER = "X-Requested-With";
private boolean blockUnknown = false; private boolean blockUnknown = false;
public boolean authenticate(String username, String pwd) { public boolean authenticate(String username, String pwd) {
@ -55,7 +57,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
@Override @Override
public void init(Map<String, Object> pluginConfig) { public void init(Map<String, Object> pluginConfig) {
Object o = pluginConfig.get(BLOCK_UNKNOWN); Object o = pluginConfig.get(PROPERTY_BLOCK_UNKNOWN);
if (o != null) { if (o != null) {
try { try {
blockUnknown = Boolean.parseBoolean(o.toString()); blockUnknown = Boolean.parseBoolean(o.toString());
@ -94,9 +96,18 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
return provider; return provider;
} }
private void authenticationFailure(HttpServletResponse response, String message) throws IOException { private void authenticationFailure(HttpServletResponse response, boolean isAjaxRequest, String message) throws IOException {
for (Map.Entry<String, String> entry : authenticationProvider.getPromptHeaders().entrySet()) { for (Map.Entry<String, String> entry : authenticationProvider.getPromptHeaders().entrySet()) {
response.setHeader(entry.getKey(), entry.getValue()); String value = entry.getValue();
// Prevent browser from intercepting basic authentication header when reqeust from Admin UI
if (isAjaxRequest && HttpHeaders.WWW_AUTHENTICATE.equalsIgnoreCase(entry.getKey()) && value != null) {
if (value.startsWith("Basic ")) {
value = "x" + value;
log.debug("Prefixing {} header for Basic Auth with 'x' to prevent browser basic auth popup",
HttpHeaders.WWW_AUTHENTICATE);
}
}
response.setHeader(entry.getKey(), value);
} }
response.sendError(401, message); response.sendError(401, message);
} }
@ -108,6 +119,8 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
HttpServletResponse response = (HttpServletResponse) servletResponse; HttpServletResponse response = (HttpServletResponse) servletResponse;
String authHeader = request.getHeader("Authorization"); String authHeader = request.getHeader("Authorization");
boolean isAjaxRequest = isAjaxRequest(request);
if (authHeader != null) { if (authHeader != null) {
BasicAuthPlugin.authHeader.set(new BasicHeader("Authorization", authHeader)); BasicAuthPlugin.authHeader.set(new BasicHeader("Authorization", authHeader));
StringTokenizer st = new StringTokenizer(authHeader); StringTokenizer st = new StringTokenizer(authHeader);
@ -122,7 +135,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
String pwd = credentials.substring(p + 1).trim(); String pwd = credentials.substring(p + 1).trim();
if (!authenticate(username, pwd)) { if (!authenticate(username, pwd)) {
log.debug("Bad auth credentials supplied in Authorization header"); log.debug("Bad auth credentials supplied in Authorization header");
authenticationFailure(response, "Bad credentials"); authenticationFailure(response, isAjaxRequest, "Bad credentials");
} else { } else {
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) { HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
@Override @Override
@ -135,7 +148,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
} }
} else { } else {
authenticationFailure(response, "Invalid authentication token"); authenticationFailure(response, isAjaxRequest, "Invalid authentication token");
} }
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new Error("Couldn't retrieve authentication", e); throw new Error("Couldn't retrieve authentication", e);
@ -144,7 +157,7 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
} }
} else { } else {
if (blockUnknown) { if (blockUnknown) {
authenticationFailure(response, "require authentication"); authenticationFailure(response, isAjaxRequest, "require authentication");
} else { } else {
request.setAttribute(AuthenticationPlugin.class.getName(), authenticationProvider.getPromptHeaders()); request.setAttribute(AuthenticationPlugin.class.getName(), authenticationProvider.getPromptHeaders());
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
@ -180,8 +193,16 @@ public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEdita
return blockUnknown; return blockUnknown;
} }
public static final String BLOCK_UNKNOWN = "blockUnknown"; public static final String PROPERTY_BLOCK_UNKNOWN = "blockUnknown";
private static final Set<String> PROPS = ImmutableSet.of(BLOCK_UNKNOWN); public static final String PROPERTY_REALM = "realm";
private static final Set<String> PROPS = ImmutableSet.of(PROPERTY_BLOCK_UNKNOWN, PROPERTY_REALM);
/**
* Check if the request is an AJAX request, i.e. from the Admin UI or other SPA front
* @param request the servlet request
* @return true if the request is AJAX request
*/
private boolean isAjaxRequest(HttpServletRequest request) {
return "XMLHttpRequest".equalsIgnoreCase(request.getHeader(X_REQUESTED_WITH_HEADER));
}
} }

View File

@ -64,8 +64,11 @@ public class Sha256AuthenticationProvider implements ConfigEditablePlugin, Basi
@Override @Override
public void init(Map<String, Object> pluginConfig) { public void init(Map<String, Object> pluginConfig) {
if (pluginConfig.get("realm") != null) this.realm = (String) pluginConfig.get("realm"); if (pluginConfig.containsKey(BasicAuthPlugin.PROPERTY_REALM)) {
else this.realm = "solr"; this.realm = (String) pluginConfig.get(BasicAuthPlugin.PROPERTY_REALM);
} else {
this.realm = "solr";
}
promptHeader = Collections.unmodifiableMap(Collections.singletonMap("WWW-Authenticate", "Basic realm=\"" + realm + "\"")); promptHeader = Collections.unmodifiableMap(Collections.singletonMap("WWW-Authenticate", "Basic realm=\"" + realm + "\""));
credentials = new LinkedHashMap<>(); credentials = new LinkedHashMap<>();

View File

@ -360,18 +360,6 @@ public class SolrDispatchFilter extends BaseSolrFilter {
} }
} }
AtomicReference<HttpServletRequest> wrappedRequest = new AtomicReference<>();
if (!authenticateRequest(request, response, wrappedRequest)) { // the response and status code have already been sent
return;
}
if (wrappedRequest.get() != null) {
request = wrappedRequest.get();
}
if (cores.getAuthenticationPlugin() != null) {
log.debug("User principal: {}", request.getUserPrincipal());
}
// No need to even create the HttpSolrCall object if this path is excluded. // No need to even create the HttpSolrCall object if this path is excluded.
if (excludePatterns != null) { if (excludePatterns != null) {
String requestPath = request.getServletPath(); String requestPath = request.getServletPath();
@ -389,6 +377,18 @@ public class SolrDispatchFilter extends BaseSolrFilter {
} }
} }
AtomicReference<HttpServletRequest> wrappedRequest = new AtomicReference<>();
if (!authenticateRequest(request, response, wrappedRequest)) { // the response and status code have already been sent
return;
}
if (wrappedRequest.get() != null) {
request = wrappedRequest.get();
}
if (cores.getAuthenticationPlugin() != null) {
log.debug("User principal: {}", request.getUserPrincipal());
}
HttpSolrCall call = getHttpSolrCall(request, response, retry); HttpSolrCall call = getHttpSolrCall(request, response, retry);
ExecutorUtil.setServerThreadFlag(Boolean.TRUE); ExecutorUtil.setServerThreadFlag(Boolean.TRUE);
try { try {
@ -458,8 +458,20 @@ public class SolrDispatchFilter extends BaseSolrFilter {
// /admin/info/key must be always open. see SOLR-9188 // /admin/info/key must be always open. see SOLR-9188
// tests work only w/ getPathInfo // tests work only w/ getPathInfo
//otherwise it's just enough to have getServletPath() //otherwise it's just enough to have getServletPath()
if (PublicKeyHandler.PATH.equals(request.getServletPath()) || String requestPath = request.getPathInfo();
PublicKeyHandler.PATH.equals(request.getPathInfo())) return true; if (requestPath == null)
requestPath = request.getServletPath();
if (PublicKeyHandler.PATH.equals(requestPath)) {
if (log.isDebugEnabled())
log.debug("Pass through PKI authentication endpoint");
return true;
}
// /solr/ (Admin UI) must be always open to allow displaying Admin UI with login page
if ("/solr/".equals(requestPath) || "/".equals(requestPath)) {
if (log.isDebugEnabled())
log.debug("Pass through Admin UI entry point");
return true;
}
String header = request.getHeader(PKIAuthenticationPlugin.HEADER); String header = request.getHeader(PKIAuthenticationPlugin.HEADER);
if (header != null && cores.getPkiAuthenticationPlugin() != null) if (header != null && cores.getPkiAuthenticationPlugin() != null)
authenticationPlugin = cores.getPkiAuthenticationPlugin(); authenticationPlugin = cores.getPkiAuthenticationPlugin();

View File

@ -157,6 +157,16 @@ Solr has one implementation of an authorization plugin:
* <<rule-based-authorization-plugin.adoc#rule-based-authorization-plugin,Rule-Based Authorization Plugin>> * <<rule-based-authorization-plugin.adoc#rule-based-authorization-plugin,Rule-Based Authorization Plugin>>
== Authenticating in the Admin UI
Whenever an authentication plugin is enabled, authentication is also required for all or some operations in the Admin UI. The Admin UI is an AngularJS application running inside your browser, and is treated as any other external client by Solr.
When authentication is required the Admin UI will presented you with a login dialogue. The authentication plugins currently supported by the Admin UI are:
* `BasicAuthPlugin`
If your plugin of choice is not supported, you will have to interact with Solr sending HTTP requests instead of through the graphical user interface of the Admin UI. All operations supported by Admin UI can be performed through Solr's RESTful APIs.
== Securing Inter-Node Requests == Securing Inter-Node Requests
There are a lot of requests that originate from the Solr nodes itself. For example, requests from overseer to nodes, recovery threads, etc. Each Authentication plugin declares whether it is capable of securing inter-node requests or not. If not, Solr will fall back to using a special internode authentication mechanism where each Solr node is a super user and is fully trusted by other Solr nodes, described below. There are a lot of requests that originate from the Solr nodes itself. For example, requests from overseer to nodes, recovery threads, etc. Each Authentication plugin declares whether it is capable of securing inter-node requests or not. If not, Solr will fall back to using a special internode authentication mechanism where each Solr node is a super user and is fully trusted by other Solr nodes, described below.

View File

@ -36,13 +36,14 @@ An example `security.json` showing both sections is shown below to show how thes
"authentication":{ <1> "authentication":{ <1>
"blockUnknown": true, <2> "blockUnknown": true, <2>
"class":"solr.BasicAuthPlugin", "class":"solr.BasicAuthPlugin",
"credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="} <3> "credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="}, <3>
"realm":"My Solr users" <4>
}, },
"authorization":{ "authorization":{
"class":"solr.RuleBasedAuthorizationPlugin", "class":"solr.RuleBasedAuthorizationPlugin",
"permissions":[{"name":"security-edit", "permissions":[{"name":"security-edit",
"role":"admin"}], <4> "role":"admin"}], <5>
"user-role":{"solr":"admin"} <5> "user-role":{"solr":"admin"} <6>
}} }}
---- ----
@ -51,13 +52,16 @@ There are several things defined in this file:
<1> Basic authentication and rule-based authorization plugins are enabled. <1> Basic authentication and rule-based authorization plugins are enabled.
<2> The parameter `"blockUnknown":true` means that unauthenticated requests are not allowed to pass through. <2> The parameter `"blockUnknown":true` means that unauthenticated requests are not allowed to pass through.
<3> A user called 'solr', with a password `'SolrRocks'` has been defined. <3> A user called 'solr', with a password `'SolrRocks'` has been defined.
<4> The 'admin' role has been defined, and it has permission to edit security settings. <4> We override the `realm` property to display another text on the login prompt.
<5> The 'solr' user has been defined to the 'admin' role. <5> The 'admin' role has been defined, and it has permission to edit security settings.
<6> The 'solr' user has been defined to the 'admin' role.
Save your settings to a file called `security.json` locally. If you are using Solr in standalone mode, you should put this file in `$SOLR_HOME`. Save your settings to a file called `security.json` locally. If you are using Solr in standalone mode, you should put this file in `$SOLR_HOME`.
If `blockUnknown` does not appear in the `security.json` file, it will default to `false`. This has the effect of not requiring authentication at all. In some cases, you may want this; for example, if you want to have `security.json` in place but aren't ready to enable authentication. However, you will want to ensure that this parameter is set to `true` in order for authentication to be truly enabled in your system. If `blockUnknown` does not appear in the `security.json` file, it will default to `false`. This has the effect of not requiring authentication at all. In some cases, you may want this; for example, if you want to have `security.json` in place but aren't ready to enable authentication. However, you will want to ensure that this parameter is set to `true` in order for authentication to be truly enabled in your system.
If `realm` is not defined, it will default to `solr`.
If you are using SolrCloud, you must upload `security.json` to ZooKeeper. You can use this example command, ensuring that the ZooKeeper port is correct: If you are using SolrCloud, you must upload `security.json` to ZooKeeper. You can use this example command, ensuring that the ZooKeeper port is correct:
[source,bash] [source,bash]
@ -139,11 +143,11 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
=== Set a Property === Set a Property
Set properties for the authentication plugin. The only currently supported property for the Basic Authentication plugin is `blockUnknown`. Set properties for the authentication plugin. The currently supported properties for the Basic Authentication plugin are `blockUnknown` and `realm`.
[.dynamic-tabs] [.dynamic-tabs]
-- --
[example.tab-pane#v1set-property] [example.tab-pane#v1set-property-blockUnknown]
==== ====
[.tab-label]*V1 API* [.tab-label]*V1 API*
@ -153,7 +157,7 @@ curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'C
---- ----
==== ====
[example.tab-pane#v2set-property] [example.tab-pane#v2set-property-blockUnknown]
==== ====
[.tab-label]*V2 API* [.tab-label]*V2 API*
@ -164,6 +168,31 @@ curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentica
==== ====
-- --
The authentication realm defaults to `solr` and is displayed in the `WWW-Authenticate` HTTP header and in the Admin UI login page. To change the realm, set the `realm` property:
[.dynamic-tabs]
--
[example.tab-pane#v1set-property-realm]
====
[.tab-label]*V1 API*
[source,bash]
----
curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'Content-type:application/json' -d '{"set-property": {"realm":"My Solr users"}}'
----
====
[example.tab-pane#v2set-property-realm]
====
[.tab-label]*V2 API*
[source,bash]
----
curl --user solr:SolrRocks http://localhost:8983/api/cluster/security/authentication -H 'Content-type:application/json' -d '{"set-property": {"realm":"My Solr users"}}'
----
====
--
== Using Basic Auth with SolrJ == Using Basic Auth with SolrJ
In SolrJ, the basic authentication credentials need to be set for each request as in this example: In SolrJ, the basic authentication credentials need to be set for each request as in this example:

View File

@ -33,7 +33,7 @@
--> -->
<init-param> <init-param>
<param-name>excludePatterns</param-name> <param-name>excludePatterns</param-name>
<param-value>/partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/tpl/.+</param-value> <param-value>/partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/templates/.+</param-value>
</init-param> </init-param>
</filter> </filter>

View File

@ -0,0 +1,103 @@
/*
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.
*/
#content #login
{
background-position: 0 50%;
padding-left: 21px;
vertical-align: center;
horiz-align: center;
}
#content #login h1,
#content #login .h1 {
font-size: 2.5rem;
}
#content #login h2,
#content #login .h2 {
font-size: 2rem;
}
#content #login p
{
margin-top: 0;
margin-bottom: 1rem;
}
#content #login .login-error
{
font-size: 1rem;
color: red;
margin-top: 10px;
margin-bottom: 10px;
}
#content #login button {
border-radius: 0;
}
#content #login button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
#content #login .btn {
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
#content #login .form-inline .form-group {
display: -ms-flexbox;
display: flex;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
-ms-flex-flow: row wrap;
flex-flow: row wrap;
-ms-flex-align: center;
align-items: center;
margin-bottom: 0;
}
#content #login .form-control {
display: block;
width: 80%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}

View File

@ -247,6 +247,8 @@ limitations under the License.
#menu #index.global p a { background-image: url( ../../img/ico/dashboard.png ); } #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 #logging.global p a { background-image: url( ../../img/ico/inbox-document-text.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 ); } #menu #logging.global .level a { background-image: url( ../../img/ico/gear.png ); }

View File

@ -34,6 +34,7 @@ limitations under the License.
<link rel="stylesheet" type="text/css" href="css/angular/index.css?_=${version}"> <link rel="stylesheet" type="text/css" href="css/angular/index.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/java-properties.css?_=${version}"> <link rel="stylesheet" type="text/css" href="css/angular/java-properties.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/logging.css?_=${version}"> <link rel="stylesheet" type="text/css" href="css/angular/logging.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/login.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/menu.css?_=${version}"> <link rel="stylesheet" type="text/css" href="css/angular/menu.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/plugins.css?_=${version}"> <link rel="stylesheet" type="text/css" href="css/angular/plugins.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/documents.css?_=${version}"> <link rel="stylesheet" type="text/css" href="css/angular/documents.css?_=${version}">
@ -61,9 +62,11 @@ limitations under the License.
<script src="libs/highlight.js"></script> <script src="libs/highlight.js"></script>
<script src="libs/d3.js"></script> <script src="libs/d3.js"></script>
<script src="libs/jquery-ui.min.js"></script> <script src="libs/jquery-ui.min.js"></script>
<script src="libs/angular-utf8-base64.min.js"></script>
<script src="js/angular/app.js"></script> <script src="js/angular/app.js"></script>
<script src="js/angular/services.js"></script> <script src="js/angular/services.js"></script>
<script src="js/angular/controllers/index.js"></script> <script src="js/angular/controllers/index.js"></script>
<script src="js/angular/controllers/login.js"></script>
<script src="js/angular/controllers/logging.js"></script> <script src="js/angular/controllers/logging.js"></script>
<script src="js/angular/controllers/cloud.js"></script> <script src="js/angular/controllers/cloud.js"></script>
<script src="js/angular/controllers/collections.js"></script> <script src="js/angular/controllers/collections.js"></script>
@ -139,88 +142,91 @@ limitations under the License.
<div> <div>
<ul id="menu"> <ul id="menu">
<li id="login" class="global" ng-class="{active:page=='login'}" ng-show="http401 || currentUser"><p><a href="#/login">{{http401 ? "Login" : "Logout " + currentUser}}</a></p></li>
<li id="index" class="global" ng-class="{active:page=='index'}"><p><a href="#/">Dashboard</a></p></li>
<div ng-show="!http401">
<li id="logging" class="global" ng-class="{active:page=='logging'}"><p><a href="#/~logging">Logging</a></p> <li id="index" class="global" ng-class="{active:page=='index'}"><p><a href="#/">Dashboard</a></p></li>
<ul ng-show="showingLogging">
<li class="level" ng-class="{active:page=='logging-levels'}"><a href="#/~logging/level">Level</a></li> <li id="logging" class="global" ng-class="{active:page=='logging'}"><p><a href="#/~logging">Logging</a></p>
</ul> <ul ng-show="showingLogging">
</li> <li class="level" ng-class="{active:page=='logging-levels'}"><a href="#/~logging/level">Level</a></li>
</ul>
<li id="cloud" class="global optional" ng-show="isCloudEnabled" ng-class="{active:showingCloud}"><p><a href="#/~cloud">Cloud</a></p> </li>
<ul ng-show="showingCloud">
<li class="nodes" ng-class="{active:page=='cloud-nodes'}"><a href="#/~cloud?view=nodes">Nodes</a></li> <li id="cloud" class="global optional" ng-show="isCloudEnabled" ng-class="{active:showingCloud}"><p><a href="#/~cloud">Cloud</a></p>
<li class="tree" ng-class="{active:page=='cloud-tree'}"><a href="#/~cloud?view=tree">Tree</a></li> <ul ng-show="showingCloud">
<li class="zkstatus" ng-class="{active:page=='cloud-zkstatus'}"><a href="#/~cloud?view=zkstatus">ZK Status</a></li> <li class="nodes" ng-class="{active:page=='cloud-nodes'}"><a href="#/~cloud?view=nodes">Nodes</a></li>
<li class="graph" ng-class="{active:page=='cloud-graph'}"><a href="#/~cloud?view=graph">Graph</a></li> <li class="tree" ng-class="{active:page=='cloud-tree'}"><a href="#/~cloud?view=tree">Tree</a></li>
</ul> <li class="zkstatus" ng-class="{active:page=='cloud-zkstatus'}"><a href="#/~cloud?view=zkstatus">ZK Status</a></li>
</li> <li class="graph" ng-class="{active:page=='cloud-graph'}"><a href="#/~cloud?view=graph">Graph</a></li>
</ul>
<li ng-show="isCloudEnabled" id="collections" class="global" ng-class="{active:page=='collections'}"><p><a href="#/~collections">Collections</a></p></li> </li>
<li ng-hide="isCloudEnabled" id="cores" class="global" ng-class="{active:page=='cores'}"><p><a href="#/~cores">Core Admin</a></p></li>
<li ng-show="isCloudEnabled" id="collections" class="global" ng-class="{active:page=='collections'}"><p><a href="#/~collections">Collections</a></p></li>
<li id="java-properties" class="global" ng-class="{active:page=='java-props'}"><p><a href="#/~java-properties">Java Properties</a></li> <li ng-hide="isCloudEnabled" id="cores" class="global" ng-class="{active:page=='cores'}"><p><a href="#/~cores">Core Admin</a></p></li>
<li id="threads" class="global" ng-class="{active:page=='threads'}"><p><a href="#/~threads">Thread Dump</a></p></li> <li id="java-properties" class="global" ng-class="{active:page=='java-props'}"><p><a href="#/~java-properties">Java Properties</a></li>
<li ng-show="isCloudEnabled" id="cluster-suggestions" class="global" ng-class="{active:page=='cluster-suggestions'}"><p><a href="#/~cluster-suggestions">Suggestions</a></p></li>
<li id="threads" class="global" ng-class="{active:page=='threads'}"><p><a href="#/~threads">Thread Dump</a></p></li>
<li ng-show="isCloudEnabled" id="cluster-suggestions" class="global" ng-class="{active:page=='cluster-suggestions'}"><p><a href="#/~cluster-suggestions">Suggestions</a></p></li>
</div>
</ul> </ul>
<div id="collection-selector" ng-show="isCloudEnabled"> <div ng-show="!http401">
<div id="has-collections" ng-show="collections.length!=0"> <div id="collection-selector" ng-show="isCloudEnabled">
<select data-placeholder="Collection Selector" <div id="has-collections" ng-show="collections.length!=0">
ng-model="currentCollection" <select data-placeholder="Collection Selector"
chosen ng-model="currentCollection"
ng-change="showCollection(currentCollection)" chosen
ng-options="collection.name for collection in collections"></select> ng-change="showCollection(currentCollection)"
ng-options="collection.name for collection in collections"></select>
</div>
<p id="has-no-collections" ng-show="collections.length==0"><a href="#/~collections">
No collections available
<span>Go and create one</span>
</a></p>
</div> </div>
<p id="has-no-collections" ng-show="collections.length==0"><a href="#/~collections"> <div id="collection-menu" class="sub-menu" ng-show="currentCollection">
No collections available <ul>
<span>Go and create one</span> <li class="overview" ng-class="{active:page=='collection-overview'}"><a href="#/{{currentCollection.name}}/collection-overview"><span>Overview</span></a></li>
</a></p> <li class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCollection.name}}/analysis"><span>Analysis</span></a></li>
</div> <li class="dataimport" ng-class="{active:page=='dataimport'}"><a href="#/{{currentCollection.name}}/dataimport"><span>Dataimport</span></a></li>
<div id="collection-menu" class="sub-menu" ng-show="currentCollection"> <li class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCollection.name}}/documents"><span>Documents</span></a></li>
<ul> <li class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCollection.name}}/files"><span>Files</span></a></li>
<li class="overview" ng-class="{active:page=='collection-overview'}"><a href="#/{{currentCollection.name}}/collection-overview"><span>Overview</span></a></li> <li class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCollection.name}}/query"><span>Query</span></a></li>
<li class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCollection.name}}/analysis"><span>Analysis</span></a></li> <li class="stream" ng-class="{active:page=='stream'}"><a href="#/{{currentCollection.name}}/stream"><span>Stream</span></a></li>
<li class="dataimport" ng-class="{active:page=='dataimport'}"><a href="#/{{currentCollection.name}}/dataimport"><span>Dataimport</span></a></li> <li class="schema" ng-class="{active:page=='schema'}"><a href="#/{{currentCollection.name}}/schema"><span>Schema</span></a></li>
<li class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCollection.name}}/documents"><span>Documents</span></a></li> </ul>
<li class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCollection.name}}/files"><span>Files</span></a></li> </div>
<li class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCollection.name}}/query"><span>Query</span></a></li> <div id="core-selector">
<li class="stream" ng-class="{active:page=='stream'}"><a href="#/{{currentCollection.name}}/stream"><span>Stream</span></a></li> <div id="has-cores" ng-show="cores.length!=0">
<li class="schema" ng-class="{active:page=='schema'}"><a href="#/{{currentCollection.name}}/schema"><span>Schema</span></a></li> <select data-placeholder="Core Selector"
</ul> ng-model="currentCore"
</div> chosen
<div id="core-selector"> ng-change="showCore(currentCore)"
<div id="has-cores" ng-show="cores.length!=0"> ng-options="core.name for core in cores"></select>
<select data-placeholder="Core Selector" </div>
ng-model="currentCore" <p id="has-no-cores" ng-show="cores.length==0"><a href="#/~cores">
chosen No cores available
ng-change="showCore(currentCore)" <span>Go and create one</span>
ng-options="core.name for core in cores"></select> </a></p>
</div>
<div id="core-menu" class="sub-menu" ng-show="currentCore">
<ul>
<li class="overview" ng-class="{active:page=='overview'}"><a href="#/{{currentCore.name}}/core-overview"><span>Overview</span></a></li>
<li ng-hide="isCloudEnabled" class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCore.name}}/analysis"><span>Analysis</span></a></li>
<li ng-hide="isCloudEnabled" class="dataimport" ng-class="{active:page=='dataimport'}"><a href="#/{{currentCore.name}}/dataimport"><span>Dataimport</span></a></li>
<li ng-hide="isCloudEnabled" class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCore.name}}/documents"><span>Documents</span></a></li>
<li ng-hide="isCloudEnabled" class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCore.name}}/files"><span>Files</span></a></li>
<li class="ping" ng-class="{active:page=='ping'}"><a ng-click="ping()"><span>Ping</span><small class="qtime" ng-show="showPing"> (<span>{{pingMS}}ms</span>)</small></a></li>
<li class="plugins" ng-class="{active:page=='plugins'}"><a href="#/{{currentCore.name}}/plugins"><span>Plugins / Stats</span></a></li>
<li ng-hide="isCloudEnabled" class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCore.name}}/query"><span>Query</span></a></li>
<li ng-hide="isCloudEnabled" class="replication" ng-class="{active:page=='replication'}"><a href="#/{{currentCore.name}}/replication"><span>Replication</span></a></li>
<li ng-hide="isCloudEnabled" class="schema" ng-class="{active:page=='schema'}"><a href="#/{{currentCore.name}}/schema"><span>Schema</span></a></li>
<li class="segments" ng-class="{active:page=='segments'}"><a href="#/{{currentCore.name}}/segments"><span>Segments info</span></a></li>
</ul>
</div> </div>
<p id="has-no-cores" ng-show="cores.length==0"><a href="#/~cores">
No cores available
<span>Go and create one</span>
</a></p>
</div> </div>
<div id="core-menu" class="sub-menu" ng-show="currentCore">
<ul>
<li class="overview" ng-class="{active:page=='overview'}"><a href="#/{{currentCore.name}}"><span>Overview</span></a></li>
<li ng-hide="isCloudEnabled" class="analysis" ng-class="{active:page=='analysis'}"><a href="#/{{currentCore.name}}/analysis"><span>Analysis</span></a></li>
<li ng-hide="isCloudEnabled" class="dataimport" ng-class="{active:page=='dataimport'}"><a href="#/{{currentCore.name}}/dataimport"><span>Dataimport</span></a></li>
<li ng-hide="isCloudEnabled" class="documents" ng-class="{active:page=='documents'}"><a href="#/{{currentCore.name}}/documents"><span>Documents</span></a></li>
<li ng-hide="isCloudEnabled" class="files" ng-class="{active:page=='files'}"><a href="#/{{currentCore.name}}/files"><span>Files</span></a></li>
<li class="ping" ng-class="{active:page=='ping'}"><a ng-click="ping()"><span>Ping</span><small class="qtime" ng-show="showPing"> (<span>{{pingMS}}ms</span>)</small></a></li>
<li class="plugins" ng-class="{active:page=='plugins'}"><a href="#/{{currentCore.name}}/plugins"><span>Plugins / Stats</span></a></li>
<li ng-hide="isCloudEnabled" class="query" ng-class="{active:page=='query'}"><a href="#/{{currentCore.name}}/query"><span>Query</span></a></li>
<li ng-hide="isCloudEnabled" class="replication" ng-class="{active:page=='replication'}"><a href="#/{{currentCore.name}}/replication"><span>Replication</span></a></li>
<li ng-hide="isCloudEnabled" class="schema" ng-class="{active:page=='schema'}"><a href="#/{{currentCore.name}}/schema"><span>Schema</span></a></li>
<li class="segments" ng-class="{active:page=='segments'}"><a href="#/{{currentCore.name}}/segments"><span>Segments info</span></a></li>
</ul>
</div>
</div> </div>
</div> </div>

View File

@ -21,7 +21,8 @@ var solrAdminApp = angular.module("solrAdminApp", [
"ngCookies", "ngCookies",
"ngtimeago", "ngtimeago",
"solrAdminServices", "solrAdminServices",
"localytics.directives" "localytics.directives",
"ab-base64"
]); ]);
solrAdminApp.config([ solrAdminApp.config([
@ -31,6 +32,10 @@ solrAdminApp.config([
templateUrl: 'partials/index.html', templateUrl: 'partials/index.html',
controller: 'IndexController' controller: 'IndexController'
}). }).
when('/login', {
templateUrl: 'partials/login.html',
controller: 'LoginController'
}).
when('/~logging', { when('/~logging', {
templateUrl: 'partials/logging.html', templateUrl: 'partials/logging.html',
controller: 'LoggingController' controller: 'LoggingController'
@ -315,7 +320,7 @@ solrAdminApp.config([
} }
}; };
}) })
.factory('httpInterceptor', function($q, $rootScope, $timeout, $injector) { .factory('httpInterceptor', function($q, $rootScope, $location, $timeout, $injector) {
var activeRequests = 0; var activeRequests = 0;
var started = function(config) { var started = function(config) {
@ -326,6 +331,9 @@ solrAdminApp.config([
delete $rootScope.exceptions[config.url]; delete $rootScope.exceptions[config.url];
} }
activeRequests++; activeRequests++;
if (sessionStorage.getItem("auth.header")) {
config.headers['Authorization'] = sessionStorage.getItem("auth.header");
}
config.timeout = 10000; config.timeout = 10000;
return config || $q.when(config); return config || $q.when(config);
}; };
@ -343,6 +351,11 @@ solrAdminApp.config([
$rootScope.$broadcast('connectionStatusInactive'); $rootScope.$broadcast('connectionStatusInactive');
},2000); },2000);
} }
if (!$location.path().startsWith('/login')) {
sessionStorage.removeItem("http401");
sessionStorage.removeItem("auth.state");
sessionStorage.removeItem("auth.statusText");
}
return response || $q.when(response); return response || $q.when(response);
}; };
@ -361,16 +374,38 @@ solrAdminApp.config([
var $http = $injector.get('$http'); var $http = $injector.get('$http');
var result = $http(rejection.config); var result = $http(rejection.config);
return result; return result;
} else if (rejection.status === 401) {
// Authentication redirect
var headers = rejection.headers();
var wwwAuthHeader = headers['www-authenticate'];
sessionStorage.setItem("auth.wwwAuthHeader", wwwAuthHeader);
sessionStorage.setItem("auth.statusText", rejection.statusText);
sessionStorage.setItem("http401", "true");
sessionStorage.removeItem("auth.scheme");
sessionStorage.removeItem("auth.realm");
sessionStorage.removeItem("auth.username");
sessionStorage.removeItem("auth.header");
sessionStorage.removeItem("auth.state");
if ($location.path().includes('/login')) {
if (!sessionStorage.getItem("auth.location")) {
sessionStorage.setItem("auth.location", "/");
}
} else {
sessionStorage.setItem("auth.location", $location.path());
$location.path('/login');
}
} else { } else {
$rootScope.exceptions[rejection.config.url] = rejection.data.error; $rootScope.exceptions[rejection.config.url] = rejection.data.error;
} }
return $q.reject(rejection); return $q.reject(rejection);
} };
return {request: started, response: ended, responseError: failed}; return {request: started, response: ended, responseError: failed};
}) })
.config(function($httpProvider) { .config(function($httpProvider) {
$httpProvider.interceptors.push("httpInterceptor"); $httpProvider.interceptors.push("httpInterceptor");
// Force BasicAuth plugin to serve us a 'Authorization: xBasic xxxx' header so browser will not pop up login dialogue
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
}) })
.directive('fileModel', function ($parse) { .directive('fileModel', function ($parse) {
return { return {
@ -441,6 +476,8 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
$scope.showingLogging = page.lastIndexOf("logging", 0) === 0; $scope.showingLogging = page.lastIndexOf("logging", 0) === 0;
$scope.showingCloud = page.lastIndexOf("cloud", 0) === 0; $scope.showingCloud = page.lastIndexOf("cloud", 0) === 0;
$scope.page = page; $scope.page = page;
$scope.currentUser = sessionStorage.getItem("auth.username");
$scope.http401 = sessionStorage.getItem("http401");
}; };
$scope.ping = function() { $scope.ping = function() {
@ -456,7 +493,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
} }
$scope.showCore = function(core) { $scope.showCore = function(core) {
$location.url("/" + core.name); $location.url("/" + core.name + "/core-overview");
} }
$scope.showCollection = function(collection) { $scope.showCollection = function(collection) {

View File

@ -0,0 +1,146 @@
/*
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('LoginController',
['$scope', '$routeParams', '$rootScope', '$location', '$window', 'AuthenticationService', 'Constants',
function ($scope, $routeParams, $rootScope, $location, $window, AuthenticationService, Constants) {
$scope.resetMenu("login", Constants.IS_ROOT_PAGE);
$scope.subPath = $routeParams.route;
$rootScope.exceptions = {};
// Session variables set in app.js 401 interceptor
var wwwAuthHeader = sessionStorage.getItem("auth.wwwAuthHeader");
var authScheme = sessionStorage.getItem("auth.scheme");
if (wwwAuthHeader) {
// Parse www-authenticate header
var wwwHeader = wwwAuthHeader.match(/(\w+)\s+(.*)/);
authScheme = wwwHeader[1];
var authParams = www_auth_parse_params(wwwHeader[2]);
if (typeof authParams === 'string' || authParams instanceof String) {
$scope.authParamsError = authParams;
} else {
$scope.authParamsError = undefined;
}
var realm = authParams['realm'];
sessionStorage.setItem("auth.realm", realm);
if (authScheme === 'Basic' || authScheme === 'xBasic') {
authScheme = 'Basic';
}
sessionStorage.setItem("auth.scheme", authScheme);
}
var supportedSchemes = ['Basic', 'Bearer'];
$scope.authSchemeSupported = supportedSchemes.includes(authScheme);
$scope.authScheme = sessionStorage.getItem("auth.scheme");
$scope.authRealm = sessionStorage.getItem("auth.realm");
$scope.wwwAuthHeader = sessionStorage.getItem("auth.wwwAuthHeader");
$scope.statusText = sessionStorage.getItem("auth.statusText");
$scope.authConfig = sessionStorage.getItem("auth.config");
$scope.authLocation = sessionStorage.getItem("auth.location");
$scope.authLoggedinUser = sessionStorage.getItem("auth.username");
$scope.authHeader = sessionStorage.getItem("auth.header");
$scope.login = function () {
AuthenticationService.SetCredentials($scope.username, $scope.password);
$location.path($scope.authLocation); // Redirect to the location that caused the login prompt
};
$scope.logout = function() {
// reset login status
AuthenticationService.ClearCredentials();
$location.path("/");
};
$scope.isLoggedIn = function() {
return (sessionStorage.getItem("auth.username") !== null);
};
}]);
// This function is copied and adapted from MIT-licensed https://github.com/randymized/www-authenticate/blob/master/lib/parsers.js
www_auth_parse_params= function (header) {
// This parser will definitely fail if there is more than one challenge
var params = {};
var tok, last_tok, _i, _len, key, value;
var state= 0; //0: token,
var m= header.split(/([",=])/);
for (_i = 0, _len = m.length; _i < _len; _i++) {
last_tok= tok;
tok = m[_i];
if (!tok.length) continue;
switch (state) {
case 0: // token
key= tok.trim();
state= 1; // expect equals
continue;
case 1: // expect equals
if ('=' != tok) return 'Equal sign was expected after '+key;
state= 2;
continue;
case 2: // expect value
if ('"' == tok) {
value= '';
state= 3; // expect quoted
continue;
}
else {
params[key]= value= tok.trim();
state= 9; // expect comma or end
continue;
}
case 3: // handling quoted string
if ('"' == tok) {
state= 8; // end quoted
continue;
}
else {
value+= tok;
state= 3; // continue accumulating quoted string
continue;
}
case 8: // end quote encountered
if ('"' == tok) {
// double quoted
value+= '"';
state= 3; // back to quoted string
continue;
}
if (',' == tok) {
params[key]= value;
state= 0;
continue;
}
else {
return 'Unexpected token ('+tok+') after '+value+'"';
}
continue;
case 9: // expect commma
if (',' != tok) return 'Comma expected after '+value;
state= 0;
continue;
}
}
switch (state) { // terminal state
case 0: // Empty or ignoring terminal comma
case 9: // Expecting comma or end of header
return params;
case 8: // Last token was end quote
params[key]= value;
return params;
default:
return 'Unexpected end of www-authenticate value.';
}
};

View File

@ -262,4 +262,26 @@ solrAdminServices.factory('System',
return $resource(':core/config', {wt: 'json', core: '@core', _:Date.now()}, { return $resource(':core/config', {wt: 'json', core: '@core', _:Date.now()}, {
get: {method: "GET"} get: {method: "GET"}
}) })
}]); }])
.factory('AuthenticationService',
['base64', function (base64) {
var service = {};
service.SetCredentials = function (username, password) {
var authdata = base64.encode(username + ':' + password);
sessionStorage.setItem("auth.header", "Basic " + authdata);
sessionStorage.setItem("auth.username", username);
};
service.ClearCredentials = function () {
sessionStorage.removeItem("auth.header");
sessionStorage.removeItem("auth.scheme");
sessionStorage.removeItem("auth.realm");
sessionStorage.removeItem("auth.username");
sessionStorage.removeItem("auth.wwwAuthHeader");
sessionStorage.removeItem("auth.statusText");
};
return service;
}]);

217
solr/webapp/web/libs/angular-utf8-base64.js vendored Executable file
View File

@ -0,0 +1,217 @@
/*
The MIT License (MIT)
Copyright (c) 2014 Andrey Bezyazychniy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
'use strict';
angular.module('ab-base64',[]).constant('base64', (function() {
/*
* Encapsulation of Vassilis Petroulias's base64.js library for AngularJS
* Original notice included below
*/
/*
Copyright Vassilis Petroulias [DRDigit]
Licensed 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.
*/
var B64 = {
alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
lookup: null,
ie: /MSIE /.test(navigator.userAgent),
ieo: /MSIE [67]/.test(navigator.userAgent),
encode: function (s) {
/* jshint bitwise:false */
var buffer = B64.toUtf8(s),
position = -1,
result,
len = buffer.length,
nan0, nan1, nan2, enc = [, , , ];
if (B64.ie) {
result = [];
while (++position < len) {
nan0 = buffer[position];
nan1 = buffer[++position];
enc[0] = nan0 >> 2;
enc[1] = ((nan0 & 3) << 4) | (nan1 >> 4);
if (isNaN(nan1))
enc[2] = enc[3] = 64;
else {
nan2 = buffer[++position];
enc[2] = ((nan1 & 15) << 2) | (nan2 >> 6);
enc[3] = (isNaN(nan2)) ? 64 : nan2 & 63;
}
result.push(B64.alphabet.charAt(enc[0]), B64.alphabet.charAt(enc[1]), B64.alphabet.charAt(enc[2]), B64.alphabet.charAt(enc[3]));
}
return result.join('');
} else {
result = '';
while (++position < len) {
nan0 = buffer[position];
nan1 = buffer[++position];
enc[0] = nan0 >> 2;
enc[1] = ((nan0 & 3) << 4) | (nan1 >> 4);
if (isNaN(nan1))
enc[2] = enc[3] = 64;
else {
nan2 = buffer[++position];
enc[2] = ((nan1 & 15) << 2) | (nan2 >> 6);
enc[3] = (isNaN(nan2)) ? 64 : nan2 & 63;
}
result += B64.alphabet[enc[0]] + B64.alphabet[enc[1]] + B64.alphabet[enc[2]] + B64.alphabet[enc[3]];
}
return result;
}
},
decode: function (s) {
/* jshint bitwise:false */
s = s.replace(/\s/g, '');
if (s.length % 4)
throw new Error('InvalidLengthError: decode failed: The string to be decoded is not the correct length for a base64 encoded string.');
if(/[^A-Za-z0-9+\/=\s]/g.test(s))
throw new Error('InvalidCharacterError: decode failed: The string contains characters invalid in a base64 encoded string.');
var buffer = B64.fromUtf8(s),
position = 0,
result,
len = buffer.length;
if (B64.ieo) {
result = [];
while (position < len) {
if (buffer[position] < 128)
result.push(String.fromCharCode(buffer[position++]));
else if (buffer[position] > 191 && buffer[position] < 224)
result.push(String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63)));
else
result.push(String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63)));
}
return result.join('');
} else {
result = '';
while (position < len) {
if (buffer[position] < 128)
result += String.fromCharCode(buffer[position++]);
else if (buffer[position] > 191 && buffer[position] < 224)
result += String.fromCharCode(((buffer[position++] & 31) << 6) | (buffer[position++] & 63));
else
result += String.fromCharCode(((buffer[position++] & 15) << 12) | ((buffer[position++] & 63) << 6) | (buffer[position++] & 63));
}
return result;
}
},
toUtf8: function (s) {
/* jshint bitwise:false */
var position = -1,
len = s.length,
chr, buffer = [];
if (/^[\x00-\x7f]*$/.test(s)) while (++position < len)
buffer.push(s.charCodeAt(position));
else while (++position < len) {
chr = s.charCodeAt(position);
if (chr < 128)
buffer.push(chr);
else if (chr < 2048)
buffer.push((chr >> 6) | 192, (chr & 63) | 128);
else
buffer.push((chr >> 12) | 224, ((chr >> 6) & 63) | 128, (chr & 63) | 128);
}
return buffer;
},
fromUtf8: function (s) {
/* jshint bitwise:false */
var position = -1,
len, buffer = [],
enc = [, , , ];
if (!B64.lookup) {
len = B64.alphabet.length;
B64.lookup = {};
while (++position < len)
B64.lookup[B64.alphabet.charAt(position)] = position;
position = -1;
}
len = s.length;
while (++position < len) {
enc[0] = B64.lookup[s.charAt(position)];
enc[1] = B64.lookup[s.charAt(++position)];
buffer.push((enc[0] << 2) | (enc[1] >> 4));
enc[2] = B64.lookup[s.charAt(++position)];
if (enc[2] === 64)
break;
buffer.push(((enc[1] & 15) << 4) | (enc[2] >> 2));
enc[3] = B64.lookup[s.charAt(++position)];
if (enc[3] === 64)
break;
buffer.push(((enc[2] & 3) << 6) | enc[3]);
}
return buffer;
}
};
var B64url = {
decode: function(input) {
// Replace non-url compatible chars with base64 standard chars
input = input
.replace(/-/g, '+')
.replace(/_/g, '/');
// Pad out with standard base64 required padding characters
var pad = input.length % 4;
if(pad) {
if(pad === 1) {
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
}
input += new Array(5-pad).join('=');
}
return B64.decode(input);
},
encode: function(input) {
var output = B64.encode(input);
return output
.replace(/\+/g, '-')
.replace(/\//g, '_')
.split('=', 1)[0];
}
};
return {
decode: B64.decode,
encode: B64.encode,
urldecode: B64url.decode,
urlencode: B64url.encode,
};
})());

View File

@ -0,0 +1,45 @@
/*
The MIT License (MIT)
Copyright (c) 2014 Andrey Bezyazychniy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
* Encapsulation of Vassilis Petroulias's base64.js library for AngularJS
* Original notice included below
*/
/*
Copyright Vassilis Petroulias [DRDigit]
Licensed 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.
*/
"use strict";angular.module("ab-base64",[]).constant("base64",function(){var a={alphabet:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",lookup:null,ie:/MSIE /.test(navigator.userAgent),ieo:/MSIE [67]/.test(navigator.userAgent),encode:function(b){var c,d,e,f,g=a.toUtf8(b),h=-1,i=g.length,j=[,,,];if(a.ie){for(c=[];++h<i;)d=g[h],e=g[++h],j[0]=d>>2,j[1]=(3&d)<<4|e>>4,isNaN(e)?j[2]=j[3]=64:(f=g[++h],j[2]=(15&e)<<2|f>>6,j[3]=isNaN(f)?64:63&f),c.push(a.alphabet.charAt(j[0]),a.alphabet.charAt(j[1]),a.alphabet.charAt(j[2]),a.alphabet.charAt(j[3]));return c.join("")}for(c="";++h<i;)d=g[h],e=g[++h],j[0]=d>>2,j[1]=(3&d)<<4|e>>4,isNaN(e)?j[2]=j[3]=64:(f=g[++h],j[2]=(15&e)<<2|f>>6,j[3]=isNaN(f)?64:63&f),c+=a.alphabet[j[0]]+a.alphabet[j[1]]+a.alphabet[j[2]]+a.alphabet[j[3]];return c},decode:function(b){if(b=b.replace(/\s/g,""),b.length%4)throw new Error("InvalidLengthError: decode failed: The string to be decoded is not the correct length for a base64 encoded string.");if(/[^A-Za-z0-9+\/=\s]/g.test(b))throw new Error("InvalidCharacterError: decode failed: The string contains characters invalid in a base64 encoded string.");var c,d=a.fromUtf8(b),e=0,f=d.length;if(a.ieo){for(c=[];f>e;)c.push(d[e]<128?String.fromCharCode(d[e++]):d[e]>191&&d[e]<224?String.fromCharCode((31&d[e++])<<6|63&d[e++]):String.fromCharCode((15&d[e++])<<12|(63&d[e++])<<6|63&d[e++]));return c.join("")}for(c="";f>e;)c+=String.fromCharCode(d[e]<128?d[e++]:d[e]>191&&d[e]<224?(31&d[e++])<<6|63&d[e++]:(15&d[e++])<<12|(63&d[e++])<<6|63&d[e++]);return c},toUtf8:function(a){var b,c=-1,d=a.length,e=[];if(/^[\x00-\x7f]*$/.test(a))for(;++c<d;)e.push(a.charCodeAt(c));else for(;++c<d;)b=a.charCodeAt(c),128>b?e.push(b):2048>b?e.push(b>>6|192,63&b|128):e.push(b>>12|224,b>>6&63|128,63&b|128);return e},fromUtf8:function(b){var c,d=-1,e=[],f=[,,,];if(!a.lookup){for(c=a.alphabet.length,a.lookup={};++d<c;)a.lookup[a.alphabet.charAt(d)]=d;d=-1}for(c=b.length;++d<c&&(f[0]=a.lookup[b.charAt(d)],f[1]=a.lookup[b.charAt(++d)],e.push(f[0]<<2|f[1]>>4),f[2]=a.lookup[b.charAt(++d)],64!==f[2])&&(e.push((15&f[1])<<4|f[2]>>2),f[3]=a.lookup[b.charAt(++d)],64!==f[3]);)e.push((3&f[2])<<6|f[3]);return e}},b={decode:function(b){b=b.replace(/-/g,"+").replace(/_/g,"/");var c=b.length%4;if(c){if(1===c)throw new Error("InvalidLengthError: Input base64url string is the wrong length to determine padding");b+=new Array(5-c).join("=")}return a.decode(b)},encode:function(b){var c=a.encode(b);return c.replace(/\+/g,"-").replace(/\//g,"_").split("=",1)[0]}};return{decode:a.decode,encode:a.encode,urldecode:b.decode,urlencode:b.encode}}());

View File

@ -0,0 +1,80 @@
<!--
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="login" class="clearfix">
<div ng-show="authScheme === 'Basic'">
<h1>Basic Authentication</h1>
<div class="login-error" ng-show="statusText !== 'require authentication' || authParamsError !== null">
{{statusText}}{{authParamsError}}
</div>
<div ng-show="!isLoggedIn()">
<p>
Solr requires authentication for resource {{authLocation === '/' ? 'Dashboard' : authLocation}}.<br/>
Please log in with your username and password for realm {{authRealm}}.
</p>
<br/>
<div ng-show="error" class="alert alert-danger">{{error}}</div>
<form name="form" ng-submit="login()" role="form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" id="username" class="form-control" ng-model="username" required />
<span ng-show="form.username.$dirty && form.username.$error.required" class="help-block">Username is required</span>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" id="password" class="form-control" ng-model="password" required />
<span ng-show="form.password.$dirty && form.password.$error.required" class="help-block">Password is required</span>
</div>
<br/>
<div class="form-actions">
<button type="submit" ng-disabled="form.$invalid" class="btn btn-danger">Login</button>
</div>
</form>
</div>
<div ng-show="isLoggedIn()">
<p>
Logged in as user {{authLoggedinUser}}. Realm={{authRealm}}.<br/>
</p>
<br/>
<form name="logoutForm" ng-submit="logout()" role="form" ng-show="isLoggedIn()">
<div class="form-actions">
<button type="submit" class="btn btn-danger">Logout</button>
</div>
</form>
</div>
</div>
<div ng-show="!authSchemeSupported">
<h1>Authentication scheme not supported</h1>
<div class="login-error">
{{statusText}}
</div>
<p>Some or all Solr operations are protected by an authentication scheme that is not yet supported by this Admin UI ({{authScheme}}).</p>
<p>Solr returned an error response:
<hr/>
<pre>HTTP 401 {{statusText}}
WWW-Authenticate: {{wwwAuthHeader}}</pre>
<hr/>
</p>
<p>A possible workaround may be to use another client that supports this scheme.</p>
</div>
</div>