add the ability to register a custom authentication realms
This adds the extension points necessary to enable a user to write a elasticsearch plugin that can integrate with Shield and add a custom authentication realm. For the most part, the work here just exposes the existing interfaces we have been using for Realms and factories to create realms. An additional interface was added to allow for a custom authentication failure handler to be used. This was needed to support use cases like SSO and Kerberos where additional headers may need to be sent to the user or a different HTTP response code would need to be sent. Relates to elastic/elasticsearch#24 Original commit: elastic/x-pack-elasticsearch@13442e5919
This commit is contained in:
parent
7e552f393b
commit
8fd5fe7ed8
|
@ -317,6 +317,7 @@
|
|||
<module>smoke-test-plugins-ssl</module>-->
|
||||
<module>shield-core-rest-tests</module>
|
||||
<module>smoke-test-watcher-with-shield</module>
|
||||
<module>shield-example-realm</module>
|
||||
</modules>
|
||||
|
||||
<profiles>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
~ ELASTICSEARCH CONFIDENTIAL
|
||||
~ __________________
|
||||
~
|
||||
~ [2014] Elasticsearch Incorporated. All Rights Reserved.
|
||||
~
|
||||
~ NOTICE: All information contained herein is, and remains
|
||||
~ the property of Elasticsearch Incorporated and its suppliers,
|
||||
~ if any. The intellectual and technical concepts contained
|
||||
~ herein are proprietary to Elasticsearch Incorporated
|
||||
~ and its suppliers and may be covered by U.S. and Foreign Patents,
|
||||
~ patents in process, and are protected by trade secret or copyright law.
|
||||
~ Dissemination of this information or reproduction of this material
|
||||
~ is strictly forbidden unless prior written permission is obtained
|
||||
~ from Elasticsearch Incorporated.
|
||||
-->
|
||||
|
||||
<project name="shield-example-realm"
|
||||
xmlns:ac="antlib:net.sf.antcontrib">
|
||||
|
||||
<import file="${elasticsearch.integ.antfile.default}"/>
|
||||
|
||||
<!-- redefined to work with auth -->
|
||||
<macrodef name="waitfor-elasticsearch">
|
||||
<attribute name="port"/>
|
||||
<attribute name="timeoutproperty"/>
|
||||
<sequential>
|
||||
<echo>Waiting for elasticsearch to become available on port @{port}...</echo>
|
||||
<waitfor maxwait="30" maxwaitunit="second"
|
||||
checkevery="500" checkeveryunit="millisecond"
|
||||
timeoutproperty="@{timeoutproperty}">
|
||||
<socket server="127.0.0.1" port="@{port}"/>
|
||||
</waitfor>
|
||||
</sequential>
|
||||
</macrodef>
|
||||
|
||||
<target name="start-external-cluster-with-plugin" depends="setup-workspace">
|
||||
<ac:for list="${xplugins.list}" param="xplugin.name">
|
||||
<sequential>
|
||||
<fail message="Expected @{xplugin.name}-${version}.zip as a dependency, but could not be found in ${integ.deps}/plugins}">
|
||||
<condition>
|
||||
<not>
|
||||
<available file="${integ.deps}/plugins/@{xplugin.name}-${elasticsearch.version}.zip" />
|
||||
</not>
|
||||
</condition>
|
||||
</fail>
|
||||
</sequential>
|
||||
</ac:for>
|
||||
|
||||
<ac:for param="file">
|
||||
<path>
|
||||
<fileset dir="${integ.deps}/plugins"/>
|
||||
</path>
|
||||
<sequential>
|
||||
<local name="plugin.name"/>
|
||||
<convert-plugin-name file="@{file}" outputproperty="plugin.name"/>
|
||||
<install-plugin name="${plugin.name}" file="@{file}"/>
|
||||
</sequential>
|
||||
</ac:for>
|
||||
|
||||
<local name="home"/>
|
||||
<property name="home" location="${integ.scratch}/elasticsearch-${elasticsearch.version}"/>
|
||||
|
||||
<echo>Adding shield users...</echo>
|
||||
<run-script script="${home}/bin/shield/esusers">
|
||||
<nested>
|
||||
<arg value="useradd"/>
|
||||
<arg value="test_user"/>
|
||||
<arg value="-p"/>
|
||||
<arg value="changeme"/>
|
||||
<arg value="-r"/>
|
||||
<arg value="user"/>
|
||||
</nested>
|
||||
</run-script>
|
||||
|
||||
<startup-elasticsearch>
|
||||
<additional-args>
|
||||
<arg value="-Dshield.authc.realms.custom.order=0"/>
|
||||
<arg value="-Dshield.authc.realms.custom.type=custom"/>
|
||||
<arg value="-Dshield.authc.realms.esusers.order=1"/>
|
||||
<arg value="-Dshield.authc.realms.esusers.type=esusers"/>
|
||||
</additional-args>
|
||||
</startup-elasticsearch>
|
||||
|
||||
<echo>Checking we can connect with basic auth on port ${integ.http.port}...</echo>
|
||||
<local name="temp.file"/>
|
||||
<tempfile property="temp.file" destdir="${java.io.tmpdir}"/>
|
||||
<get src="http://127.0.0.1:${integ.http.port}" dest="${temp.file}"
|
||||
username="test_user" password="changeme" verbose="true" retries="10"/>
|
||||
</target>
|
||||
</project>
|
|
@ -0,0 +1,123 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>x-plugins-qa</artifactId>
|
||||
<groupId>org.elasticsearch.qa</groupId>
|
||||
<version>2.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>shield-example-realm</artifactId>
|
||||
<name>QA: Shield Example Realm</name>
|
||||
<description>A basic example custom realm with tests to ensure the functionality works in Shield</description>
|
||||
|
||||
<properties>
|
||||
<elasticsearch.plugin.classname>org.elasticsearch.example.ExampleRealmPlugin</elasticsearch.plugin.classname>
|
||||
<elasticsearch.plugin.isolated>false</elasticsearch.plugin.isolated>
|
||||
<elasticsearch.integ.antfile>${project.basedir}/integration-tests.xml</elasticsearch.integ.antfile>
|
||||
<xplugins.list>license,shield,shield-example-realm</xplugins.list>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.elasticsearch</groupId>
|
||||
<artifactId>elasticsearch</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.elasticsearch.plugin</groupId>
|
||||
<artifactId>shield</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>integ-setup-dependencies</id>
|
||||
<phase>pre-integration-test</phase>
|
||||
<goals>
|
||||
<goal>copy</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<skip>${skip.integ.tests}</skip>
|
||||
<useBaseVersion>true</useBaseVersion>
|
||||
<outputDirectory>${integ.deps}/plugins</outputDirectory>
|
||||
|
||||
<artifactItems>
|
||||
<!-- elasticsearch distribution -->
|
||||
<artifactItem>
|
||||
<groupId>org.elasticsearch.distribution.zip</groupId>
|
||||
<artifactId>elasticsearch</artifactId>
|
||||
<version>${elasticsearch.version}</version>
|
||||
<type>zip</type>
|
||||
<overWrite>true</overWrite>
|
||||
<outputDirectory>${integ.deps}</outputDirectory>
|
||||
</artifactItem>
|
||||
|
||||
<!-- commercial plugins -->
|
||||
<artifactItem>
|
||||
<groupId>org.elasticsearch.plugin</groupId>
|
||||
<artifactId>license</artifactId>
|
||||
<version>${elasticsearch.version}</version>
|
||||
<type>zip</type>
|
||||
<overWrite>true</overWrite>
|
||||
</artifactItem>
|
||||
|
||||
<artifactItem>
|
||||
<groupId>org.elasticsearch.plugin</groupId>
|
||||
<artifactId>shield</artifactId>
|
||||
<version>${elasticsearch.version}</version>
|
||||
<type>zip</type>
|
||||
<overWrite>true</overWrite>
|
||||
</artifactItem>
|
||||
|
||||
<artifactItem>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>${project.artifactId}</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>zip</type>
|
||||
<overWrite>true</overWrite>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ant-contrib</groupId>
|
||||
<artifactId>ant-contrib</artifactId>
|
||||
<version>1.0b3</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>ant</groupId>
|
||||
<artifactId>ant</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.ant</groupId>
|
||||
<artifactId>ant-nodeps</artifactId>
|
||||
<version>1.8.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.example;
|
||||
|
||||
import org.elasticsearch.example.realm.CustomAuthenticationFailureHandler;
|
||||
import org.elasticsearch.example.realm.CustomRealm;
|
||||
import org.elasticsearch.example.realm.CustomRealmFactory;
|
||||
import org.elasticsearch.plugins.Plugin;
|
||||
import org.elasticsearch.shield.authc.AuthenticationModule;
|
||||
|
||||
public class ExampleRealmPlugin extends Plugin {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "custom realm example";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "a very basic implementation of a custom realm to validate it works";
|
||||
}
|
||||
|
||||
public void onModule(AuthenticationModule authenticationModule) {
|
||||
authenticationModule.addCustomRealm(CustomRealm.TYPE, CustomRealmFactory.class);
|
||||
authenticationModule.setAuthenticationFailureHandler(CustomAuthenticationFailureHandler.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.example.realm;
|
||||
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.shield.authc.DefaultAuthenticationFailureHandler;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
|
||||
public class CustomAuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException unsuccessfulAuthentication(RestRequest request, AuthenticationToken token) {
|
||||
ElasticsearchSecurityException e = super.unsuccessfulAuthentication(request, token);
|
||||
// set a custom header
|
||||
e.addHeader("WWW-Authenticate", "custom-challenge");
|
||||
return e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException unsuccessfulAuthentication(TransportMessage message, AuthenticationToken token, String action) {
|
||||
ElasticsearchSecurityException e = super.unsuccessfulAuthentication(message, token, action);
|
||||
// set a custom header
|
||||
e.addHeader("WWW-Authenticate", "custom-challenge");
|
||||
return e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException missingToken(RestRequest request) {
|
||||
ElasticsearchSecurityException e = super.missingToken(request);
|
||||
// set a custom header
|
||||
e.addHeader("WWW-Authenticate", "custom-challenge");
|
||||
return e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException missingToken(TransportMessage message, String action) {
|
||||
ElasticsearchSecurityException e = super.missingToken(message, action);
|
||||
// set a custom header
|
||||
e.addHeader("WWW-Authenticate", "custom-challenge");
|
||||
return e;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.example.realm;
|
||||
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.shield.authc.Realm;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
|
||||
public class CustomRealm extends Realm<UsernamePasswordToken> {
|
||||
|
||||
public static final String TYPE = "custom";
|
||||
|
||||
static final String USER_HEADER = "User";
|
||||
static final String PW_HEADER = "Password";
|
||||
|
||||
static final String KNOWN_USER = "custom_user";
|
||||
static final String KNOWN_PW = "changeme";
|
||||
static final String[] ROLES = new String[] { "admin" };
|
||||
|
||||
public CustomRealm(RealmConfig config) {
|
||||
super(TYPE, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(AuthenticationToken token) {
|
||||
return token instanceof UsernamePasswordToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsernamePasswordToken token(RestRequest request) {
|
||||
String user = request.header(USER_HEADER);
|
||||
if (user != null) {
|
||||
String password = request.header(PW_HEADER);
|
||||
if (password != null) {
|
||||
return new UsernamePasswordToken(user, new SecuredString(password.toCharArray()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsernamePasswordToken token(TransportMessage<?> message) {
|
||||
String user = message.getHeader(USER_HEADER);
|
||||
if (user != null) {
|
||||
String password = message.getHeader(PW_HEADER);
|
||||
if (password != null) {
|
||||
return new UsernamePasswordToken(user, new SecuredString(password.toCharArray()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User authenticate(UsernamePasswordToken token) {
|
||||
final String actualUser = token.principal();
|
||||
if (KNOWN_USER.equals(actualUser) && SecuredString.constantTimeEquals(token.credentials(), KNOWN_PW)) {
|
||||
return new User.Simple(actualUser, ROLES);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.example.realm;
|
||||
|
||||
import org.elasticsearch.shield.authc.Realm;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
|
||||
public class CustomRealmFactory extends Realm.Factory<CustomRealm> {
|
||||
|
||||
public CustomRealmFactory() {
|
||||
super(CustomRealm.TYPE, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomRealm create(RealmConfig config) {
|
||||
return new CustomRealm(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomRealm createDefault(String name) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.example.realm;
|
||||
|
||||
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
|
||||
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
|
||||
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
|
||||
import org.elasticsearch.client.support.Headers;
|
||||
import org.elasticsearch.client.transport.NoNodeAvailableException;
|
||||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.shield.ShieldPlugin;
|
||||
import org.elasticsearch.test.ESIntegTestCase;
|
||||
import org.elasticsearch.test.rest.client.http.HttpResponse;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
* Integration test to test authentication with the custom realm
|
||||
*/
|
||||
public class CustomRealmIT extends ESIntegTestCase {
|
||||
|
||||
@Override
|
||||
protected Settings externalClusterClientSettings() {
|
||||
return Settings.builder()
|
||||
.put("plugin.types", ShieldPlugin.class.getName())
|
||||
.put(Headers.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER)
|
||||
.put(Headers.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpConnectionWithNoAuthentication() throws Exception {
|
||||
HttpResponse response = httpClient().path("/").execute();
|
||||
assertThat(response.getStatusCode(), is(401));
|
||||
String value = response.getHeaders().get("WWW-Authenticate");
|
||||
assertThat(value, is("custom-challenge"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpAuthentication() throws Exception {
|
||||
HttpResponse response = httpClient().path("/")
|
||||
.addHeader(CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER)
|
||||
.addHeader(CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
||||
.execute();
|
||||
assertThat(response.getStatusCode(), is(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransportClient() throws Exception {
|
||||
NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get();
|
||||
NodeInfo[] nodes = nodeInfos.getNodes();
|
||||
assertTrue(nodes.length > 0);
|
||||
TransportAddress publishAddress = randomFrom(nodes).getTransport().address().publishAddress();
|
||||
String clusterName = nodeInfos.getClusterNameAsString();
|
||||
|
||||
Settings settings = Settings.builder()
|
||||
.put("path.home", createTempDir())
|
||||
.put("plugin.types", ShieldPlugin.class.getName())
|
||||
.put("cluster.name", clusterName)
|
||||
.put(Headers.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER)
|
||||
.put(Headers.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
||||
.build();
|
||||
try (TransportClient client = TransportClient.builder().settings(settings).build()) {
|
||||
client.addTransportAddress(publishAddress);
|
||||
ClusterHealthResponse response = client.admin().cluster().prepareHealth().execute().actionGet();
|
||||
assertThat(response.isTimedOut(), is(false));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransportClientWrongAuthentication() throws Exception {
|
||||
NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get();
|
||||
NodeInfo[] nodes = nodeInfos.getNodes();
|
||||
assertTrue(nodes.length > 0);
|
||||
TransportAddress publishAddress = randomFrom(nodes).getTransport().address().publishAddress();
|
||||
String clusterName = nodeInfos.getClusterNameAsString();
|
||||
|
||||
Settings settings = Settings.builder()
|
||||
.put("path.home", createTempDir())
|
||||
.put("plugin.types", ShieldPlugin.class.getName())
|
||||
.put("cluster.name", clusterName)
|
||||
.put(Headers.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER + randomAsciiOfLength(1))
|
||||
.put(Headers.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
||||
.build();
|
||||
try (TransportClient client = TransportClient.builder().settings(settings).build()) {
|
||||
client.addTransportAddress(publishAddress);
|
||||
client.admin().cluster().prepareHealth().execute().actionGet();
|
||||
fail("authentication failure should have resulted in a NoNodesAvailableException");
|
||||
} catch (NoNodeAvailableException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.example.realm;
|
||||
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.RealmConfig;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
public class CustomRealmTests extends ESTestCase {
|
||||
|
||||
@Test
|
||||
public void testAuthenticate() {
|
||||
Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build();
|
||||
CustomRealm realm = new CustomRealm(new RealmConfig("test", Settings.EMPTY, globalSettings));
|
||||
UsernamePasswordToken token = new UsernamePasswordToken(CustomRealm.KNOWN_USER, new SecuredString(CustomRealm.KNOWN_PW.toCharArray()));
|
||||
User user = realm.authenticate(token);
|
||||
assertThat(user, notNullValue());
|
||||
assertThat(user.roles(), equalTo(CustomRealm.ROLES));
|
||||
assertThat(user.principal(), equalTo(CustomRealm.KNOWN_USER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateBadUser() {
|
||||
Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build();
|
||||
CustomRealm realm = new CustomRealm(new RealmConfig("test", Settings.EMPTY, globalSettings));
|
||||
UsernamePasswordToken token = new UsernamePasswordToken(CustomRealm.KNOWN_USER + "1", new SecuredString(CustomRealm.KNOWN_PW.toCharArray()));
|
||||
User user = realm.authenticate(token);
|
||||
assertThat(user, nullValue());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.shield.authc;
|
||||
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
|
||||
/**
|
||||
* A AuthenticationFailureHandler is responsible for the handling of a request that has failed authentication. This must
|
||||
* consist of returning an exception and this exception can have headers to indicate authentication is required or another
|
||||
* HTTP operation such as a redirect.
|
||||
*
|
||||
* For example, when using Basic authentication, most clients wait to send credentials until they have been challenged
|
||||
* for them. In this workflow a client makes a request, the server responds with a 401 status with the header
|
||||
* <code>WWW-Authenticate: Basic realm=auth-realm</code>, and then the client will send credentials. The same scheme also
|
||||
* applies for other methods of authentication, with changes to the value provided in the WWW-Authenticate header.
|
||||
*
|
||||
* Additionally, some methods of authentication may require a different status code. When using an single sign on system,
|
||||
* clients will often retrieve a token from a single sign on system that is presented to the server and verified. When a
|
||||
* client does not provide such a token, then the server can choose to redirect the client to the single sign on system to
|
||||
* retrieve a token. This can be accomplished in the AuthenticationFailureHandler by setting the {@link org.elasticsearch.rest.RestStatus#FOUND}
|
||||
* with a <code>Location</code> header that contains the location to redirect the user to.
|
||||
*/
|
||||
public interface AuthenticationFailureHandler {
|
||||
|
||||
/**
|
||||
* This method is called when there has been an authentication failure for the given REST request and authentication
|
||||
* token.
|
||||
*
|
||||
* @param request The request that could not be authenticated
|
||||
* @param token The token that was extracted from the request
|
||||
* @return ElasticsearchSecurityException with the appropriate headers and message
|
||||
*/
|
||||
ElasticsearchSecurityException unsuccessfulAuthentication(RestRequest request, AuthenticationToken token);
|
||||
|
||||
/**
|
||||
* This method is called when there has been an authentication failure for the given message and token
|
||||
* @param message The transport message that could not be authenticated
|
||||
* @param token The token that was extracted from the message
|
||||
* @param action The name of the action that the message is trying to perform
|
||||
* @return ElasticsearchSecurityException with the appropriate headers and message
|
||||
*/
|
||||
ElasticsearchSecurityException unsuccessfulAuthentication(TransportMessage message, AuthenticationToken token, String action);
|
||||
|
||||
/**
|
||||
* The method is called when an exception has occurred while processing the REST request. This could be an error that
|
||||
* occurred while attempting to extract a token or while attempting to authenticate the request
|
||||
* @param request The request that was being authenticated when the exception occurred
|
||||
* @param e The exception that was thrown
|
||||
* @return ElasticsearchSecurityException with the appropriate headers and message
|
||||
*/
|
||||
ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e);
|
||||
|
||||
/**
|
||||
* The method is called when an exception has occurred while processing the transport message. This could be an error that
|
||||
* occurred while attempting to extract a token or while attempting to authenticate the request
|
||||
* @param message The message that was being authenticated when the exception occurred
|
||||
* @param e The exception that was thrown
|
||||
* @return ElasticsearchSecurityException with the appropriate headers and message
|
||||
*/
|
||||
ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, Exception e);
|
||||
|
||||
/**
|
||||
* This method is called when a REST request is received and no authentication token could be extracted AND anonymous
|
||||
* access is disabled. If anonymous access is enabled, this method will not be called
|
||||
* @param request The request that did not have a token
|
||||
* @return ElasticsearchSecurityException with the appropriate headers and message
|
||||
*/
|
||||
ElasticsearchSecurityException missingToken(RestRequest request);
|
||||
|
||||
/**
|
||||
* This method is called when a transport message is received and no authentication token could be extracted AND
|
||||
* anonymous access is disabled. If anonymous access is enabled this method will not be called
|
||||
* @param message The message that did not have a token
|
||||
* @param action The name of the action that the message is trying to perform
|
||||
* @return ElasticsearchSecurityException with the appropriate headers and message
|
||||
*/
|
||||
ElasticsearchSecurityException missingToken(TransportMessage message, String action);
|
||||
|
||||
/**
|
||||
* This method is called when anonymous access is enabled, a request does not pass authorization with the anonymous
|
||||
* user, AND the anonymous service is configured to thrown a authentication exception instead of a authorization
|
||||
* exception
|
||||
* @param action the action that failed authorization for anonymous access
|
||||
* @return ElasticsearchSecurityException with the appropriate headers and message
|
||||
*/
|
||||
ElasticsearchSecurityException authenticationRequired(String action);
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.elasticsearch.common.inject.multibindings.MapBinder;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authc.activedirectory.ActiveDirectoryRealm;
|
||||
|
@ -13,11 +14,20 @@ import org.elasticsearch.shield.authc.ldap.LdapRealm;
|
|||
import org.elasticsearch.shield.authc.pki.PkiRealm;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class AuthenticationModule extends AbstractShieldModule.Node {
|
||||
|
||||
private static final List<String> INTERNAL_REALM_TYPES = ImmutableList.of(ESUsersRealm.TYPE, ActiveDirectoryRealm.TYPE, LdapRealm.TYPE, PkiRealm.TYPE);
|
||||
|
||||
private final Map<String, Class<? extends Realm.Factory<? extends Realm<? extends AuthenticationToken>>>> customRealms = new HashMap<>();
|
||||
|
||||
private Class<? extends AuthenticationFailureHandler> authcFailureHandler = null;
|
||||
|
||||
public AuthenticationModule(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
@ -29,9 +39,40 @@ public class AuthenticationModule extends AbstractShieldModule.Node {
|
|||
mapBinder.addBinding(ActiveDirectoryRealm.TYPE).to(ActiveDirectoryRealm.Factory.class).asEagerSingleton();
|
||||
mapBinder.addBinding(LdapRealm.TYPE).to(LdapRealm.Factory.class).asEagerSingleton();
|
||||
mapBinder.addBinding(PkiRealm.TYPE).to(PkiRealm.Factory.class).asEagerSingleton();
|
||||
for (Entry<String, Class<? extends Realm.Factory<? extends Realm<? extends AuthenticationToken>>>> entry : customRealms.entrySet()) {
|
||||
mapBinder.addBinding(entry.getKey()).to(entry.getValue()).asEagerSingleton();
|
||||
}
|
||||
|
||||
bind(Realms.class).asEagerSingleton();
|
||||
bind(AnonymousService.class).asEagerSingleton();
|
||||
if (authcFailureHandler == null) {
|
||||
bind(AuthenticationFailureHandler.class).to(DefaultAuthenticationFailureHandler.class).asEagerSingleton();
|
||||
} else {
|
||||
bind(AuthenticationFailureHandler.class).to(authcFailureHandler).asEagerSingleton();
|
||||
}
|
||||
bind(AuthenticationService.class).to(InternalAuthenticationService.class).asEagerSingleton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a custom realm type and factory for use as a authentication realm
|
||||
* @param type the type of the realm that identifies it. Must not be null, empty, or the same value as one of the built-in realms
|
||||
* @param clazz the factory class that is used to create this type of realm. Must not be null
|
||||
*/
|
||||
public void addCustomRealm(String type, Class<? extends Realm.Factory<? extends Realm<? extends AuthenticationToken>>> clazz) {
|
||||
if (type == null || type.isEmpty()) {
|
||||
throw new IllegalArgumentException("type must not be null or empty");
|
||||
} else if (clazz == null) {
|
||||
throw new IllegalArgumentException("realm factory class cannot be null");
|
||||
} else if (INTERNAL_REALM_TYPES.contains(type)) {
|
||||
throw new IllegalArgumentException("cannot redefine the type [" + type + "] with custom realm [" + clazz.getName() + "]");
|
||||
}
|
||||
customRealms.put(type, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AuthenticationFailureHandler} to the specified implementation
|
||||
*/
|
||||
public void setAuthenticationFailureHandler(Class<? extends AuthenticationFailureHandler> clazz) {
|
||||
this.authcFailureHandler = clazz;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.shield.authc;
|
||||
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
|
||||
import static org.elasticsearch.shield.support.Exceptions.authenticationError;
|
||||
|
||||
/**
|
||||
* The default implementation of a {@link AuthenticationFailureHandler}. This handler will return an exception with a
|
||||
* RestStatus of 401 and the WWW-Authenticate header with a Basic challenge.
|
||||
*/
|
||||
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException unsuccessfulAuthentication(RestRequest request, AuthenticationToken token) {
|
||||
return authenticationError("unable to authenticate user [{}] for REST request [{}]", token.principal(), request.uri());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException unsuccessfulAuthentication(TransportMessage message, AuthenticationToken token, String action) {
|
||||
return authenticationError("unable to authenticate user [{}] for action [{}]", token.principal(), action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e) {
|
||||
if (e instanceof ElasticsearchSecurityException) {
|
||||
assert ((ElasticsearchSecurityException) e).status() == RestStatus.UNAUTHORIZED;
|
||||
assert ((ElasticsearchSecurityException) e).getHeader("WWW-Authenticate").size() == 1;
|
||||
return (ElasticsearchSecurityException) e;
|
||||
}
|
||||
return authenticationError("error attempting to authenticate request", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, Exception e) {
|
||||
if (e instanceof ElasticsearchSecurityException) {
|
||||
assert ((ElasticsearchSecurityException) e).status() == RestStatus.UNAUTHORIZED;
|
||||
assert ((ElasticsearchSecurityException) e).getHeader("WWW-Authenticate").size() == 1;
|
||||
return (ElasticsearchSecurityException) e;
|
||||
}
|
||||
return authenticationError("error attempting to authenticate request", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException missingToken(RestRequest request) {
|
||||
return authenticationError("missing authentication token for REST request [{}]", request.uri());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException missingToken(TransportMessage message, String action) {
|
||||
return authenticationError("missing authentication token for action [{}]", action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException authenticationRequired(String action) {
|
||||
return authenticationError("action [{}] requires authentication", action);
|
||||
}
|
||||
}
|
|
@ -39,15 +39,18 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
private final AuditTrail auditTrail;
|
||||
private final CryptoService cryptoService;
|
||||
private final AnonymousService anonymousService;
|
||||
private final AuthenticationFailureHandler failureHandler;
|
||||
private final boolean signUserHeader;
|
||||
|
||||
@Inject
|
||||
public InternalAuthenticationService(Settings settings, Realms realms, AuditTrail auditTrail, CryptoService cryptoService, AnonymousService anonymousService) {
|
||||
public InternalAuthenticationService(Settings settings, Realms realms, AuditTrail auditTrail, CryptoService cryptoService,
|
||||
AnonymousService anonymousService, AuthenticationFailureHandler failureHandler) {
|
||||
super(settings);
|
||||
this.realms = realms;
|
||||
this.auditTrail = auditTrail;
|
||||
this.cryptoService = cryptoService;
|
||||
this.anonymousService = anonymousService;
|
||||
this.failureHandler = failureHandler;
|
||||
this.signUserHeader = settings.getAsBoolean(SETTING_SIGN_USER_HEADER, true);
|
||||
}
|
||||
|
||||
|
@ -63,11 +66,7 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
logger.warn("failed to extract token from request: {}", e.getMessage());
|
||||
}
|
||||
auditTrail.authenticationFailed(request);
|
||||
|
||||
if (e instanceof ElasticsearchSecurityException) {
|
||||
throw (ElasticsearchSecurityException) e;
|
||||
}
|
||||
throw authenticationError("error attempting to authenticate request", e);
|
||||
throw failureHandler.exceptionProcessingRequest(request, e);
|
||||
}
|
||||
|
||||
if (token == null) {
|
||||
|
@ -78,7 +77,7 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
return anonymousService.anonymousUser();
|
||||
}
|
||||
auditTrail.anonymousAccessDenied(request);
|
||||
throw authenticationError("missing authentication token for REST request [{}]", request.uri());
|
||||
throw failureHandler.missingToken(request);
|
||||
}
|
||||
|
||||
User user;
|
||||
|
@ -89,14 +88,11 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
logger.debug("authentication of request failed for principal [{}], uri [{}]", e, token.principal(), request.uri());
|
||||
}
|
||||
auditTrail.authenticationFailed(token, request);
|
||||
if (e instanceof ElasticsearchSecurityException) {
|
||||
throw (ElasticsearchSecurityException) e;
|
||||
}
|
||||
throw authenticationError("error attempting to authenticate request", e);
|
||||
throw failureHandler.exceptionProcessingRequest(request, e);
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
throw authenticationError("unable to authenticate user [{}] for REST request [{}]", token.principal(), request.uri());
|
||||
throw failureHandler.unsuccessfulAuthentication(request, token);
|
||||
}
|
||||
// we must put the user in the request context, so it'll be copied to the
|
||||
// transport request - without it, the transport will assume system user
|
||||
|
@ -197,10 +193,7 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
logger.warn("failed to extract token from transport message: ", e.getMessage());
|
||||
}
|
||||
auditTrail.authenticationFailed(action, message);
|
||||
if (e instanceof ElasticsearchSecurityException) {
|
||||
throw e;
|
||||
}
|
||||
throw authenticationError("error attempting to authenticate request", e);
|
||||
throw failureHandler.exceptionProcessingRequest(message, e);
|
||||
}
|
||||
|
||||
if (token == null) {
|
||||
|
@ -211,7 +204,7 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
return anonymousService.anonymousUser();
|
||||
}
|
||||
auditTrail.anonymousAccessDenied(action, message);
|
||||
throw authenticationError("missing authentication token for action [{}]", action);
|
||||
throw failureHandler.missingToken(message, action);
|
||||
}
|
||||
|
||||
User user;
|
||||
|
@ -222,14 +215,11 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
logger.debug("authentication of transport message failed for principal [{}], action [{}]", e, token.principal(), action);
|
||||
}
|
||||
auditTrail.authenticationFailed(token, action, message);
|
||||
if (e instanceof ElasticsearchSecurityException) {
|
||||
throw (ElasticsearchSecurityException) e;
|
||||
}
|
||||
throw authenticationError("error attempting to authenticate request", e);
|
||||
throw failureHandler.exceptionProcessingRequest(message, e);
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
throw authenticationError("unable to authenticate user [{}] for action [{}]", token.principal(), action);
|
||||
throw failureHandler.unsuccessfulAuthentication(message, token, action);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
|
|
@ -93,7 +93,8 @@ public abstract class Realm<T extends AuthenticationToken> implements Comparable
|
|||
|
||||
/**
|
||||
* A factory for a specific realm type. Knows how to create a new realm given the appropriate
|
||||
* settings
|
||||
* settings. The factory will be called when creating a realm during the parsing of realms defined in the
|
||||
* elasticsearch.yml file
|
||||
*/
|
||||
public static abstract class Factory<R extends Realm> {
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.elasticsearch.search.action.SearchServiceTransportAction;
|
|||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.audit.AuditTrail;
|
||||
import org.elasticsearch.shield.authc.AnonymousService;
|
||||
import org.elasticsearch.shield.authc.AuthenticationFailureHandler;
|
||||
import org.elasticsearch.shield.authz.indicesresolver.DefaultIndicesResolver;
|
||||
import org.elasticsearch.shield.authz.indicesresolver.IndicesResolver;
|
||||
import org.elasticsearch.shield.authz.store.RolesStore;
|
||||
|
@ -47,9 +48,11 @@ public class InternalAuthorizationService extends AbstractComponent implements A
|
|||
private final AuditTrail auditTrail;
|
||||
private final IndicesResolver[] indicesResolvers;
|
||||
private final AnonymousService anonymousService;
|
||||
private final AuthenticationFailureHandler authcFailureHandler;
|
||||
|
||||
@Inject
|
||||
public InternalAuthorizationService(Settings settings, RolesStore rolesStore, ClusterService clusterService, AuditTrail auditTrail, AnonymousService anonymousService) {
|
||||
public InternalAuthorizationService(Settings settings, RolesStore rolesStore, ClusterService clusterService,
|
||||
AuditTrail auditTrail, AnonymousService anonymousService, AuthenticationFailureHandler authcFailureHandler) {
|
||||
super(settings);
|
||||
this.rolesStore = rolesStore;
|
||||
this.clusterService = clusterService;
|
||||
|
@ -58,6 +61,7 @@ public class InternalAuthorizationService extends AbstractComponent implements A
|
|||
new DefaultIndicesResolver(this)
|
||||
};
|
||||
this.anonymousService = anonymousService;
|
||||
this.authcFailureHandler = authcFailureHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -238,7 +242,7 @@ public class InternalAuthorizationService extends AbstractComponent implements A
|
|||
// Special case for anonymous user
|
||||
if (anonymousService.isAnonymous(user)) {
|
||||
if (!anonymousService.authorizationExceptionsEnabled()) {
|
||||
throw authenticationError("action [{}] requires authentication", action);
|
||||
throw authcFailureHandler.authenticationRequired(action);
|
||||
}
|
||||
}
|
||||
return authorizationError("action [{}] is unauthorized for user [{}]", action, user.principal());
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.shield.authc;
|
||||
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.common.inject.Guice;
|
||||
import org.elasticsearch.common.inject.Injector;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.settings.SettingsModule;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.env.EnvironmentModule;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.audit.AuditTrailModule;
|
||||
import org.elasticsearch.shield.authc.activedirectory.ActiveDirectoryRealm;
|
||||
import org.elasticsearch.shield.authc.esusers.ESUsersRealm;
|
||||
import org.elasticsearch.shield.authc.ldap.LdapRealm;
|
||||
import org.elasticsearch.shield.authc.pki.PkiRealm;
|
||||
import org.elasticsearch.shield.crypto.CryptoModule;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.threadpool.ThreadPoolModule;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
* Unit tests for the AuthenticationModule
|
||||
*/
|
||||
public class AuthenticationModuleTests extends ESTestCase {
|
||||
|
||||
@Test
|
||||
public void testAddingReservedRealmType() {
|
||||
Settings settings = Settings.EMPTY;
|
||||
AuthenticationModule module = new AuthenticationModule(settings);
|
||||
try {
|
||||
module.addCustomRealm(randomFrom(PkiRealm.TYPE, LdapRealm.TYPE, ActiveDirectoryRealm.TYPE, ESUsersRealm.TYPE),
|
||||
randomFrom(PkiRealm.Factory.class, LdapRealm.Factory.class, ActiveDirectoryRealm.Factory.class, ESUsersRealm.Factory.class));
|
||||
fail("overriding a built in realm type is not allowed!");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), containsString("cannot redefine"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddingNullOrEmptyType() {
|
||||
Settings settings = Settings.EMPTY;
|
||||
AuthenticationModule module = new AuthenticationModule(settings);
|
||||
try {
|
||||
module.addCustomRealm(randomBoolean() ? null : "",
|
||||
randomFrom(PkiRealm.Factory.class, LdapRealm.Factory.class, ActiveDirectoryRealm.Factory.class, ESUsersRealm.Factory.class));
|
||||
fail("type must not be null");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), containsString("null or empty"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddingNullFactory() {
|
||||
Settings settings = Settings.EMPTY;
|
||||
AuthenticationModule module = new AuthenticationModule(settings);
|
||||
try {
|
||||
module.addCustomRealm(randomAsciiOfLength(7), null);
|
||||
fail("factory must not be null");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), containsString("null"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisteringCustomRealm() {
|
||||
Settings settings = Settings.builder()
|
||||
.put("name", "foo")
|
||||
.put("path.home", createTempDir())
|
||||
.put("client.type", "node").build();
|
||||
AuthenticationModule module = new AuthenticationModule(settings);
|
||||
// adding the same factory with a different type is valid the way realms are implemented...
|
||||
module.addCustomRealm("custom", ESUsersRealm.Factory.class);
|
||||
Environment env = new Environment(settings);
|
||||
ThreadPool pool = new ThreadPool(settings);
|
||||
try {
|
||||
Injector injector = Guice.createInjector(module, new SettingsModule(settings), new AuditTrailModule(settings), new CryptoModule(settings), new EnvironmentModule(env), new ThreadPoolModule(pool));
|
||||
Realms realms = injector.getInstance(Realms.class);
|
||||
Realm.Factory factory = realms.realmFactory("custom");
|
||||
assertThat(factory, notNullValue());
|
||||
assertThat(factory, instanceOf(ESUsersRealm.Factory.class));
|
||||
} finally {
|
||||
pool.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultFailureHandler() {
|
||||
Settings settings = Settings.builder()
|
||||
.put("name", "foo")
|
||||
.put("path.home", createTempDir())
|
||||
.put("client.type", "node").build();
|
||||
AuthenticationModule module = new AuthenticationModule(settings);
|
||||
// setting it to null should have no effect
|
||||
if (randomBoolean()) {
|
||||
module.setAuthenticationFailureHandler(null);
|
||||
}
|
||||
|
||||
if (randomBoolean()) {
|
||||
module.addCustomRealm("custom", ESUsersRealm.Factory.class);
|
||||
}
|
||||
Environment env = new Environment(settings);
|
||||
ThreadPool pool = new ThreadPool(settings);
|
||||
|
||||
try {
|
||||
Injector injector = Guice.createInjector(module, new SettingsModule(settings), new AuditTrailModule(settings), new CryptoModule(settings), new EnvironmentModule(env), new ThreadPoolModule(pool));
|
||||
AuthenticationFailureHandler failureHandler = injector.getInstance(AuthenticationFailureHandler.class);
|
||||
assertThat(failureHandler, notNullValue());
|
||||
assertThat(failureHandler, instanceOf(DefaultAuthenticationFailureHandler.class));
|
||||
} finally {
|
||||
pool.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingFailureHandler() {
|
||||
Settings settings = Settings.builder()
|
||||
.put("name", "foo")
|
||||
.put("path.home", createTempDir())
|
||||
.put("client.type", "node").build();
|
||||
AuthenticationModule module = new AuthenticationModule(settings);
|
||||
module.setAuthenticationFailureHandler(NoOpFailureHandler.class);
|
||||
|
||||
if (randomBoolean()) {
|
||||
module.addCustomRealm("custom", ESUsersRealm.Factory.class);
|
||||
}
|
||||
Environment env = new Environment(settings);
|
||||
ThreadPool pool = new ThreadPool(settings);
|
||||
|
||||
try {
|
||||
Injector injector = Guice.createInjector(module, new SettingsModule(settings), new AuditTrailModule(settings), new CryptoModule(settings), new EnvironmentModule(env), new ThreadPoolModule(pool));
|
||||
AuthenticationFailureHandler failureHandler = injector.getInstance(AuthenticationFailureHandler.class);
|
||||
assertThat(failureHandler, notNullValue());
|
||||
assertThat(failureHandler, instanceOf(NoOpFailureHandler.class));
|
||||
} finally {
|
||||
pool.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// this class must be public for injection...
|
||||
public static class NoOpFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException unsuccessfulAuthentication(RestRequest request, AuthenticationToken token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException unsuccessfulAuthentication(TransportMessage message, AuthenticationToken token, String action) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException missingToken(RestRequest request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException missingToken(TransportMessage message, String action) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchSecurityException authenticationRequired(String action) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -76,7 +76,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase {
|
|||
|
||||
auditTrail = mock(AuditTrail.class);
|
||||
anonymousService = mock(AnonymousService.class);
|
||||
service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, anonymousService);
|
||||
service = new InternalAuthenticationService(Settings.EMPTY, realms, auditTrail, cryptoService, anonymousService, new DefaultAuthenticationFailureHandler());
|
||||
}
|
||||
|
||||
@Test @SuppressWarnings("unchecked")
|
||||
|
@ -343,7 +343,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase {
|
|||
@Test
|
||||
public void testAutheticate_Transport_ContextAndHeader_NoSigning() throws Exception {
|
||||
Settings settings = Settings.builder().put(InternalAuthenticationService.SETTING_SIGN_USER_HEADER, false).build();
|
||||
service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, anonymousService);
|
||||
service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, anonymousService, new DefaultAuthenticationFailureHandler());
|
||||
|
||||
User user1 = new User.Simple("username", "r1", "r2");
|
||||
when(firstRealm.supports(token)).thenReturn(true);
|
||||
|
@ -418,7 +418,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase {
|
|||
}
|
||||
Settings settings = builder.build();
|
||||
AnonymousService holder = new AnonymousService(settings);
|
||||
service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, holder);
|
||||
service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, holder, new DefaultAuthenticationFailureHandler());
|
||||
|
||||
RestRequest request = new FakeRestRequest();
|
||||
|
||||
|
@ -435,7 +435,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase {
|
|||
Settings settings = Settings.builder()
|
||||
.putArray("shield.authc.anonymous.roles", "r1", "r2", "r3")
|
||||
.build();
|
||||
service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new AnonymousService(settings));
|
||||
service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new AnonymousService(settings), new DefaultAuthenticationFailureHandler());
|
||||
|
||||
InternalMessage message = new InternalMessage();
|
||||
|
||||
|
@ -450,7 +450,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase {
|
|||
Settings settings = Settings.builder()
|
||||
.putArray("shield.authc.anonymous.roles", "r1", "r2", "r3")
|
||||
.build();
|
||||
service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new AnonymousService(settings));
|
||||
service = new InternalAuthenticationService(settings, realms, auditTrail, cryptoService, new AnonymousService(settings), new DefaultAuthenticationFailureHandler());
|
||||
|
||||
InternalMessage message = new InternalMessage();
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.elasticsearch.search.action.SearchServiceTransportAction;
|
|||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.audit.AuditTrail;
|
||||
import org.elasticsearch.shield.authc.AnonymousService;
|
||||
import org.elasticsearch.shield.authc.DefaultAuthenticationFailureHandler;
|
||||
import org.elasticsearch.shield.authz.store.RolesStore;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.transport.TransportRequest;
|
||||
|
@ -49,7 +50,7 @@ public class InternalAuthorizationServiceTests extends ESTestCase {
|
|||
clusterService = mock(ClusterService.class);
|
||||
auditTrail = mock(AuditTrail.class);
|
||||
AnonymousService anonymousService = new AnonymousService(Settings.EMPTY);
|
||||
internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, anonymousService);
|
||||
internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, anonymousService, new DefaultAuthenticationFailureHandler());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -290,7 +291,7 @@ public class InternalAuthorizationServiceTests extends ESTestCase {
|
|||
TransportRequest request = new IndicesExistsRequest("b");
|
||||
ClusterState state = mock(ClusterState.class);
|
||||
AnonymousService anonymousService = new AnonymousService(Settings.builder().put("shield.authc.anonymous.roles", "a_all").build());
|
||||
internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, anonymousService);
|
||||
internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, anonymousService, new DefaultAuthenticationFailureHandler());
|
||||
|
||||
when(rolesStore.role("a_all")).thenReturn(Permission.Global.Role.builder("a_all").add(Privilege.Index.ALL, "a").build());
|
||||
when(clusterService.state()).thenReturn(state);
|
||||
|
@ -315,7 +316,7 @@ public class InternalAuthorizationServiceTests extends ESTestCase {
|
|||
.put("shield.authc.anonymous.roles", "a_all")
|
||||
.put(AnonymousService.SETTING_AUTHORIZATION_EXCEPTION_ENABLED, false)
|
||||
.build());
|
||||
internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, anonymousService);
|
||||
internalAuthorizationService = new InternalAuthorizationService(Settings.EMPTY, rolesStore, clusterService, auditTrail, anonymousService, new DefaultAuthenticationFailureHandler());
|
||||
|
||||
when(rolesStore.role("a_all")).thenReturn(Permission.Global.Role.builder("a_all").add(Privilege.Index.ALL, "a").build());
|
||||
when(clusterService.state()).thenReturn(state);
|
||||
|
|
Loading…
Reference in New Issue