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:
jaymode 2015-08-11 12:17:26 -04:00
parent 7e552f393b
commit 8fd5fe7ed8
18 changed files with 939 additions and 33 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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> {

View File

@ -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());

View File

@ -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;
}
}
}

View File

@ -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();

View File

@ -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);