Merge branch 'master' into doc-draft
Merging PR elastic/elasticsearch#99 per Uri Original commit: elastic/x-pack-elasticsearch@022a898a9f
This commit is contained in:
commit
a0a7b9b7ff
20
pom.xml
20
pom.xml
|
@ -30,8 +30,9 @@
|
|||
</repositories>
|
||||
|
||||
<properties>
|
||||
<lucene.version>4.10.0</lucene.version>
|
||||
<elasticsearch.version>1.4.0-SNAPSHOT</elasticsearch.version>
|
||||
<lucene.version>4.10.1</lucene.version>
|
||||
<lucene.maven.version>4.10.1</lucene.maven.version>
|
||||
<elasticsearch.version>1.4.0.Beta1-SNAPSHOT</elasticsearch.version>
|
||||
|
||||
<tests.jvms>auto</tests.jvms>
|
||||
<tests.shuffle>true</tests.shuffle>
|
||||
|
@ -42,6 +43,7 @@
|
|||
<tests.heap.size>512m</tests.heap.size>
|
||||
<tests.topn>5</tests.topn>
|
||||
<execution.hint.file>.local-${project.version}-execution-hints.log</execution.hint.file>
|
||||
<tests.rest>false</tests.rest>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
@ -49,7 +51,7 @@
|
|||
<dependency>
|
||||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-test-framework</artifactId>
|
||||
<version>${lucene.version}</version>
|
||||
<version>${lucene.maven.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
@ -119,6 +121,18 @@
|
|||
<version>2.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.3.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.groovy</groupId>
|
||||
<artifactId>groovy-all</artifactId>
|
||||
<version>2.3.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- real dependencies -->
|
||||
|
||||
|
|
|
@ -8,20 +8,30 @@ package org.elasticsearch.shield;
|
|||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.action.search.ClearScrollRequest;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.action.search.SearchScrollRequest;
|
||||
import org.elasticsearch.action.support.ActionFilter;
|
||||
import org.elasticsearch.action.support.ActionFilterChain;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.rest.*;
|
||||
import org.elasticsearch.shield.audit.AuditTrail;
|
||||
import org.elasticsearch.shield.authc.AuthenticationService;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.shield.authc.system.SystemRealm;
|
||||
import org.elasticsearch.shield.authz.AuthorizationException;
|
||||
import org.elasticsearch.shield.authz.AuthorizationService;
|
||||
import org.elasticsearch.shield.authz.SystemRole;
|
||||
import org.elasticsearch.shield.key.KeyService;
|
||||
import org.elasticsearch.shield.key.SignatureException;
|
||||
import org.elasticsearch.shield.transport.TransportFilter;
|
||||
import org.elasticsearch.transport.TransportRequest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
@ -29,15 +39,19 @@ public class SecurityFilter extends AbstractComponent {
|
|||
|
||||
private final AuthenticationService authcService;
|
||||
private final AuthorizationService authzService;
|
||||
private final KeyService keyService;
|
||||
private final AuditTrail auditTrail;
|
||||
|
||||
@Inject
|
||||
public SecurityFilter(Settings settings, AuthenticationService authcService, AuthorizationService authzService) {
|
||||
public SecurityFilter(Settings settings, AuthenticationService authcService, AuthorizationService authzService, KeyService keyService, AuditTrail auditTrail) {
|
||||
super(settings);
|
||||
this.authcService = authcService;
|
||||
this.authzService = authzService;
|
||||
this.keyService = keyService;
|
||||
this.auditTrail = auditTrail;
|
||||
}
|
||||
|
||||
void process(String action, TransportRequest request) {
|
||||
User authenticateAndAuthorize(String action, TransportRequest request) {
|
||||
|
||||
// if the action is a system action, we'll fall back on the system user, otherwise we
|
||||
// won't fallback on any user and an authentication exception will be thrown
|
||||
|
@ -46,6 +60,56 @@ public class SecurityFilter extends AbstractComponent {
|
|||
AuthenticationToken token = authcService.token(action, request, defaultToken);
|
||||
User user = authcService.authenticate(action, request, token);
|
||||
authzService.authorize(user, action, request);
|
||||
return user;
|
||||
}
|
||||
|
||||
User authenticate(RestRequest request) {
|
||||
AuthenticationToken token = authcService.token(request);
|
||||
return authcService.authenticate(request, token);
|
||||
}
|
||||
|
||||
<Request extends ActionRequest> Request unsign(User user, String action, Request request) {
|
||||
|
||||
try {
|
||||
|
||||
if (request instanceof SearchScrollRequest) {
|
||||
SearchScrollRequest scrollRequest = (SearchScrollRequest) request;
|
||||
String scrollId = scrollRequest.scrollId();
|
||||
scrollRequest.scrollId(keyService.unsignAndVerify(scrollId));
|
||||
return request;
|
||||
}
|
||||
|
||||
if (request instanceof ClearScrollRequest) {
|
||||
ClearScrollRequest clearScrollRequest = (ClearScrollRequest) request;
|
||||
List<String> signedIds = clearScrollRequest.scrollIds();
|
||||
List<String> unsignedIds = new ArrayList<>(signedIds.size());
|
||||
for (String signedId : signedIds) {
|
||||
unsignedIds.add(keyService.unsignAndVerify(signedId));
|
||||
}
|
||||
clearScrollRequest.scrollIds(unsignedIds);
|
||||
return request;
|
||||
}
|
||||
|
||||
return request;
|
||||
|
||||
} catch (SignatureException se) {
|
||||
auditTrail.tamperedRequest(user, action, request);
|
||||
throw new AuthorizationException("Invalid request: " + se.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
<Response extends ActionResponse> Response sign(User user, String action, Response response) {
|
||||
|
||||
if (response instanceof SearchResponse) {
|
||||
SearchResponse searchResponse = (SearchResponse) response;
|
||||
String scrollId = searchResponse.getScrollId();
|
||||
if (scrollId != null && !keyService.signed(scrollId)) {
|
||||
searchResponse.scrollId(keyService.sign(scrollId));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public static class Rest extends RestFilter {
|
||||
|
@ -65,7 +129,7 @@ public class SecurityFilter extends AbstractComponent {
|
|||
|
||||
@Override
|
||||
public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) throws Exception {
|
||||
filter.authcService.extractAndRegisterToken(request);
|
||||
filter.authenticate(request);
|
||||
filterChain.continueProcessing(request, channel);
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +145,7 @@ public class SecurityFilter extends AbstractComponent {
|
|||
|
||||
@Override
|
||||
public void inboundRequest(String action, TransportRequest request) {
|
||||
filter.process(action, request);
|
||||
filter.authenticateAndAuthorize(action, request);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,8 +161,9 @@ public class SecurityFilter extends AbstractComponent {
|
|||
@Override
|
||||
public void apply(String action, ActionRequest request, ActionListener listener, ActionFilterChain chain) {
|
||||
try {
|
||||
filter.process(action, request);
|
||||
chain.proceed(action, request, listener);
|
||||
User user = filter.authenticateAndAuthorize(action, request);
|
||||
request = filter.unsign(user, action, request);
|
||||
chain.proceed(action, request, new SigningListener(user, action, filter, listener));
|
||||
} catch (Throwable t) {
|
||||
listener.onFailure(t);
|
||||
}
|
||||
|
@ -114,4 +179,30 @@ public class SecurityFilter extends AbstractComponent {
|
|||
return Integer.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
static class SigningListener<Response extends ActionResponse> implements ActionListener<Response> {
|
||||
|
||||
private final User user;
|
||||
private final String action;
|
||||
private final SecurityFilter filter;
|
||||
private final ActionListener innerListener;
|
||||
|
||||
private SigningListener(User user, String action, SecurityFilter filter, ActionListener innerListener) {
|
||||
this.user = user;
|
||||
this.action = action;
|
||||
this.filter = filter;
|
||||
this.innerListener = innerListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Response response) {
|
||||
response = this.filter.sign(user, action, response);
|
||||
innerListener.onResponse(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable e) {
|
||||
innerListener.onFailure(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,15 +5,20 @@
|
|||
*/
|
||||
package org.elasticsearch.shield;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SecurityFilterModule extends AbstractModule {
|
||||
public class SecurityFilterModule extends AbstractShieldModule.Node {
|
||||
|
||||
public SecurityFilterModule(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
protected void configureNode() {
|
||||
bind(SecurityFilter.class).asEagerSingleton();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,24 +9,24 @@ import org.elasticsearch.ElasticsearchException;
|
|||
|
||||
import org.elasticsearch.common.collect.ImmutableMap;
|
||||
import org.elasticsearch.common.collect.Lists;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SecurityException extends ElasticsearchException.WithRestHeaders {
|
||||
public class ShieldException extends ElasticsearchException.WithRestHeaders {
|
||||
|
||||
public static final ImmutableMap<String, List<String>> HEADERS = ImmutableMap.<String, List<String>>builder()
|
||||
.put("WWW-Authenticate", Lists.newArrayList("Basic realm=\""+ SecurityPlugin.NAME +"\""))
|
||||
.put("WWW-Authenticate", Lists.newArrayList("Basic realm=\""+ ShieldPlugin.NAME +"\""))
|
||||
.build();
|
||||
|
||||
public SecurityException(String msg) {
|
||||
public ShieldException(String msg) {
|
||||
super(msg, HEADERS);
|
||||
}
|
||||
|
||||
public SecurityException(String msg, Throwable cause) {
|
||||
public ShieldException(String msg, Throwable cause) {
|
||||
super(msg, cause, HEADERS);
|
||||
}
|
||||
}
|
|
@ -7,61 +7,60 @@ package org.elasticsearch.shield;
|
|||
|
||||
import org.elasticsearch.action.ActionModule;
|
||||
import org.elasticsearch.common.collect.ImmutableList;
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.Module;
|
||||
import org.elasticsearch.common.inject.PreProcessModule;
|
||||
import org.elasticsearch.common.inject.SpawnModules;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.audit.AuditTrailModule;
|
||||
import org.elasticsearch.shield.authc.AuthenticationModule;
|
||||
import org.elasticsearch.shield.authz.AuthorizationModule;
|
||||
import org.elasticsearch.shield.key.KeyModule;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
import org.elasticsearch.shield.transport.SecuredRestModule;
|
||||
import org.elasticsearch.shield.transport.SecuredTransportModule;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SecurityModule extends AbstractModule implements SpawnModules, PreProcessModule {
|
||||
public class ShieldModule extends AbstractShieldModule.Spawn implements PreProcessModule {
|
||||
|
||||
private final Settings settings;
|
||||
private final boolean isClient;
|
||||
private final boolean isShieldEnabled;
|
||||
private final boolean enabled;
|
||||
|
||||
public SecurityModule(Settings settings) {
|
||||
this.settings = settings;
|
||||
this.isClient = settings.getAsBoolean("node.client", false);
|
||||
this.isShieldEnabled = settings.getAsBoolean("shield.enabled", true);
|
||||
public ShieldModule(Settings settings) {
|
||||
super(settings);
|
||||
this.enabled = settings.getAsBoolean("shield.enabled", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processModule(Module module) {
|
||||
if (module instanceof ActionModule && isShieldEnabled && !isClient) {
|
||||
if (module instanceof ActionModule && enabled && !clientMode) {
|
||||
((ActionModule) module).registerFilter(SecurityFilter.Action.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends Module> spawnModules() {
|
||||
public Iterable<? extends Module> spawnModules(boolean clientMode) {
|
||||
// don't spawn modules if shield is explicitly disabled
|
||||
if (!isShieldEnabled) {
|
||||
if (!enabled) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
// spawn needed parts in client mode
|
||||
if (isClient) {
|
||||
return ImmutableList.of(new SecuredTransportModule(), new SecurityFilterModule());
|
||||
if (clientMode) {
|
||||
return ImmutableList.of(new SecuredTransportModule(settings));
|
||||
}
|
||||
|
||||
return ImmutableList.of(
|
||||
new AuthenticationModule(settings),
|
||||
new AuthorizationModule(),
|
||||
new AuthorizationModule(settings),
|
||||
new AuditTrailModule(settings),
|
||||
new SecuredTransportModule(),
|
||||
new SecuredRestModule(),
|
||||
new SecurityFilterModule());
|
||||
new SecuredTransportModule(settings),
|
||||
new SecuredRestModule(settings),
|
||||
new SecurityFilterModule(settings),
|
||||
new KeyModule(settings));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
protected void configure(boolean clientMode) {
|
||||
}
|
||||
}
|
|
@ -8,13 +8,13 @@ package org.elasticsearch.shield;
|
|||
/**
|
||||
*
|
||||
*/
|
||||
public class SecuritySettingsException extends SecurityException {
|
||||
public class ShieldSettingsException extends ShieldException {
|
||||
|
||||
public SecuritySettingsException(String msg) {
|
||||
public ShieldSettingsException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public SecuritySettingsException(String msg, Throwable cause) {
|
||||
public ShieldSettingsException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ public class ShieldVersion implements Serializable {
|
|||
// the (internal) format of the id is there so we can easily do after/before checks on the id
|
||||
|
||||
public static final int V_1_0_0_ID = /*00*/1000099;
|
||||
public static final ShieldVersion V_1_0_0 = new ShieldVersion(V_1_0_0_ID, true, Version.V_1_4_0);
|
||||
public static final ShieldVersion V_1_0_0 = new ShieldVersion(V_1_0_0_ID, true, Version.V_1_4_0_Beta1);
|
||||
|
||||
public static final ShieldVersion CURRENT = V_1_0_0;
|
||||
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.audit;
|
||||
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class AuditException extends org.elasticsearch.shield.SecurityException {
|
||||
public class AuditException extends ShieldException {
|
||||
|
||||
public AuditException(String msg) {
|
||||
super(msg);
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.audit;
|
||||
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
import org.elasticsearch.transport.TransportRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -31,10 +33,18 @@ public interface AuditTrail {
|
|||
public void authenticationFailed(AuthenticationToken token, String action, TransportMessage<?> message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(AuthenticationToken token, RestRequest request) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage<?> message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accessGranted(User user, String action, TransportMessage<?> message) {
|
||||
}
|
||||
|
@ -42,6 +52,10 @@ public interface AuditTrail {
|
|||
@Override
|
||||
public void accessDenied(User user, String action, TransportMessage<?> message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tamperedRequest(User user, String action, TransportRequest request) {
|
||||
}
|
||||
};
|
||||
|
||||
String name();
|
||||
|
@ -50,10 +64,16 @@ public interface AuditTrail {
|
|||
|
||||
void authenticationFailed(AuthenticationToken token, String action, TransportMessage<?> message);
|
||||
|
||||
void authenticationFailed(AuthenticationToken token, RestRequest request);
|
||||
|
||||
void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage<?> message);
|
||||
|
||||
void authenticationFailed(String realm, AuthenticationToken token, RestRequest request);
|
||||
|
||||
void accessGranted(User user, String action, TransportMessage<?> message);
|
||||
|
||||
void accessDenied(User user, String action, TransportMessage<?> message);
|
||||
|
||||
void tamperedRequest(User user, String action, TransportRequest request);
|
||||
|
||||
}
|
||||
|
|
|
@ -7,27 +7,28 @@ package org.elasticsearch.shield.audit;
|
|||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.collect.Sets;
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.multibindings.Multibinder;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.audit.logfile.LoggingAuditTrail;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class AuditTrailModule extends AbstractModule {
|
||||
public class AuditTrailModule extends AbstractShieldModule.Node {
|
||||
|
||||
private final Settings settings;
|
||||
private final boolean enabled;
|
||||
|
||||
public AuditTrailModule(Settings settings) {
|
||||
this.settings = settings;
|
||||
super(settings);
|
||||
enabled = settings.getAsBoolean("shield.audit.enabled", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
if (!settings.getAsBoolean("shield.audit.enabled", false)) {
|
||||
protected void configureNode() {
|
||||
if (!enabled) {
|
||||
bind(AuditTrail.class).toInstance(AuditTrail.NOOP);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -8,9 +8,11 @@ package org.elasticsearch.shield.audit;
|
|||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
import org.elasticsearch.transport.TransportRequest;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -34,37 +36,57 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail {
|
|||
|
||||
@Override
|
||||
public void anonymousAccess(String action, TransportMessage<?> message) {
|
||||
for (int i = 0; i < auditTrails.length; i++) {
|
||||
auditTrails[i].anonymousAccess(action, message);
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
auditTrail.anonymousAccess(action, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(AuthenticationToken token, String action, TransportMessage<?> message) {
|
||||
for (int i = 0; i < auditTrails.length; i++) {
|
||||
auditTrails[i].authenticationFailed(token, action, message);
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
auditTrail.authenticationFailed(token, action, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage<?> message) {
|
||||
for (int i = 0; i < auditTrails.length; i++) {
|
||||
auditTrails[i].authenticationFailed(realm, token, action, message);
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
auditTrail.authenticationFailed(realm, token, action, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(AuthenticationToken token, RestRequest request) {
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
auditTrail.authenticationFailed(token, request);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) {
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
auditTrail.authenticationFailed(realm, token, request);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accessGranted(User user, String action, TransportMessage<?> message) {
|
||||
for (int i = 0; i < auditTrails.length; i++) {
|
||||
auditTrails[i].accessGranted(user, action, message);
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
auditTrail.accessGranted(user, action, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accessDenied(User user, String action, TransportMessage<?> message) {
|
||||
for (int i = 0; i < auditTrails.length; i++) {
|
||||
auditTrails[i].accessDenied(user, action, message);
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
auditTrail.accessDenied(user, action, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tamperedRequest(User user, String action, TransportRequest request) {
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
auditTrail.tamperedRequest(user, action, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ import org.elasticsearch.common.inject.Inject;
|
|||
import org.elasticsearch.common.logging.ESLogger;
|
||||
import org.elasticsearch.common.logging.Loggers;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.audit.AuditTrail;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
import org.elasticsearch.transport.TransportRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -55,6 +57,15 @@ public class LoggingAuditTrail implements AuditTrail {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(AuthenticationToken token, RestRequest request) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("AUTHENTICATION_FAILED\thost=[{}], URI=[{}], principal=[{}], request=[{}]", request.getRemoteAddress(), request.uri(), token.principal(), request);
|
||||
} else {
|
||||
logger.error("AUTHENTICATION_FAILED\thost=[{}], URI=[{}], principal=[{}]", request.getRemoteAddress(), request.uri(), token.principal());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage<?> message) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
|
@ -62,6 +73,13 @@ public class LoggingAuditTrail implements AuditTrail {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("AUTHENTICATION_FAILED[{}]\thost=[{}], URI=[{}], principal=[{}], request=[{}]", realm, request.getRemoteAddress(), request.uri(), token.principal(), request);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accessGranted(User user, String action, TransportMessage<?> message) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
@ -80,4 +98,12 @@ public class LoggingAuditTrail implements AuditTrail {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tamperedRequest(User user, String action, TransportRequest request) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("TAMPERED REQUEST\thost=[{}], action=[{}], principal=[{}], request=[{}]", request.remoteAddress(), action, user.principal(), request);
|
||||
} else {
|
||||
logger.error("TAMPERED REQUEST\thost=[{}], action=[{}], principal=[{}]", request.remoteAddress(), action, user.principal());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
package org.elasticsearch.shield.authc;
|
||||
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class AuthenticationException extends org.elasticsearch.shield.SecurityException {
|
||||
public class AuthenticationException extends ShieldException {
|
||||
|
||||
public AuthenticationException(String msg) {
|
||||
super(msg);
|
||||
|
|
|
@ -6,55 +6,31 @@
|
|||
package org.elasticsearch.shield.authc;
|
||||
|
||||
import org.elasticsearch.common.collect.ImmutableList;
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.Module;
|
||||
import org.elasticsearch.common.inject.SpawnModules;
|
||||
import org.elasticsearch.common.inject.util.Providers;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authc.esusers.ESUsersModule;
|
||||
import org.elasticsearch.shield.authc.esusers.ESUsersRealm;
|
||||
import org.elasticsearch.shield.authc.ldap.LdapModule;
|
||||
import org.elasticsearch.shield.authc.ldap.LdapRealm;
|
||||
import org.elasticsearch.shield.authc.system.SystemRealm;
|
||||
|
||||
import static org.elasticsearch.common.inject.name.Names.named;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class AuthenticationModule extends AbstractModule implements SpawnModules {
|
||||
|
||||
private final Settings settings;
|
||||
private final boolean esusersEnabled;
|
||||
private final boolean ldapEnabled;
|
||||
public class AuthenticationModule extends AbstractShieldModule.Node.Spawn {
|
||||
|
||||
public AuthenticationModule(Settings settings) {
|
||||
this.settings = settings;
|
||||
this.esusersEnabled = ESUsersModule.enabled(settings);
|
||||
this.ldapEnabled = LdapModule.enabled(settings);
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends Module> spawnModules() {
|
||||
ImmutableList.Builder<Module> modules = ImmutableList.builder();
|
||||
modules.add(new SystemRealm.Module());
|
||||
if (esusersEnabled) {
|
||||
modules.add(new ESUsersModule());
|
||||
}
|
||||
if (ldapEnabled) {
|
||||
modules.add(new LdapModule(settings));
|
||||
}
|
||||
return modules.build();
|
||||
public Iterable<? extends Node> spawnModules() {
|
||||
return ImmutableList.of(
|
||||
new SystemRealm.Module(settings),
|
||||
new ESUsersModule(settings),
|
||||
new LdapModule(settings));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
protected void configureNode() {
|
||||
bind(AuthenticationService.class).to(InternalAuthenticationService.class).asEagerSingleton();
|
||||
if (!esusersEnabled) {
|
||||
bind(ESUsersRealm.class).toProvider(Providers.of((ESUsersRealm) null));
|
||||
}
|
||||
if (!ldapEnabled) {
|
||||
bind(LdapRealm.class).toProvider(Providers.of((LdapRealm) null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ public interface AuthenticationService {
|
|||
* Extracts an authentication token from the given rest request and if found registers it on
|
||||
* the request. If not found, an {@link AuthenticationException} is thrown.
|
||||
*/
|
||||
void extractAndRegisterToken(RestRequest request) throws AuthenticationException;
|
||||
AuthenticationToken token(RestRequest request) throws AuthenticationException;
|
||||
|
||||
/**
|
||||
* Extracts the authenticate token from the given message. If no recognized auth token is associated
|
||||
|
@ -49,4 +49,18 @@ public interface AuthenticationService {
|
|||
*/
|
||||
User authenticate(String action, TransportMessage<?> message, AuthenticationToken token) throws AuthenticationException;
|
||||
|
||||
/**
|
||||
* Authenticates the user associated with the given request based on the given authentication token.
|
||||
*
|
||||
* On successful authentication, the {@link org.elasticsearch.shield.User user} that is associated
|
||||
* with the request (i.e. that is associated with the token's {@link AuthenticationToken#principal() principal})
|
||||
* will be returned. If authentication fails, an {@link AuthenticationException} will be thrown.
|
||||
*
|
||||
* @param request The executed request
|
||||
* @param token The authentication token associated with the given request (must not be {@code null})
|
||||
* @return The authenticated User
|
||||
* @throws AuthenticationException If no user could be authenticated (can either be due to missing
|
||||
* supported authentication token, or simply due to bad credentials.
|
||||
*/
|
||||
User authenticate(RestRequest request, AuthenticationToken token) throws AuthenticationException;
|
||||
}
|
||||
|
|
|
@ -13,4 +13,6 @@ public interface AuthenticationToken {
|
|||
String principal();
|
||||
|
||||
Object credentials();
|
||||
|
||||
void clearCredentials();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc;
|
||||
|
||||
import org.elasticsearch.common.ContextHolder;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.inject.internal.Nullable;
|
||||
|
@ -35,12 +36,12 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void extractAndRegisterToken(RestRequest request) throws AuthenticationException {
|
||||
public AuthenticationToken token(RestRequest request) throws AuthenticationException {
|
||||
for (Realm realm : realms) {
|
||||
AuthenticationToken token = realm.token(request);
|
||||
if (token != null) {
|
||||
request.putInContext(TOKEN_CTX_KEY, token);
|
||||
return;
|
||||
return token;
|
||||
}
|
||||
}
|
||||
throw new AuthenticationException("Missing authentication token");
|
||||
|
@ -97,24 +98,52 @@ public class InternalAuthenticationService extends AbstractComponent implements
|
|||
@SuppressWarnings("unchecked")
|
||||
public User authenticate(String action, TransportMessage<?> message, AuthenticationToken token) throws AuthenticationException {
|
||||
assert token != null : "cannot authenticate null tokens";
|
||||
User user = (User) message.getContext().get(USER_CTX_KEY);
|
||||
if (user != null) {
|
||||
return user;
|
||||
}
|
||||
for (Realm realm : realms) {
|
||||
if (realm.supports(token)) {
|
||||
user = realm.authenticate(token);
|
||||
if (user != null) {
|
||||
message.putInContext(USER_CTX_KEY, user);
|
||||
return user;
|
||||
} else if (auditTrail != null) {
|
||||
auditTrail.authenticationFailed(realm.type(), token, action, message);
|
||||
try {
|
||||
User user = (User) message.getContext().get(USER_CTX_KEY);
|
||||
if (user != null) {
|
||||
return user;
|
||||
}
|
||||
for (Realm realm : realms) {
|
||||
if (realm.supports(token)) {
|
||||
user = realm.authenticate(token);
|
||||
if (user != null) {
|
||||
message.putInContext(USER_CTX_KEY, user);
|
||||
return user;
|
||||
} else if (auditTrail != null) {
|
||||
auditTrail.authenticationFailed(realm.type(), token, action, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auditTrail != null) {
|
||||
auditTrail.authenticationFailed(token, action, message);
|
||||
}
|
||||
throw new AuthenticationException("Unable to authenticate user for request");
|
||||
} finally {
|
||||
token.clearCredentials();
|
||||
}
|
||||
if (auditTrail != null) {
|
||||
auditTrail.authenticationFailed(token, action, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User authenticate(RestRequest request, AuthenticationToken token) throws AuthenticationException {
|
||||
assert token != null : "cannot authenticate null tokens";
|
||||
try {
|
||||
for (Realm realm : realms) {
|
||||
if (realm.supports(token)) {
|
||||
User user = realm.authenticate(token);
|
||||
if (user != null) {
|
||||
request.putInContext(USER_CTX_KEY, user);
|
||||
return user;
|
||||
} else if (auditTrail != null) {
|
||||
auditTrail.authenticationFailed(realm.type(), token, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auditTrail != null) {
|
||||
auditTrail.authenticationFailed(token, request);
|
||||
}
|
||||
throw new AuthenticationException("Unable to authenticate user for request");
|
||||
} finally {
|
||||
token.clearCredentials();
|
||||
}
|
||||
throw new AuthenticationException("Unable to authenticate user for request");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,27 +5,39 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.esusers;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.util.Providers;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authc.Realm;
|
||||
import org.elasticsearch.shield.authc.support.UserPasswdStore;
|
||||
import org.elasticsearch.shield.authc.support.UserRolesStore;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
import static org.elasticsearch.common.inject.name.Names.named;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ESUsersModule extends AbstractModule {
|
||||
public class ESUsersModule extends AbstractShieldModule.Node {
|
||||
|
||||
public static boolean enabled(Settings settings) {
|
||||
return settings.getComponentSettings(ESUsersModule.class).getAsBoolean("enabled", true);
|
||||
private final boolean enabled;
|
||||
|
||||
public ESUsersModule(Settings settings) {
|
||||
super(settings);
|
||||
enabled = enabled(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(Realm.class).annotatedWith(named(ESUsersRealm.TYPE)).to(ESUsersRealm.class).asEagerSingleton();
|
||||
bind(UserPasswdStore.class).annotatedWith(named("file")).to(FileUserPasswdStore.class).asEagerSingleton();
|
||||
bind(UserRolesStore.class).annotatedWith(named("file")).to(FileUserRolesStore.class).asEagerSingleton();
|
||||
protected void configureNode() {
|
||||
if (enabled) {
|
||||
bind(Realm.class).annotatedWith(named(ESUsersRealm.TYPE)).to(ESUsersRealm.class).asEagerSingleton();
|
||||
bind(UserPasswdStore.class).annotatedWith(named("file")).to(FileUserPasswdStore.class).asEagerSingleton();
|
||||
bind(UserRolesStore.class).annotatedWith(named("file")).to(FileUserRolesStore.class).asEagerSingleton();
|
||||
} else {
|
||||
bind(ESUsersRealm.class).toProvider(Providers.<ESUsersRealm>of(null));
|
||||
}
|
||||
}
|
||||
|
||||
static boolean enabled(Settings settings) {
|
||||
return settings.getComponentSettings(ESUsersModule.class).getAsBoolean("enabled", true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import org.elasticsearch.common.component.AbstractComponent;
|
|||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.inject.name.Named;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
|
@ -25,20 +25,18 @@ import org.elasticsearch.transport.TransportMessage;
|
|||
*/
|
||||
public class ESUsersRealm extends AbstractComponent implements Realm<UsernamePasswordToken> {
|
||||
|
||||
static {
|
||||
BaseRestHandler.addUsefulHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
|
||||
}
|
||||
|
||||
public static final String TYPE = "esusers";
|
||||
|
||||
final UserPasswdStore userPasswdStore;
|
||||
final UserRolesStore userRolesStore;
|
||||
|
||||
@Inject
|
||||
public ESUsersRealm(Settings settings, @Named("file") UserPasswdStore userPasswdStore, @Named("file") UserRolesStore userRolesStore) {
|
||||
public ESUsersRealm(Settings settings, @Named("file") UserPasswdStore userPasswdStore,
|
||||
@Named("file") UserRolesStore userRolesStore, RestController restController) {
|
||||
super(settings);
|
||||
this.userPasswdStore = userPasswdStore;
|
||||
this.userRolesStore = userRolesStore;
|
||||
restController.registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -15,8 +15,9 @@ import org.elasticsearch.common.logging.ESLogger;
|
|||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.authc.support.Hasher;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
import org.elasticsearch.shield.authc.support.UserPasswdStore;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
import org.elasticsearch.watcher.FileWatcher;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
@ -61,7 +62,7 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyPassword(String username, char[] password) {
|
||||
public boolean verifyPassword(String username, SecuredString password) {
|
||||
if (esUsers == null) {
|
||||
return false;
|
||||
}
|
||||
|
@ -75,8 +76,7 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd
|
|||
public static Path resolveFile(Settings settings, Environment env) {
|
||||
String location = settings.get("shield.authc.esusers.files.users");
|
||||
if (location == null) {
|
||||
File shieldDirectory = new File(env.configFile(), SecurityPlugin.NAME);
|
||||
return shieldDirectory.toPath().resolve(".users");
|
||||
return ShieldPlugin.resolveConfigFile(env, ".users");
|
||||
}
|
||||
return Paths.get(location);
|
||||
}
|
||||
|
@ -86,6 +86,9 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd
|
|||
* empty map is returned
|
||||
*/
|
||||
public static ImmutableMap<String, char[]> parseFile(Path path, @Nullable ESLogger logger) {
|
||||
if (logger != null) {
|
||||
logger.trace("Reading users file located at [{}]", path);
|
||||
}
|
||||
if (!Files.exists(path)) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import org.elasticsearch.common.logging.ESLogger;
|
|||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.authc.support.UserRolesStore;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
import org.elasticsearch.watcher.FileWatcher;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
@ -68,8 +68,7 @@ public class FileUserRolesStore extends AbstractComponent implements UserRolesSt
|
|||
public static Path resolveFile(Settings settings, Environment env) {
|
||||
String location = settings.get("shield.authc.esusers.files.users_roles");
|
||||
if (location == null) {
|
||||
File shieldDirectory = new File(env.configFile(), SecurityPlugin.NAME);
|
||||
return shieldDirectory.toPath().resolve(".users_roles");
|
||||
return ShieldPlugin.resolveConfigFile(env, ".users_roles");
|
||||
}
|
||||
return Paths.get(location);
|
||||
}
|
||||
|
@ -79,6 +78,10 @@ public class FileUserRolesStore extends AbstractComponent implements UserRolesSt
|
|||
* an empty map is returned
|
||||
*/
|
||||
public static ImmutableMap<String, String[]> parseFile(Path path, @Nullable ESLogger logger) {
|
||||
if (logger != null) {
|
||||
logger.trace("Reading users roles file located at [{}]", path);
|
||||
}
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
|
|
@ -11,12 +11,16 @@ import org.elasticsearch.common.cli.CliTool;
|
|||
import org.elasticsearch.common.cli.CliToolConfig;
|
||||
import org.elasticsearch.common.cli.Terminal;
|
||||
import org.elasticsearch.common.cli.commons.CommandLine;
|
||||
import org.elasticsearch.common.collect.*;
|
||||
import org.elasticsearch.common.collect.Lists;
|
||||
import org.elasticsearch.common.collect.Maps;
|
||||
import org.elasticsearch.common.collect.ObjectArrays;
|
||||
import org.elasticsearch.common.collect.Sets;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.authc.esusers.FileUserPasswdStore;
|
||||
import org.elasticsearch.shield.authc.esusers.FileUserRolesStore;
|
||||
import org.elasticsearch.shield.authc.support.Hasher;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
@ -97,14 +101,14 @@ public class ESUsersTool extends CliTool {
|
|||
|
||||
String rolesCsv = cli.getOptionValue("roles");
|
||||
String[] roles = (rolesCsv != null) ? rolesCsv.split(",") : Strings.EMPTY_ARRAY;
|
||||
return new Useradd(terminal, username, password, roles);
|
||||
return new Useradd(terminal, username, new SecuredString(password), roles);
|
||||
}
|
||||
|
||||
final String username;
|
||||
final char[] passwd;
|
||||
final SecuredString passwd;
|
||||
final String[] roles;
|
||||
|
||||
Useradd(Terminal terminal, String username, char[] passwd, String... roles) {
|
||||
Useradd(Terminal terminal, String username, SecuredString passwd, String... roles) {
|
||||
super(terminal);
|
||||
this.username = username;
|
||||
this.passwd = passwd;
|
||||
|
@ -212,12 +216,13 @@ public class ESUsersTool extends CliTool {
|
|||
}
|
||||
|
||||
final String username;
|
||||
final char[] passwd;
|
||||
final SecuredString passwd;
|
||||
|
||||
Passwd(Terminal terminal, String username, char[] passwd) {
|
||||
super(terminal);
|
||||
this.username = username;
|
||||
this.passwd = passwd;
|
||||
this.passwd = new SecuredString(passwd);
|
||||
Arrays.fill(passwd, (char) 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.elasticsearch.common.collect.ImmutableMap;
|
|||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
|
||||
import javax.naming.Context;
|
||||
import javax.naming.NamingEnumeration;
|
||||
|
@ -42,7 +44,7 @@ public class ActiveDirectoryConnectionFactory extends AbstractComponent implemen
|
|||
super(settings);
|
||||
domainName = componentSettings.get(AD_DOMAIN_NAME_SETTING);
|
||||
if (domainName == null) {
|
||||
throw new org.elasticsearch.shield.SecurityException("Missing [" + AD_DOMAIN_NAME_SETTING + "] setting for active directory");
|
||||
throw new ShieldException("Missing [" + AD_DOMAIN_NAME_SETTING + "] setting for active directory");
|
||||
}
|
||||
userSearchDN = componentSettings.get(AD_USER_SEARCH_BASEDN_SETTING, buildDnFromDomain(domainName));
|
||||
int port = componentSettings.getAsInt(AD_PORT, 389);
|
||||
|
@ -62,13 +64,12 @@ public class ActiveDirectoryConnectionFactory extends AbstractComponent implemen
|
|||
* @return An authenticated
|
||||
*/
|
||||
@Override
|
||||
public LdapConnection bind(String userName, char[] password) {
|
||||
public LdapConnection bind(String userName, SecuredString password) {
|
||||
String userPrincipal = userName + "@" + this.domainName;
|
||||
|
||||
Hashtable<String, Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
|
||||
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
|
||||
ldapEnv.put(Context.SECURITY_PRINCIPAL, userPrincipal);
|
||||
ldapEnv.put(Context.SECURITY_CREDENTIALS, password);
|
||||
ldapEnv.put(Context.SECURITY_CREDENTIALS, password.internalChars());
|
||||
|
||||
try {
|
||||
DirContext ctx = new InitialDirContext(ldapEnv);
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.ldap;
|
||||
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
|
||||
/**
|
||||
* This factory holds settings needed for authenticating to LDAP and creating LdapConnections.
|
||||
* Each created LdapConnection needs to be closed or else connections will pill up consuming resources.
|
||||
|
@ -24,6 +26,6 @@ public interface LdapConnectionFactory {
|
|||
* Password authenticated bind
|
||||
* @param user name of the user to authenticate the connection with.
|
||||
*/
|
||||
LdapConnection bind(String user, char[] password) ;
|
||||
LdapConnection bind(String user, SecuredString password) ;
|
||||
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.elasticsearch.common.logging.ESLogger;
|
|||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
import org.elasticsearch.watcher.FileWatcher;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
@ -32,7 +33,7 @@ import java.util.*;
|
|||
*/
|
||||
public class LdapGroupToRoleMapper extends AbstractComponent {
|
||||
|
||||
public static final String ROLE_MAPPING_DEFAULT_FILE_NAME = ".role_mapping";
|
||||
public static final String DEFAULT_FILE_NAME = ".role_mapping";
|
||||
public static final String ROLE_MAPPING_FILE_SETTING = "files.role_mapping";
|
||||
public static final String USE_UNMAPPED_GROUPS_AS_ROLES_SETTING = "unmapped_groups_as_roles";
|
||||
|
||||
|
@ -57,6 +58,14 @@ public class LdapGroupToRoleMapper extends AbstractComponent {
|
|||
this.listener = listener;
|
||||
}
|
||||
|
||||
public static Path resolveFile(Settings settings, Environment env) {
|
||||
String location = settings.get(ROLE_MAPPING_FILE_SETTING);
|
||||
if (location == null) {
|
||||
return ShieldPlugin.resolveConfigFile(env, DEFAULT_FILE_NAME);
|
||||
}
|
||||
return Paths.get(location);
|
||||
}
|
||||
|
||||
public static ImmutableMap<LdapName, Set<String>> parseFile(Path path, ESLogger logger) {
|
||||
if (!Files.exists(path)) {
|
||||
return ImmutableMap.of();
|
||||
|
@ -92,14 +101,6 @@ public class LdapGroupToRoleMapper extends AbstractComponent {
|
|||
}
|
||||
}
|
||||
|
||||
public static Path resolveFile(Settings settings, Environment env) {
|
||||
String location = settings.get(ROLE_MAPPING_FILE_SETTING);
|
||||
if (location == null) {
|
||||
return env.configFile().toPath().resolve(ROLE_MAPPING_DEFAULT_FILE_NAME);
|
||||
}
|
||||
return Paths.get(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will map the groupDN's to ES Roles
|
||||
*/
|
||||
|
|
|
@ -5,24 +5,42 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.ldap;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.util.Providers;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authc.AuthenticationModule;
|
||||
import org.elasticsearch.shield.authc.Realm;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
import static org.elasticsearch.common.inject.name.Names.named;
|
||||
|
||||
/**
|
||||
* Configures Ldap object injections
|
||||
*/
|
||||
public class LdapModule extends AbstractModule {
|
||||
private final Settings settings;
|
||||
public class LdapModule extends AbstractShieldModule.Node {
|
||||
|
||||
private final boolean enabled;
|
||||
|
||||
public LdapModule(Settings settings) {
|
||||
this.settings = settings;
|
||||
super(settings);
|
||||
enabled = enabled(settings);
|
||||
}
|
||||
|
||||
public static boolean enabled(Settings settings) {
|
||||
@Override
|
||||
protected void configureNode() {
|
||||
if (enabled) {
|
||||
bind(Realm.class).annotatedWith(named(LdapRealm.TYPE)).to(LdapRealm.class).asEagerSingleton();
|
||||
bind(LdapGroupToRoleMapper.class).asEagerSingleton();
|
||||
String mode = settings.getComponentSettings(LdapModule.class).get("mode", "ldap");
|
||||
if ("ldap".equals(mode)) {
|
||||
bind(LdapConnectionFactory.class).to(StandardLdapConnectionFactory.class);
|
||||
} else {
|
||||
bind(LdapConnectionFactory.class).to(ActiveDirectoryConnectionFactory.class);
|
||||
}
|
||||
} else {
|
||||
bind(LdapRealm.class).toProvider(Providers.of((LdapRealm) null));
|
||||
}
|
||||
}
|
||||
|
||||
static boolean enabled(Settings settings) {
|
||||
Settings authcSettings = settings.getAsSettings("shield.authc");
|
||||
if (!authcSettings.names().contains("ldap")) {
|
||||
return false;
|
||||
|
@ -30,16 +48,4 @@ public class LdapModule extends AbstractModule {
|
|||
Settings ldapSettings = authcSettings.getAsSettings("ldap");
|
||||
return ldapSettings.getAsBoolean("enabled", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(Realm.class).annotatedWith(named(LdapRealm.TYPE)).to(LdapRealm.class).asEagerSingleton();
|
||||
bind(LdapGroupToRoleMapper.class).asEagerSingleton();
|
||||
String mode = settings.getComponentSettings(LdapModule.class).get("mode", "ldap");
|
||||
if ("ldap".equals(mode)) {
|
||||
bind(LdapConnectionFactory.class).to(StandardLdapConnectionFactory.class);
|
||||
} else {
|
||||
bind(LdapConnectionFactory.class).to(ActiveDirectoryConnectionFactory.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,16 +7,15 @@ package org.elasticsearch.shield.authc.ldap;
|
|||
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.shield.SecurityException;
|
||||
import org.elasticsearch.shield.authc.Realm;
|
||||
import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm;
|
||||
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -25,21 +24,17 @@ import java.util.Set;
|
|||
*/
|
||||
public class LdapRealm extends CachingUsernamePasswordRealm implements Realm<UsernamePasswordToken> {
|
||||
|
||||
static {
|
||||
BaseRestHandler.addUsefulHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
|
||||
}
|
||||
|
||||
public static final String TYPE = "ldap";
|
||||
|
||||
private final LdapConnectionFactory connectionFactory;
|
||||
private final LdapGroupToRoleMapper roleMapper;
|
||||
|
||||
@Inject
|
||||
public LdapRealm(Settings settings, LdapConnectionFactory ldap, LdapGroupToRoleMapper roleMapper) {
|
||||
public LdapRealm(Settings settings, LdapConnectionFactory ldap, LdapGroupToRoleMapper roleMapper, RestController restController) {
|
||||
super(settings);
|
||||
|
||||
this.connectionFactory = ldap;
|
||||
this.roleMapper = roleMapper;
|
||||
restController.registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -66,9 +61,8 @@ public class LdapRealm extends CachingUsernamePasswordRealm implements Realm<Use
|
|||
List<String> groupDNs = session.getGroups();
|
||||
Set<String> roles = roleMapper.mapRoles(groupDNs);
|
||||
User.Simple user = new User.Simple(token.principal(), roles.toArray(new String[roles.size()]));
|
||||
Arrays.fill(token.credentials(), '\0');
|
||||
return user;
|
||||
} catch (SecurityException e){
|
||||
} catch (ShieldException e){
|
||||
logger.info("Authentication Failed for user [{}]", e, token.principal());
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.elasticsearch.common.collect.ImmutableMap;
|
|||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
|
||||
import javax.naming.Context;
|
||||
import javax.naming.NamingException;
|
||||
|
@ -43,11 +45,11 @@ public class StandardLdapConnectionFactory extends AbstractComponent implements
|
|||
super(settings);
|
||||
userDnTemplates = componentSettings.getAsArray(USER_DN_TEMPLATES_SETTING);
|
||||
if (userDnTemplates == null) {
|
||||
throw new org.elasticsearch.shield.SecurityException("Missing required ldap setting [" + USER_DN_TEMPLATES_SETTING + "]");
|
||||
throw new ShieldException("Missing required ldap setting [" + USER_DN_TEMPLATES_SETTING + "]");
|
||||
}
|
||||
String[] ldapUrls = componentSettings.getAsArray(URLS_SETTING);
|
||||
if (ldapUrls == null) {
|
||||
throw new org.elasticsearch.shield.SecurityException("Missing required ldap setting [" + URLS_SETTING + "]");
|
||||
throw new ShieldException("Missing required ldap setting [" + URLS_SETTING + "]");
|
||||
}
|
||||
sharedLdapEnv = ImmutableMap.<String, Serializable>builder()
|
||||
.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
|
||||
|
@ -67,11 +69,11 @@ public class StandardLdapConnectionFactory extends AbstractComponent implements
|
|||
* @return authenticated exception
|
||||
*/
|
||||
@Override
|
||||
public LdapConnection bind(String username, char[] password) {
|
||||
public LdapConnection bind(String username, SecuredString password) {
|
||||
//SASL, MD5, etc. all options here stink, we really need to go over ssl + simple authentication
|
||||
Hashtable<String, Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
|
||||
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
|
||||
ldapEnv.put(Context.SECURITY_CREDENTIALS, password);
|
||||
ldapEnv.put(Context.SECURITY_CREDENTIALS, password.internalChars());
|
||||
|
||||
for (String template : userDnTemplates) {
|
||||
String dn = buildDnFromTemplate(username, template);
|
||||
|
@ -86,6 +88,7 @@ public class StandardLdapConnectionFactory extends AbstractComponent implements
|
|||
logger.warn("Failed ldap authentication with user template [{}], dn [{}]", template, dn);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LdapException("Failed ldap authentication");
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ package org.elasticsearch.shield.authc.support;
|
|||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
|
@ -638,13 +637,16 @@ public class BCrypt {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hash a password using the OpenBSD bcrypt scheme
|
||||
* Hash a password using the OpenBSD bcrypt scheme.
|
||||
*
|
||||
* Modified from the original to take a SecuredString instead of the original
|
||||
*
|
||||
* @param password the password to hash
|
||||
* @param salt the salt to hash with (perhaps generated
|
||||
* using BCrypt.gensalt)
|
||||
* @return the hashed password
|
||||
*/
|
||||
public static String hashpw(String password, String salt) {
|
||||
public static String hashpw(SecuredString password, String salt) {
|
||||
BCrypt B;
|
||||
String real_salt;
|
||||
byte passwordb[], saltb[], hashed[];
|
||||
|
@ -669,12 +671,19 @@ public class BCrypt {
|
|||
rounds = Integer.parseInt(salt.substring(off, off + 2));
|
||||
|
||||
real_salt = salt.substring(off + 3, off + 25);
|
||||
try {
|
||||
|
||||
/* original code before introducing SecuredString
|
||||
try {
|
||||
passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
throw new AssertionError("UTF-8 is not supported");
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// the next line is the SecuredString replacement for the above commented-out section
|
||||
passwordb = ( minor >= 'a' ? password.concat("\000"): password ).utf8Bytes();
|
||||
|
||||
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
|
||||
|
||||
B = new BCrypt();
|
||||
|
@ -740,12 +749,14 @@ public class BCrypt {
|
|||
|
||||
/**
|
||||
* Check that a plaintext password matches a previously hashed
|
||||
* one
|
||||
* one.
|
||||
*
|
||||
* Modified from the original to take a SecuredString plaintext
|
||||
* @param plaintext the plaintext password to verify
|
||||
* @param hashed the previously-hashed password
|
||||
* @return true if the passwords match, false otherwise
|
||||
*/
|
||||
public static boolean checkpw(String plaintext, String hashed) {
|
||||
public static boolean checkpw(SecuredString plaintext, String hashed) {
|
||||
return hashed.compareTo(hashpw(plaintext, hashed)) == 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ public abstract class CachingUserPasswdStore extends AbstractComponent implement
|
|||
}
|
||||
|
||||
@Override
|
||||
public final boolean verifyPassword(final String username, final char[] password) {
|
||||
public final boolean verifyPassword(final String username, final SecuredString password) {
|
||||
if (cache == null) {
|
||||
return doVerifyPassword(username, password);
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ public abstract class CachingUserPasswdStore extends AbstractComponent implement
|
|||
* Verifies the given password. Both the given username, and if the username is verified, then the
|
||||
* given password. This method is used when the caching is disabled.
|
||||
*/
|
||||
protected abstract boolean doVerifyPassword(String username, char[] password);
|
||||
protected abstract boolean doVerifyPassword(String username, SecuredString password);
|
||||
|
||||
protected abstract PasswordHash passwordHash(String username);
|
||||
|
||||
|
@ -92,8 +92,8 @@ public abstract class CachingUserPasswdStore extends AbstractComponent implement
|
|||
}
|
||||
|
||||
@Override
|
||||
public final void store(String username, char[] key) {
|
||||
doStore(username, key);
|
||||
public final void store(String username, SecuredString password) {
|
||||
doStore(username, password);
|
||||
expire(username);
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ public abstract class CachingUserPasswdStore extends AbstractComponent implement
|
|||
expire(username);
|
||||
}
|
||||
|
||||
protected abstract void doStore(String username, char[] password);
|
||||
protected abstract void doStore(String username, SecuredString password);
|
||||
|
||||
protected abstract void doRemove(String username);
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ public abstract class CachingUserPasswdStore extends AbstractComponent implement
|
|||
*/
|
||||
static interface PasswordHash {
|
||||
|
||||
boolean verify(char[] password);
|
||||
boolean verify(SecuredString password);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,12 +116,12 @@ public abstract class CachingUsernamePasswordRealm extends AbstractComponent imp
|
|||
public static class UserWithHash {
|
||||
User user;
|
||||
char[] hash;
|
||||
public UserWithHash(User user, char[] password){
|
||||
public UserWithHash(User user, SecuredString password){
|
||||
this.user = user;
|
||||
this.hash = Hasher.HTPASSWD.hash(password);
|
||||
}
|
||||
|
||||
public boolean verify(char[] password){
|
||||
public boolean verify(SecuredString password){
|
||||
return Hasher.HTPASSWD.verify(password, hash);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.support;
|
||||
|
||||
import org.elasticsearch.common.base.Charsets;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Helper class similar to Arrays to handle conversions for Char arrays
|
||||
*/
|
||||
public class CharArrays {
|
||||
static char[] utf8BytesToChars(byte[] utf8Bytes) {
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(utf8Bytes);
|
||||
CharBuffer charBuffer = Charsets.UTF_8.decode(byteBuffer);
|
||||
char[] chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit());
|
||||
byteBuffer.clear();
|
||||
charBuffer.clear();
|
||||
return chars;
|
||||
}
|
||||
/**
|
||||
* Like String.indexOf for for an array of chars
|
||||
*/
|
||||
static int indexOf(char[] array, char ch){
|
||||
for (int i = 0; (i < array.length); i++) {
|
||||
if (array[i] == ch) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static byte[] toUtf8Bytes(char[] chars) {
|
||||
CharBuffer charBuffer = CharBuffer.wrap(chars);
|
||||
ByteBuffer byteBuffer = Charsets.UTF_8.encode(charBuffer);
|
||||
byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
|
||||
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
|
||||
return bytes;
|
||||
}
|
||||
}
|
|
@ -10,12 +10,8 @@ import org.apache.commons.codec.digest.Crypt;
|
|||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.codec.digest.Md5Crypt;
|
||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||
import org.elasticsearch.common.base.Charsets;
|
||||
import org.elasticsearch.common.os.OsUtils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
|
@ -30,25 +26,25 @@ public enum Hasher {
|
|||
HTPASSWD() {
|
||||
|
||||
@Override
|
||||
public char[] hash(char[] text) {
|
||||
public char[] hash(SecuredString text) {
|
||||
String salt = org.elasticsearch.shield.authc.support.BCrypt.gensalt();
|
||||
return BCrypt.hashpw(new String(text), salt).toCharArray();
|
||||
return BCrypt.hashpw(text, salt).toCharArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(char[] text, char[] hash) {
|
||||
public boolean verify(SecuredString text, char[] hash) {
|
||||
String hashStr = new String(hash);
|
||||
if (hashStr.startsWith(BCRYPT_PREFIX_Y)) {
|
||||
hashStr = BCRYPT_PREFIX + hashStr.substring(BCRYPT_PREFIX_Y.length());
|
||||
}
|
||||
if (hashStr.startsWith(BCRYPT_PREFIX)) {
|
||||
return BCrypt.checkpw(new String(text), hashStr);
|
||||
return BCrypt.checkpw(text, hashStr);
|
||||
}
|
||||
if (hashStr.startsWith(PLAIN_PREFIX)) {
|
||||
hashStr = hashStr.substring(PLAIN_PREFIX.length());
|
||||
return hashStr.compareTo(new String(text)) == 0;
|
||||
return text.equals(hashStr);
|
||||
}
|
||||
byte[] textBytes = toBytes(text);
|
||||
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
||||
if (hashStr.startsWith(APR1_PREFIX)) {
|
||||
return hashStr.compareTo(Md5Crypt.apr1Crypt(textBytes, hashStr)) == 0;
|
||||
}
|
||||
|
@ -57,8 +53,8 @@ public enum Hasher {
|
|||
return hashStr.substring(SHA1_PREFIX.length()).compareTo(passwd64) == 0;
|
||||
}
|
||||
return CRYPT_SUPPORTED ?
|
||||
hashStr.compareTo(Crypt.crypt(textBytes, hashStr)) == 0: // crypt algo
|
||||
hashStr.compareTo(new String(text)) == 0; // plain text
|
||||
hashStr.compareTo(Crypt.crypt(textBytes, hashStr)) == 0 : // crypt algo
|
||||
text.equals(hashStr); // plain text
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -88,16 +84,8 @@ public enum Hasher {
|
|||
return hasher;
|
||||
}
|
||||
|
||||
public abstract char[] hash(char[] data);
|
||||
public abstract char[] hash(SecuredString data);
|
||||
|
||||
public abstract boolean verify(char[] data, char[] hash);
|
||||
|
||||
private static byte[] toBytes(char[] chars) {
|
||||
CharBuffer charBuffer = CharBuffer.wrap(chars);
|
||||
ByteBuffer byteBuffer = Charsets.UTF_8.encode(charBuffer);
|
||||
byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
|
||||
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
|
||||
return bytes;
|
||||
}
|
||||
public abstract boolean verify(SecuredString data, char[] hash);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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.support;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* This is not a string but a CharSequence that can be cleared of its memory. Important for handling passwords.
|
||||
*
|
||||
* @NotThreadSafe There is a chance that the chars could be cleared while doing operations on the chars.
|
||||
*
|
||||
* TODO: dot net's SecureString implementation does some obfuscation of the password to prevent gleaming passwords
|
||||
* from memory dumps. (this is hard as dot net uses windows system crypto. Thats probably the reason java still doesn't have it)
|
||||
*/
|
||||
public class SecuredString implements CharSequence {
|
||||
private final char[] chars;
|
||||
private boolean cleared = false;
|
||||
|
||||
/**
|
||||
* Note: the passed in chars are not duplicated, but used directly for performance/optimization. DO NOT
|
||||
* modify or clear the chars after it has been passed into this constructor.
|
||||
* @param chars
|
||||
*/
|
||||
public SecuredString(char[] chars){
|
||||
this.chars = new char[chars.length];
|
||||
System.arraycopy(chars, 0, this.chars, 0, chars.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* This constructor is used internally for the concatenate method. It DOES duplicate the passed in array, unlike
|
||||
* the public constructor
|
||||
*/
|
||||
private SecuredString(char[] chars, int start, int end){
|
||||
this.chars = new char[end - start];
|
||||
System.arraycopy(chars, start, this.chars, 0, this.chars.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null) return false;
|
||||
|
||||
if (o instanceof SecuredString) {
|
||||
SecuredString that = (SecuredString) o;
|
||||
|
||||
if (cleared != that.cleared) return false;
|
||||
if (!Arrays.equals(chars, that.chars)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (o instanceof CharSequence) {
|
||||
CharSequence that = (CharSequence) o;
|
||||
if (cleared) return false;
|
||||
if (chars.length != that.length()) return false;
|
||||
|
||||
for(int i=0; i < chars.length; i++){
|
||||
if (chars[i] != that.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(chars);
|
||||
result = 31 * result + (cleared ? 1 : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: This is a dangerous call that exists for performance/optimization
|
||||
* DO NOT modify the array returned by this method. To clear the array call SecureString.clear().
|
||||
* @return the internal characters that MUST NOT be cleared manually
|
||||
*/
|
||||
public char[] internalChars(){
|
||||
throwIfCleared();
|
||||
return chars;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return utf8 encoded bytes
|
||||
*/
|
||||
public byte[] utf8Bytes(){
|
||||
throwIfCleared();
|
||||
return CharArrays.toUtf8Bytes(chars);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
throwIfCleared();
|
||||
return chars.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
throwIfCleared();
|
||||
return chars[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecuredString subSequence(int start, int end) {
|
||||
throwIfCleared();
|
||||
return new SecuredString(this.chars, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually clear the underlying array holding the characters
|
||||
*/
|
||||
public void clear(){
|
||||
cleared = true;
|
||||
Arrays.fill(chars, (char) 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finalize() throws Throwable{
|
||||
clear();
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
public int indexOf(char toFind) {
|
||||
for(int i=0; i<chars.length; i++){
|
||||
if (chars[i] == toFind) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param toAppend String to combine with this SecureString
|
||||
* @return a new SecureString with toAppend concatenated
|
||||
*/
|
||||
public SecuredString concat(CharSequence toAppend ) {
|
||||
throwIfCleared();
|
||||
|
||||
CharBuffer buffer = CharBuffer.allocate(chars.length+toAppend.length());
|
||||
buffer.put(chars);
|
||||
for(int i = 0; i < toAppend.length(); i++){
|
||||
buffer.put(i+chars.length, toAppend.charAt(i));
|
||||
}
|
||||
return new SecuredString(buffer.array());
|
||||
}
|
||||
|
||||
private void throwIfCleared() {
|
||||
if (cleared) {
|
||||
throw new ElasticsearchException("Attempt to use cleared password");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,11 +10,11 @@ package org.elasticsearch.shield.authc.support;
|
|||
*/
|
||||
public interface UserPasswdStore {
|
||||
|
||||
boolean verifyPassword(String username, char[] password);
|
||||
boolean verifyPassword(String username, SecuredString password);
|
||||
|
||||
static interface Writable extends UserPasswdStore {
|
||||
|
||||
void store(String username, char[] password);
|
||||
void store(String username, SecuredString password);
|
||||
|
||||
void remove(String username);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.elasticsearch.shield.authc.AuthenticationToken;
|
|||
import org.elasticsearch.transport.TransportMessage;
|
||||
import org.elasticsearch.transport.TransportRequest;
|
||||
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
|
@ -27,9 +28,9 @@ public class UsernamePasswordToken implements AuthenticationToken {
|
|||
private static final Pattern BASIC_AUTH_PATTERN = Pattern.compile("Basic\\s(.+)");
|
||||
|
||||
private final String username;
|
||||
private final char[] password;
|
||||
private final SecuredString password;
|
||||
|
||||
public UsernamePasswordToken(String username, char[] password) {
|
||||
public UsernamePasswordToken(String username, SecuredString password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
@ -40,10 +41,15 @@ public class UsernamePasswordToken implements AuthenticationToken {
|
|||
}
|
||||
|
||||
@Override
|
||||
public char[] credentials() {
|
||||
public SecuredString credentials() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCredentials() {
|
||||
password.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -51,15 +57,17 @@ public class UsernamePasswordToken implements AuthenticationToken {
|
|||
|
||||
UsernamePasswordToken that = (UsernamePasswordToken) o;
|
||||
|
||||
return Arrays.equals(password, that.password) &&
|
||||
return password.equals(password) &&
|
||||
Objects.equals(username, that.username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(username, Arrays.hashCode(password));
|
||||
return Objects.hash(username, password.hashCode());
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static UsernamePasswordToken extractToken(TransportMessage<?> message, UsernamePasswordToken defaultToken) {
|
||||
String authStr = message.getHeader(BASIC_AUTH_HEADER);
|
||||
if (authStr == null) {
|
||||
|
@ -71,12 +79,15 @@ public class UsernamePasswordToken implements AuthenticationToken {
|
|||
throw new AuthenticationException("Invalid basic authentication header value");
|
||||
}
|
||||
|
||||
String userpasswd = new String(Base64.decodeBase64(matcher.group(1)), Charsets.UTF_8);
|
||||
int i = userpasswd.indexOf(':');
|
||||
char[] userpasswd = CharArrays.utf8BytesToChars(Base64.decodeBase64(matcher.group(1)));
|
||||
int i = CharArrays.indexOf(userpasswd, ':');
|
||||
if (i < 0) {
|
||||
throw new AuthenticationException("Invalid basic authentication header value");
|
||||
}
|
||||
return new UsernamePasswordToken(userpasswd.substring(0, i), userpasswd.substring(i+1).toCharArray());
|
||||
|
||||
return new UsernamePasswordToken(
|
||||
new String(Arrays.copyOfRange(userpasswd, 0, i)),
|
||||
new SecuredString(Arrays.copyOfRange(userpasswd, i + 1, userpasswd.length)));
|
||||
}
|
||||
|
||||
public static UsernamePasswordToken extractToken(RestRequest request, UsernamePasswordToken defaultToken) {
|
||||
|
@ -90,21 +101,27 @@ public class UsernamePasswordToken implements AuthenticationToken {
|
|||
throw new AuthenticationException("Invalid basic authentication header value");
|
||||
}
|
||||
|
||||
String userpasswd = new String(Base64.decodeBase64(matcher.group(1)), Charsets.UTF_8);
|
||||
int i = userpasswd.indexOf(':');
|
||||
char[] userpasswd = CharArrays.utf8BytesToChars(Base64.decodeBase64(matcher.group(1)));
|
||||
int i = CharArrays.indexOf(userpasswd, ':');
|
||||
if (i < 0) {
|
||||
throw new AuthenticationException("Invalid basic authentication header value");
|
||||
}
|
||||
return new UsernamePasswordToken(userpasswd.substring(0, i), userpasswd.substring(i+1).toCharArray());
|
||||
|
||||
return new UsernamePasswordToken(
|
||||
new String(Arrays.copyOfRange(userpasswd, 0, i)),
|
||||
new SecuredString(Arrays.copyOfRange(userpasswd, i + 1, userpasswd.length)));
|
||||
}
|
||||
|
||||
public static void putTokenHeader(TransportRequest request, UsernamePasswordToken token) {
|
||||
request.putHeader("Authorization", basicAuthHeaderValue(token.username, token.password));
|
||||
}
|
||||
|
||||
public static String basicAuthHeaderValue(String username, char[] passwd) {
|
||||
String basicToken = username + ":" + new String(passwd);
|
||||
basicToken = new String(Base64.encodeBase64(basicToken.getBytes(Charsets.UTF_8)), Charsets.UTF_8);
|
||||
public static String basicAuthHeaderValue(String username, SecuredString passwd) {
|
||||
CharBuffer chars = CharBuffer.allocate(username.length() + passwd.length() + 1);
|
||||
chars.put(username).put(':').put(passwd.internalChars());
|
||||
|
||||
//TODO we still have passwords in Strings in headers
|
||||
String basicToken = new String(Base64.encodeBase64(CharArrays.toUtf8Bytes(chars.array())), Charsets.UTF_8);
|
||||
return "Basic " + basicToken;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.system;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
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.support.AbstractShieldModule;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
|
||||
/**
|
||||
|
@ -27,6 +28,11 @@ public class SystemRealm implements Realm<AuthenticationToken> {
|
|||
public Object credentials() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCredentials() {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
|
@ -58,9 +64,14 @@ public class SystemRealm implements Realm<AuthenticationToken> {
|
|||
return token == TOKEN ? User.SYSTEM : null;
|
||||
}
|
||||
|
||||
public static class Module extends AbstractModule {
|
||||
public static class Module extends AbstractShieldModule.Node {
|
||||
|
||||
public Module(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
protected void configureNode() {
|
||||
bind(SystemRealm.class).asEagerSingleton();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
package org.elasticsearch.shield.authz;
|
||||
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class AuthorizationException extends org.elasticsearch.shield.SecurityException {
|
||||
public class AuthorizationException extends ShieldException {
|
||||
|
||||
public AuthorizationException(String msg) {
|
||||
super(msg);
|
||||
|
|
|
@ -5,17 +5,22 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authz;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authz.store.FileRolesStore;
|
||||
import org.elasticsearch.shield.authz.store.RolesStore;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class AuthorizationModule extends AbstractModule {
|
||||
public class AuthorizationModule extends AbstractShieldModule.Node {
|
||||
|
||||
public AuthorizationModule(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
protected void configureNode() {
|
||||
bind(RolesStore.class).to(FileRolesStore.class);
|
||||
bind(AuthorizationService.class).to(InternalAuthorizationService.class).asEagerSingleton();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authz;
|
||||
|
||||
import org.elasticsearch.action.IndicesRequest;
|
||||
import org.elasticsearch.cluster.metadata.MetaData;
|
||||
import org.elasticsearch.common.base.Predicate;
|
||||
import org.elasticsearch.common.collect.ImmutableList;
|
||||
|
@ -187,11 +188,22 @@ public interface Permission {
|
|||
|
||||
@Override @SuppressWarnings("unchecked")
|
||||
public boolean check(String action, TransportRequest request, MetaData metaData) {
|
||||
Set<String> indices = Collections.emptySet();
|
||||
for (IndicesResolver resolver : indicesResolvers) {
|
||||
if (resolver.requestType().isInstance(request)) {
|
||||
indices = resolver.resolve(request, metaData);
|
||||
break;
|
||||
|
||||
// some APIs are indices requests that are not actually associated with indices. For example,
|
||||
// search scroll request, is categorized under the indices context, but doesn't hold indices names
|
||||
// (in this case, the security check on the indices was done on the search request that initialized
|
||||
// the scroll... and we rely on the signed scroll id to provide security over this request).
|
||||
//
|
||||
// so we only check indices if indeed the request is an actual IndicesRequest, if it's not, we only
|
||||
// perform the check on the action name.
|
||||
Set<String> indices = null;
|
||||
if (request instanceof IndicesRequest) {
|
||||
indices = Collections.emptySet();
|
||||
for (IndicesResolver resolver : indicesResolvers) {
|
||||
if (resolver.requestType().isInstance(request)) {
|
||||
indices = resolver.resolve(request, metaData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,9 +244,11 @@ public interface Permission {
|
|||
return false;
|
||||
}
|
||||
|
||||
for (String index : indices) {
|
||||
if (!indexNameMatcher.apply(index)) {
|
||||
return false;
|
||||
if (indices != null) {
|
||||
for (String index : indices) {
|
||||
if (!indexNameMatcher.apply(index)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ public class DefaultIndicesResolver implements IndicesResolver<TransportRequest>
|
|||
public Set<String> resolve(TransportRequest request, MetaData metaData) {
|
||||
|
||||
boolean isIndicesRequest = request instanceof CompositeIndicesRequest || request instanceof IndicesRequest;
|
||||
assert isIndicesRequest : "the only requests passing the action matcher should be IndicesRequests";
|
||||
assert isIndicesRequest : "Request [" + request + "] is not an Indices request. The only requests passing the action matcher should be IndicesRequests";
|
||||
|
||||
// if for some reason we are missing an action... just for safety we'll reject
|
||||
if (!isIndicesRequest) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.elasticsearch.common.collect.ImmutableSet;
|
|||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.jackson.dataformat.yaml.snakeyaml.error.YAMLException;
|
||||
import org.elasticsearch.common.logging.ESLogger;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||
|
@ -20,6 +21,7 @@ import org.elasticsearch.common.xcontent.yaml.YamlXContent;
|
|||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.authz.Permission;
|
||||
import org.elasticsearch.shield.authz.Privilege;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
import org.elasticsearch.watcher.FileWatcher;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
@ -57,7 +59,7 @@ public class FileRolesStore extends AbstractComponent implements RolesStore {
|
|||
public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Listener listener) {
|
||||
super(settings);
|
||||
file = resolveFile(componentSettings, env);
|
||||
permissions = parseFile(file);
|
||||
permissions = parseFile(file, logger);
|
||||
FileWatcher watcher = new FileWatcher(file.getParent().toFile());
|
||||
watcher.addListener(new FileListener());
|
||||
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
|
||||
|
@ -72,12 +74,17 @@ public class FileRolesStore extends AbstractComponent implements RolesStore {
|
|||
public static Path resolveFile(Settings settings, Environment env) {
|
||||
String location = settings.get("files.roles");
|
||||
if (location == null) {
|
||||
return env.configFile().toPath().resolve(".roles.yml");
|
||||
return ShieldPlugin.resolveConfigFile(env, ".roles.yml");
|
||||
}
|
||||
|
||||
return Paths.get(location);
|
||||
}
|
||||
|
||||
public static ImmutableMap<String, Permission.Global> parseFile(Path path) {
|
||||
public static ImmutableMap<String, Permission.Global> parseFile(Path path, ESLogger logger) {
|
||||
if (logger != null) {
|
||||
logger.trace("Reading roles file located at [{}]", path);
|
||||
}
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
@ -218,7 +225,7 @@ public class FileRolesStore extends AbstractComponent implements RolesStore {
|
|||
@Override
|
||||
public void onFileChanged(File file) {
|
||||
if (file.equals(FileRolesStore.this.file.toFile())) {
|
||||
permissions = parseFile(file.toPath());
|
||||
permissions = parseFile(file.toPath(), logger);
|
||||
listener.onRefresh();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.base.Charsets;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
import org.elasticsearch.watcher.FileWatcher;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class InternalKeyService extends AbstractComponent implements KeyService {
|
||||
|
||||
public static final String FILE_SETTING = "shield.system_key.file";
|
||||
public static final String KEY_ALGO = "HmacSHA512";
|
||||
public static final int KEY_SIZE = 1024;
|
||||
|
||||
static final String FILE_NAME = ".system_key";
|
||||
static final String HMAC_ALGO = "HmacSHA1";
|
||||
|
||||
private static final Pattern SIG_PATTERN = Pattern.compile("\\$\\$[0-9]+\\$\\$.+");
|
||||
|
||||
private final Path keyFile;
|
||||
|
||||
private volatile SecretKey key;
|
||||
|
||||
@Inject
|
||||
public InternalKeyService(Settings settings, Environment env, ResourceWatcherService watcherService) {
|
||||
this(settings, env, watcherService, Listener.NOOP);
|
||||
}
|
||||
|
||||
InternalKeyService(Settings settings, Environment env, ResourceWatcherService watcherService, Listener listener) {
|
||||
super(settings);
|
||||
keyFile = resolveFile(settings, env);
|
||||
key = readKey(keyFile);
|
||||
FileWatcher watcher = new FileWatcher(keyFile.getParent().toFile());
|
||||
watcher.addListener(new FileListener(listener));
|
||||
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
|
||||
}
|
||||
|
||||
public static byte[] generateKey() throws Exception {
|
||||
KeyGenerator generator = KeyGenerator.getInstance(KEY_ALGO);
|
||||
generator.init(KEY_SIZE);
|
||||
return generator.generateKey().getEncoded();
|
||||
}
|
||||
|
||||
public static Path resolveFile(Settings settings, Environment env) {
|
||||
String location = settings.get(FILE_SETTING);
|
||||
if (location == null) {
|
||||
return ShieldPlugin.resolveConfigFile(env, FILE_NAME);
|
||||
}
|
||||
return Paths.get(location);
|
||||
}
|
||||
|
||||
static SecretKey readKey(Path file) {
|
||||
if (!Files.exists(file)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
byte[] bytes = Files.readAllBytes(file);
|
||||
return new SecretKeySpec(bytes, KEY_ALGO);
|
||||
} catch (IOException e) {
|
||||
throw new ShieldException("Could not read secret key", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sign(String text) {
|
||||
SecretKey key = this.key;
|
||||
if (key == null) {
|
||||
return text;
|
||||
}
|
||||
String sigStr = signInternal(text);
|
||||
return "$$" + sigStr.length() + "$$" + sigStr + text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String unsignAndVerify(String signedText) {
|
||||
SecretKey key = this.key;
|
||||
if (key == null) {
|
||||
return signedText;
|
||||
}
|
||||
|
||||
if (!signedText.startsWith("$$")) {
|
||||
throw new SignatureException("tampered signed text");
|
||||
}
|
||||
|
||||
try {
|
||||
// $$34$$sigtext
|
||||
int i = signedText.indexOf("$$", 2);
|
||||
int length = Integer.parseInt(signedText.substring(2, i));
|
||||
String sigStr = signedText.substring(i + 2, i + 2 + length);
|
||||
String text = signedText.substring(i + 2 + length);
|
||||
String sig = signInternal(text);
|
||||
if (!sig.equals(sigStr)) {
|
||||
throw new SignatureException("the signed texts don't match");
|
||||
}
|
||||
return text;
|
||||
} catch (SignatureException e) {
|
||||
throw e;
|
||||
} catch (Throwable t) {
|
||||
throw new SignatureException("error while verifying the signed text", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean signed(String text) {
|
||||
return SIG_PATTERN.matcher(text).matches();
|
||||
}
|
||||
|
||||
static Mac createMac(SecretKey key) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGO);
|
||||
mac.init(key);
|
||||
return mac;
|
||||
} catch (Exception e) {
|
||||
throw new ElasticsearchException("Could not initialize mac", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String signInternal(String text) {
|
||||
Mac mac = createMac(key);
|
||||
byte[] sig = mac.doFinal(text.getBytes(Charsets.UTF_8));
|
||||
return Base64.encodeBase64URLSafeString(sig);
|
||||
}
|
||||
|
||||
private class FileListener extends FileChangesListener {
|
||||
|
||||
private final Listener listener;
|
||||
|
||||
private FileListener(Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileCreated(File file) {
|
||||
if (file.equals(keyFile.toFile())) {
|
||||
key = readKey(file.toPath());
|
||||
listener.onKeyRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileDeleted(File file) {
|
||||
if (file.equals(keyFile.toFile())) {
|
||||
logger.error("System key file was removed! As long as the system key file is missing, elasticsearch won't function as expected for some requests (e.g. scroll/scan)");
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileChanged(File file) {
|
||||
if (file.equals(keyFile.toFile())) {
|
||||
key = readKey(file.toPath());
|
||||
listener.onKeyRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static interface Listener {
|
||||
|
||||
final Listener NOOP = new Listener() {
|
||||
@Override
|
||||
public void onKeyRefresh() {
|
||||
}
|
||||
};
|
||||
|
||||
void onKeyRefresh();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class KeyModule extends AbstractShieldModule.Node {
|
||||
|
||||
public KeyModule(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureNode() {
|
||||
bind(KeyService.class).to(InternalKeyService.class).asEagerSingleton();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface KeyService {
|
||||
|
||||
/**
|
||||
* Signs the given text and returns the signed text (original text + signature)
|
||||
*/
|
||||
String sign(String text);
|
||||
|
||||
/**
|
||||
* Unsigns the given signed text, verifies the original text with the attached signature and if valid returns
|
||||
* the unsigned (original) text. If signature verification fails a {@link SignatureException} is thrown.
|
||||
*/
|
||||
String unsignAndVerify(String text);
|
||||
|
||||
/**
|
||||
* Checks whether the given text is signed.
|
||||
*/
|
||||
boolean signed(String text);
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
import org.elasticsearch.shield.authz.AuthorizationException;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SignatureException extends AuthorizationException {
|
||||
|
||||
public SignatureException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public SignatureException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.key.tool;
|
||||
|
||||
import org.elasticsearch.common.cli.CliTool;
|
||||
import org.elasticsearch.common.cli.CliToolConfig;
|
||||
import org.elasticsearch.common.cli.Terminal;
|
||||
import org.elasticsearch.common.cli.commons.CommandLine;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.key.InternalKeyService;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd;
|
||||
import static org.elasticsearch.common.cli.CliToolConfig.config;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SystemKeyTool extends CliTool {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new SystemKeyTool().execute(args);
|
||||
}
|
||||
|
||||
private static final CliToolConfig CONFIG = config("syskey", SystemKeyTool.class)
|
||||
.cmds(Generate.CMD)
|
||||
.build();
|
||||
|
||||
public SystemKeyTool() {
|
||||
super(CONFIG);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Command parse(String cmd, CommandLine commandLine) throws Exception {
|
||||
return Generate.parse(terminal, commandLine);
|
||||
}
|
||||
|
||||
static class Generate extends Command {
|
||||
private static final String NAME = "generate";
|
||||
private static final CliToolConfig.Cmd CMD = cmd(NAME, Generate.class).build();
|
||||
|
||||
final Path path;
|
||||
|
||||
Generate(Terminal terminal, Path path) {
|
||||
super(terminal);
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public static Command parse(Terminal terminal, CommandLine cl) {
|
||||
String[] args = cl.getArgs();
|
||||
if (args.length > 1) {
|
||||
return exitCmd(ExitStatus.USAGE, terminal, "Too many arguments");
|
||||
}
|
||||
Path path = args.length != 0 ? Paths.get(args[0]) : null;
|
||||
return new Generate(terminal, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExitStatus execute(Settings settings, Environment env) throws Exception {
|
||||
Path path = this.path;
|
||||
if (path == null) {
|
||||
path = InternalKeyService.resolveFile(settings, env);
|
||||
}
|
||||
terminal.println(Terminal.Verbosity.VERBOSE, "generating...");
|
||||
byte[] key = InternalKeyService.generateKey();
|
||||
terminal.println("Storing generated key in [%s]", path.toAbsolutePath());
|
||||
Files.write(path, key, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
return ExitStatus.OK;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -7,15 +7,18 @@ package org.elasticsearch.shield.plugin;
|
|||
|
||||
import org.elasticsearch.common.collect.ImmutableList;
|
||||
import org.elasticsearch.common.inject.Module;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.plugins.AbstractPlugin;
|
||||
import org.elasticsearch.shield.SecurityModule;
|
||||
import org.elasticsearch.shield.ShieldModule;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SecurityPlugin extends AbstractPlugin {
|
||||
public class ShieldPlugin extends AbstractPlugin {
|
||||
|
||||
public static final String NAME = "shield";
|
||||
|
||||
|
@ -31,7 +34,15 @@ public class SecurityPlugin extends AbstractPlugin {
|
|||
|
||||
@Override
|
||||
public Collection<Class<? extends Module>> modules() {
|
||||
return ImmutableList.<Class<? extends Module>>of(SecurityModule.class);
|
||||
return ImmutableList.<Class<? extends Module>>of(ShieldModule.class);
|
||||
}
|
||||
|
||||
public static Path configDir(Environment env) {
|
||||
return new File(env.configFile(), NAME).toPath();
|
||||
}
|
||||
|
||||
public static Path resolveConfigFile(Environment env, String name) {
|
||||
return configDir(env).resolve(name);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.support;
|
||||
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.Module;
|
||||
import org.elasticsearch.common.inject.SpawnModules;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractShieldModule extends AbstractModule {
|
||||
|
||||
protected final Settings settings;
|
||||
protected final boolean clientMode;
|
||||
|
||||
public AbstractShieldModule(Settings settings) {
|
||||
this.settings = settings;
|
||||
this.clientMode = !"node".equals(settings.get(Client.CLIENT_TYPE_SETTING));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void configure() {
|
||||
configure(clientMode);
|
||||
}
|
||||
|
||||
protected abstract void configure(boolean clientMode);
|
||||
|
||||
public static abstract class Spawn extends AbstractShieldModule implements SpawnModules {
|
||||
|
||||
protected Spawn(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Iterable<? extends Module> spawnModules() {
|
||||
return spawnModules(clientMode);
|
||||
}
|
||||
|
||||
public abstract Iterable<? extends Module> spawnModules(boolean clientMode);
|
||||
}
|
||||
|
||||
public static abstract class Node extends AbstractShieldModule {
|
||||
|
||||
protected Node(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void configure(boolean clientMode) {
|
||||
assert !clientMode : "[" + getClass().getSimpleName() + "] is a node only module";
|
||||
configureNode();
|
||||
}
|
||||
|
||||
protected abstract void configureNode();
|
||||
|
||||
public static abstract class Spawn extends Node implements SpawnModules {
|
||||
|
||||
protected Spawn(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
public abstract Iterable<? extends AbstractShieldModule.Node> spawnModules();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -5,16 +5,21 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.transport;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.SecurityFilter;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SecuredRestModule extends AbstractModule {
|
||||
public class SecuredRestModule extends AbstractShieldModule.Node {
|
||||
|
||||
public SecuredRestModule(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
protected void configureNode() {
|
||||
bind(SecurityFilter.Rest.class).asEagerSingleton();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
package org.elasticsearch.shield.transport;
|
||||
|
||||
import org.elasticsearch.common.collect.ImmutableList;
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.Module;
|
||||
import org.elasticsearch.common.inject.PreProcessModule;
|
||||
import org.elasticsearch.common.inject.SpawnModules;
|
||||
import org.elasticsearch.common.inject.util.Providers;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.SecurityFilter;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
import org.elasticsearch.shield.transport.n2n.IPFilteringN2NAuthenticator;
|
||||
import org.elasticsearch.shield.transport.netty.N2NNettyUpstreamHandler;
|
||||
import org.elasticsearch.shield.transport.netty.NettySecuredHttpServerTransportModule;
|
||||
|
@ -21,28 +22,46 @@ import org.elasticsearch.transport.TransportModule;
|
|||
/**
|
||||
*
|
||||
*/
|
||||
public class SecuredTransportModule extends AbstractModule implements SpawnModules, PreProcessModule {
|
||||
public class SecuredTransportModule extends AbstractShieldModule.Spawn implements PreProcessModule {
|
||||
|
||||
public SecuredTransportModule(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends Module> spawnModules() {
|
||||
public Iterable<? extends Module> spawnModules(boolean clientMode) {
|
||||
|
||||
if (clientMode) {
|
||||
return ImmutableList.of(new NettySecuredTransportModule(settings));
|
||||
}
|
||||
|
||||
//todo we only need to spawn http module if we're not within the transport client context
|
||||
return ImmutableList.of(
|
||||
new NettySecuredHttpServerTransportModule(),
|
||||
new NettySecuredTransportModule());
|
||||
new NettySecuredHttpServerTransportModule(settings),
|
||||
new NettySecuredTransportModule(settings));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processModule(Module module) {
|
||||
if (module instanceof TransportModule) {
|
||||
((TransportModule) module).setTransportService(SecuredTransportService.class, SecurityPlugin.NAME);
|
||||
((TransportModule) module).setTransportService(SecuredTransportService.class, ShieldPlugin.NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
protected void configure(boolean clientMode) {
|
||||
|
||||
if (clientMode) {
|
||||
// no ip filtering on the client
|
||||
bind(N2NNettyUpstreamHandler.class).toProvider(Providers.<N2NNettyUpstreamHandler>of(null));
|
||||
return;
|
||||
}
|
||||
|
||||
bind(TransportFilter.class).to(SecurityFilter.Transport.class).asEagerSingleton();
|
||||
bind(IPFilteringN2NAuthenticator.class).asEagerSingleton();
|
||||
bind(N2NNettyUpstreamHandler.class).asEagerSingleton();
|
||||
if (settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true)) {
|
||||
bind(IPFilteringN2NAuthenticator.class).asEagerSingleton();
|
||||
bind(N2NNettyUpstreamHandler.class).asEagerSingleton();
|
||||
} else {
|
||||
bind(N2NNettyUpstreamHandler.class).toProvider(Providers.<N2NNettyUpstreamHandler>of(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import org.elasticsearch.common.settings.Settings;
|
|||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.yaml.YamlXContent;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.watcher.FileChangesListener;
|
||||
import org.elasticsearch.watcher.FileWatcher;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
@ -60,8 +60,7 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2
|
|||
private Path resolveFile(Settings settings, Environment env) {
|
||||
String location = settings.get("ip_filter.file");
|
||||
if (location == null) {
|
||||
File shieldDirectory = new File(env.configFile(), SecurityPlugin.NAME);
|
||||
return shieldDirectory.toPath().resolve(DEFAULT_FILE);
|
||||
return ShieldPlugin.resolveConfigFile(env, DEFAULT_FILE);
|
||||
}
|
||||
return Paths.get(location);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.elasticsearch.shield.transport.netty;
|
||||
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.inject.internal.Nullable;
|
||||
import org.elasticsearch.common.netty.channel.ChannelPipeline;
|
||||
import org.elasticsearch.common.netty.channel.ChannelPipelineFactory;
|
||||
import org.elasticsearch.common.netty.handler.ssl.SslHandler;
|
||||
|
@ -27,7 +28,7 @@ public class NettySecuredHttpServerTransport extends NettyHttpServerTransport {
|
|||
|
||||
@Inject
|
||||
public NettySecuredHttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays,
|
||||
N2NNettyUpstreamHandler shieldUpstreamHandler) {
|
||||
@Nullable N2NNettyUpstreamHandler shieldUpstreamHandler) {
|
||||
super(settings, networkService, bigArrays);
|
||||
this.ssl = settings.getAsBoolean("shield.http.ssl", false);
|
||||
this.shieldUpstreamHandler = shieldUpstreamHandler;
|
||||
|
@ -56,14 +57,14 @@ public class NettySecuredHttpServerTransport extends NettyHttpServerTransport {
|
|||
@Override
|
||||
public ChannelPipeline getPipeline() throws Exception {
|
||||
ChannelPipeline pipeline = super.getPipeline();
|
||||
if (settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true)) {
|
||||
pipeline.addFirst("ipfilter", shieldUpstreamHandler);
|
||||
}
|
||||
if (ssl) {
|
||||
SSLEngine engine = sslConfig.createSSLEngine();
|
||||
engine.setUseClientMode(false);
|
||||
pipeline.addFirst("ssl", new SslHandler(engine));
|
||||
}
|
||||
if (shieldUpstreamHandler != null) {
|
||||
pipeline.addFirst("ipfilter", shieldUpstreamHandler);
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,24 +5,30 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.transport.netty;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.Module;
|
||||
import org.elasticsearch.common.inject.PreProcessModule;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.http.HttpServerModule;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class NettySecuredHttpServerTransportModule extends AbstractModule implements PreProcessModule {
|
||||
public class NettySecuredHttpServerTransportModule extends AbstractShieldModule implements PreProcessModule {
|
||||
|
||||
public NettySecuredHttpServerTransportModule(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processModule(Module module) {
|
||||
if (module instanceof HttpServerModule) {
|
||||
((HttpServerModule) module).setHttpServerTransport(NettySecuredHttpServerTransport.class, SecurityPlugin.NAME);
|
||||
((HttpServerModule) module).setHttpServerTransport(NettySecuredHttpServerTransport.class, ShieldPlugin.NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {}
|
||||
protected void configure(boolean clientMode) {
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ package org.elasticsearch.shield.transport.netty;
|
|||
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.inject.internal.Nullable;
|
||||
import org.elasticsearch.common.netty.channel.ChannelPipeline;
|
||||
import org.elasticsearch.common.netty.channel.ChannelPipelineFactory;
|
||||
import org.elasticsearch.common.netty.handler.ssl.SslHandler;
|
||||
|
@ -29,7 +30,7 @@ public class NettySecuredTransport extends NettyTransport {
|
|||
|
||||
@Inject
|
||||
public NettySecuredTransport(Settings settings, ThreadPool threadPool, NetworkService networkService, BigArrays bigArrays, Version version,
|
||||
N2NNettyUpstreamHandler shieldUpstreamHandler) {
|
||||
@Nullable N2NNettyUpstreamHandler shieldUpstreamHandler) {
|
||||
super(settings, threadPool, networkService, bigArrays, version);
|
||||
this.shieldUpstreamHandler = shieldUpstreamHandler;
|
||||
this.ssl = settings.getAsBoolean("shield.transport.ssl", false);
|
||||
|
@ -63,9 +64,6 @@ public class NettySecuredTransport extends NettyTransport {
|
|||
@Override
|
||||
public ChannelPipeline getPipeline() throws Exception {
|
||||
ChannelPipeline pipeline = super.getPipeline();
|
||||
if (settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true)) {
|
||||
pipeline.addFirst("ipfilter", shieldUpstreamHandler);
|
||||
}
|
||||
if (ssl) {
|
||||
SSLEngine serverEngine = sslConfig.createSSLEngine();
|
||||
serverEngine.setUseClientMode(false);
|
||||
|
@ -73,6 +71,9 @@ public class NettySecuredTransport extends NettyTransport {
|
|||
pipeline.addFirst("ssl", new SslHandler(serverEngine));
|
||||
pipeline.replace("dispatcher", "dispatcher", new SecuredMessageChannelHandler(nettyTransport, logger));
|
||||
}
|
||||
if (shieldUpstreamHandler != null) {
|
||||
pipeline.addFirst("ipfilter", shieldUpstreamHandler);
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,25 +5,30 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.transport.netty;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.Module;
|
||||
import org.elasticsearch.common.inject.PreProcessModule;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.shield.support.AbstractShieldModule;
|
||||
import org.elasticsearch.transport.TransportModule;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class NettySecuredTransportModule extends AbstractModule implements PreProcessModule {
|
||||
public class NettySecuredTransportModule extends AbstractShieldModule implements PreProcessModule {
|
||||
|
||||
public NettySecuredTransportModule(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processModule(Module module) {
|
||||
if (module instanceof TransportModule) {
|
||||
((TransportModule) module).setTransport(NettySecuredTransport.class, SecurityPlugin.NAME);
|
||||
((TransportModule) module).setTransport(NettySecuredTransport.class, ShieldPlugin.NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {}
|
||||
protected void configure(boolean clientMode) {}
|
||||
|
||||
}
|
|
@ -22,8 +22,7 @@ import java.util.Arrays;
|
|||
public class SSLConfig {
|
||||
|
||||
private static final ESLogger logger = Loggers.getLogger(SSLConfig.class);
|
||||
// TODO removing the second one results in fails, need to verify the differences, maybe per JVM?
|
||||
static final String[] DEFAULT_CIPHERS = new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA" };
|
||||
static final String[] DEFAULT_CIPHERS = new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" };
|
||||
private final boolean clientAuth;
|
||||
|
||||
private SSLContext sslContext;
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
plugin=org.elasticsearch.shield.plugin.SecurityPlugin
|
||||
plugin=org.elasticsearch.shield.plugin.ShieldPlugin
|
||||
version=${project.version}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
NAME
|
||||
|
||||
syskeygen - Generates the system key
|
||||
|
||||
SYNOPSIS
|
||||
|
||||
syskeygen [<file>]
|
||||
|
||||
DESCRIPTION
|
||||
|
||||
Generates the system key and stores in the system_key file. By default
|
||||
it will be stored in 'config/shield/.system_key' file. If the file location
|
||||
is customized in the elasticsearch.yml (under the 'shield.system_key.file'
|
||||
setting), the generated key will be stored in that custom location.
|
||||
|
||||
It is also possible to pass the file location (where the key will be stored)
|
||||
as an argument to this command.
|
||||
|
||||
ARGUMENTS
|
||||
|
||||
<file> (optional) Specifies the file where the generated key will be
|
||||
written to
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import org.elasticsearch.ExceptionsHelper;
|
||||
import org.elasticsearch.action.index.IndexRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.shield.authz.AuthorizationException;
|
||||
import org.elasticsearch.shield.key.InternalKeyService;
|
||||
import org.elasticsearch.shield.key.KeyService;
|
||||
import org.elasticsearch.shield.test.ShieldIntegrationTest;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
|
||||
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ScrollIdSigningTests extends ShieldIntegrationTest {
|
||||
|
||||
private KeyService keyService;
|
||||
|
||||
@Override
|
||||
protected Settings nodeSettings(int nodeOrdinal) {
|
||||
return ImmutableSettings.builder()
|
||||
.put(super.nodeSettings(nodeOrdinal))
|
||||
.put(InternalKeyService.FILE_SETTING, writeFile(newFolder(), "system_key", generateKey()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
keyService = internalCluster().getInstance(KeyService.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchAndClearScroll() throws Exception {
|
||||
IndexRequestBuilder[] docs = new IndexRequestBuilder[randomIntBetween(20, 100)];
|
||||
for (int i = 0; i < docs.length; i++) {
|
||||
docs[i] = client().prepareIndex("idx", "type").setSource("field", "value");
|
||||
}
|
||||
indexRandom(true, docs);
|
||||
SearchResponse response = client().prepareSearch()
|
||||
.setQuery(matchAllQuery())
|
||||
.setScroll(TimeValue.timeValueMinutes(2))
|
||||
.setSize(randomIntBetween(1, 10)).get();
|
||||
|
||||
assertHitCount(response, docs.length);
|
||||
int hits = response.getHits().hits().length;
|
||||
|
||||
try {
|
||||
assertSigned(response.getScrollId());
|
||||
while (true) {
|
||||
response = client().prepareSearchScroll(response.getScrollId())
|
||||
.setScroll(TimeValue.timeValueMinutes(2)).get();
|
||||
assertSigned(response.getScrollId());
|
||||
assertHitCount(response, docs.length);
|
||||
hits += response.getHits().hits().length;
|
||||
if (response.getHits().getHits().length == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertThat(hits, equalTo(docs.length));
|
||||
} finally {
|
||||
clearScroll(response.getScrollId());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchScroll_WithTamperedScrollId() throws Exception {
|
||||
IndexRequestBuilder[] docs = new IndexRequestBuilder[randomIntBetween(20, 100)];
|
||||
for (int i = 0; i < docs.length; i++) {
|
||||
docs[i] = client().prepareIndex("idx", "type").setSource("field", "value");
|
||||
}
|
||||
indexRandom(true, docs);
|
||||
SearchResponse response = client().prepareSearch()
|
||||
.setQuery(matchAllQuery())
|
||||
.setScroll(TimeValue.timeValueMinutes(2))
|
||||
.setSize(randomIntBetween(1, 10)).get();
|
||||
String scrollId = response.getScrollId();
|
||||
String tamperedScrollId = randomBoolean() ? scrollId.substring(randomIntBetween(1, 10)) : scrollId + randomAsciiOfLength(randomIntBetween(3, 10));
|
||||
try {
|
||||
client().prepareSearchScroll(tamperedScrollId).setScroll(TimeValue.timeValueMinutes(2)).get();
|
||||
fail("Expected an authorization exception to be thrown when scroll id is tampered");
|
||||
} catch (Exception e) {
|
||||
assertThat(ExceptionsHelper.unwrap(e, AuthorizationException.class), notNullValue());
|
||||
} finally {
|
||||
clearScroll(scrollId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearScroll_WithTamperedScrollId() throws Exception {
|
||||
IndexRequestBuilder[] docs = new IndexRequestBuilder[randomIntBetween(20, 100)];
|
||||
for (int i = 0; i < docs.length; i++) {
|
||||
docs[i] = client().prepareIndex("idx", "type").setSource("field", "value");
|
||||
}
|
||||
indexRandom(true, docs);
|
||||
SearchResponse response = client().prepareSearch()
|
||||
.setQuery(matchAllQuery())
|
||||
.setScroll(TimeValue.timeValueMinutes(2))
|
||||
.setSize(5).get();
|
||||
String scrollId = response.getScrollId();
|
||||
String tamperedScrollId = randomBoolean() ? scrollId.substring(randomIntBetween(1, 10)) : scrollId + randomAsciiOfLength(randomIntBetween(3, 10));
|
||||
try {
|
||||
client().prepareClearScroll().addScrollId(tamperedScrollId).get();
|
||||
fail("Expected an authorization exception to be thrown when scroll id is tampered");
|
||||
} catch (Exception e) {
|
||||
assertThat(ExceptionsHelper.unwrap(e, AuthorizationException.class), notNullValue());
|
||||
} finally {
|
||||
clearScroll(scrollId);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertSigned(String scrollId) {
|
||||
assertThat(keyService.signed(scrollId), is(true));
|
||||
}
|
||||
|
||||
private static byte[] generateKey() {
|
||||
try {
|
||||
return InternalKeyService.generateKey();
|
||||
} catch (Exception e) {
|
||||
fail("failed to generate key");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,18 +7,22 @@ package org.elasticsearch.shield;
|
|||
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.ActionRequest;
|
||||
import org.elasticsearch.action.search.SearchScrollRequest;
|
||||
import org.elasticsearch.action.support.ActionFilterChain;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.rest.RestChannel;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.rest.RestFilterChain;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.audit.AuditTrail;
|
||||
import org.elasticsearch.shield.authc.AuthenticationException;
|
||||
import org.elasticsearch.shield.authc.AuthenticationService;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.shield.authc.system.SystemRealm;
|
||||
import org.elasticsearch.shield.authz.AuthorizationException;
|
||||
import org.elasticsearch.shield.authz.AuthorizationService;
|
||||
import org.elasticsearch.shield.key.KeyService;
|
||||
import org.elasticsearch.shield.key.SignatureException;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.transport.TransportRequest;
|
||||
import org.junit.Before;
|
||||
|
@ -41,13 +45,17 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
private AuthenticationService authcService;
|
||||
private AuthorizationService authzService;
|
||||
private RestController restController;
|
||||
private KeyService keyService;
|
||||
private AuditTrail auditTrail;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
authcService = mock(AuthenticationService.class);
|
||||
authzService = mock(AuthorizationService.class);
|
||||
restController = mock(RestController.class);
|
||||
filter = new SecurityFilter(ImmutableSettings.EMPTY, authcService, authzService);
|
||||
keyService = mock(KeyService.class);
|
||||
auditTrail = mock(AuditTrail.class);
|
||||
filter = new SecurityFilter(ImmutableSettings.EMPTY, authcService, authzService, keyService, auditTrail);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -57,7 +65,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
User user = new User.Simple("_username", "r1");
|
||||
when(authcService.token("_action", request, null)).thenReturn(token);
|
||||
when(authcService.authenticate("_action", request, token)).thenReturn(user);
|
||||
filter.process("_action", request);
|
||||
filter.authenticateAndAuthorize("_action", request);
|
||||
verify(authzService).authorize(user, "_action", request);
|
||||
}
|
||||
|
||||
|
@ -68,7 +76,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
User user = new User.Simple("_username", "r1");
|
||||
when(authcService.token("internal:_action", request, SystemRealm.TOKEN)).thenReturn(token);
|
||||
when(authcService.authenticate("internal:_action", request, token)).thenReturn(user);
|
||||
filter.process("internal:_action", request);
|
||||
filter.authenticateAndAuthorize("internal:_action", request);
|
||||
verify(authzService).authorize(user, "internal:_action", request);
|
||||
}
|
||||
|
||||
|
@ -80,7 +88,18 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
AuthenticationToken token = mock(AuthenticationToken.class);
|
||||
when(authcService.token("_action", request, null)).thenReturn(token);
|
||||
when(authcService.authenticate("_action", request, token)).thenThrow(new AuthenticationException("failed authc"));
|
||||
filter.process("_action", request);
|
||||
filter.authenticateAndAuthorize("_action", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcess_Rest_AuthenticationFails_Authenticate() throws Exception {
|
||||
thrown.expect(AuthenticationException.class);
|
||||
thrown.expectMessage("failed authc");
|
||||
RestRequest request = mock(RestRequest.class);
|
||||
AuthenticationToken token = mock(AuthenticationToken.class);
|
||||
when(authcService.token(request)).thenReturn(token);
|
||||
when(authcService.authenticate(request, token)).thenThrow(new AuthenticationException("failed authc"));
|
||||
filter.authenticate(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -89,9 +108,19 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
thrown.expectMessage("failed authc");
|
||||
TransportRequest request = new InternalRequest();
|
||||
when(authcService.token("_action", request, null)).thenThrow(new AuthenticationException("failed authc"));
|
||||
filter.process("_action", request);
|
||||
filter.authenticateAndAuthorize("_action", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcess_Rest_AuthenticationFails_NoToken() throws Exception {
|
||||
thrown.expect(AuthenticationException.class);
|
||||
thrown.expectMessage("failed authc");
|
||||
RestRequest request = mock(RestRequest.class);
|
||||
when(authcService.token(request)).thenThrow(new AuthenticationException("failed authc"));
|
||||
filter.authenticate(request);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testProcess_AuthorizationFails() throws Exception {
|
||||
thrown.expect(AuthorizationException.class);
|
||||
|
@ -102,7 +131,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
when(authcService.token("_action", request, null)).thenReturn(token);
|
||||
when(authcService.authenticate("_action", request, token)).thenReturn(user);
|
||||
doThrow(new AuthorizationException("failed authz")).when(authzService).authorize(user, "_action", request);
|
||||
filter.process("_action", request);
|
||||
filter.authenticateAndAuthorize("_action", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -111,7 +140,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
SecurityFilter.Transport transport = new SecurityFilter.Transport(filter);
|
||||
InternalRequest request = new InternalRequest();
|
||||
transport.inboundRequest("_action", request);
|
||||
verify(filter).process("_action", request);
|
||||
verify(filter).authenticateAndAuthorize("_action", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -121,7 +150,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
filter = mock(SecurityFilter.class);
|
||||
SecurityFilter.Transport transport = new SecurityFilter.Transport(filter);
|
||||
InternalRequest request = new InternalRequest();
|
||||
doThrow(new RuntimeException("process-error")).when(filter).process("_action", request);
|
||||
doThrow(new RuntimeException("process-error")).when(filter).authenticateAndAuthorize("_action", request);
|
||||
transport.inboundRequest("_action", request);
|
||||
}
|
||||
|
||||
|
@ -132,9 +161,10 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
ActionRequest request = mock(ActionRequest.class);
|
||||
ActionListener listener = mock(ActionListener.class);
|
||||
ActionFilterChain chain = mock(ActionFilterChain.class);
|
||||
when(filter.unsign(any(User.class), eq("_action"), eq(request))).thenReturn(request);
|
||||
action.apply("_action", request, listener, chain);
|
||||
verify(filter).process("_action", request);
|
||||
verify(chain).proceed("_action", request, listener);
|
||||
verify(filter).authenticateAndAuthorize("_action", request);
|
||||
verify(chain).proceed(eq("_action"), eq(request), isA(SecurityFilter.SigningListener.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -145,12 +175,31 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
ActionListener listener = mock(ActionListener.class);
|
||||
ActionFilterChain chain = mock(ActionFilterChain.class);
|
||||
RuntimeException exception = new RuntimeException("process-error");
|
||||
doThrow(exception).when(filter).process("_action", request);
|
||||
doThrow(exception).when(filter).authenticateAndAuthorize("_action", request);
|
||||
action.apply("_action", request, listener, chain);
|
||||
verify(listener).onFailure(exception);
|
||||
verifyNoMoreInteractions(chain);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAction_SignatureError() throws Exception {
|
||||
SecurityFilter.Action action = new SecurityFilter.Action(filter);
|
||||
SearchScrollRequest request = new SearchScrollRequest("scroll_id");
|
||||
ActionListener listener = mock(ActionListener.class);
|
||||
ActionFilterChain chain = mock(ActionFilterChain.class);
|
||||
SignatureException sigException = new SignatureException("bad bad boy");
|
||||
User user = mock(User.class);
|
||||
AuthenticationToken token = mock(AuthenticationToken.class);
|
||||
when(authcService.token("_action", request, null)).thenReturn(token);
|
||||
when(authcService.authenticate("_action", request, token)).thenReturn(user);
|
||||
when(keyService.signed("scroll_id")).thenReturn(true);
|
||||
doThrow(sigException).when(keyService).unsignAndVerify("scroll_id");
|
||||
action.apply("_action", request, listener, chain);
|
||||
verify(listener).onFailure(isA(AuthorizationException.class));
|
||||
verify(auditTrail).tamperedRequest(user, "_action", request);
|
||||
verifyNoMoreInteractions(chain);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRest_WithToken() throws Exception {
|
||||
SecurityFilter.Rest rest = new SecurityFilter.Rest(filter, restController);
|
||||
|
@ -158,7 +207,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
RestChannel channel = mock(RestChannel.class);
|
||||
RestFilterChain chain = mock(RestFilterChain.class);
|
||||
rest.process(request, channel, chain);
|
||||
verify(authcService).extractAndRegisterToken(request);
|
||||
verify(authcService).token(request);
|
||||
verify(restController).registerFilter(rest);
|
||||
}
|
||||
|
||||
|
@ -171,7 +220,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
|
|||
RestRequest request = mock(RestRequest.class);
|
||||
RestChannel channel = mock(RestChannel.class);
|
||||
RestFilterChain chain = mock(RestFilterChain.class);
|
||||
doThrow(exception).when(authcService).extractAndRegisterToken(request);
|
||||
doThrow(exception).when(authcService).token(request);
|
||||
rest.process(request, channel, chain);
|
||||
verify(restController).registerFilter(rest);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ public class AuditTrailModuleTests extends ElasticsearchTestCase {
|
|||
@Test
|
||||
public void testEnabled() throws Exception {
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put("client.type", "node")
|
||||
.put("shield.audit.enabled", false)
|
||||
.build();
|
||||
Injector injector = Guice.createInjector(new SettingsModule(settings), new AuditTrailModule(settings));
|
||||
|
@ -33,7 +34,8 @@ public class AuditTrailModuleTests extends ElasticsearchTestCase {
|
|||
|
||||
@Test
|
||||
public void testDisabledByDefault() throws Exception {
|
||||
Settings settings = ImmutableSettings.EMPTY;
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put("client.type", "node").build();
|
||||
Injector injector = Guice.createInjector(new SettingsModule(settings), new AuditTrailModule(settings));
|
||||
AuditTrail auditTrail = injector.getInstance(AuditTrail.class);
|
||||
assertThat(auditTrail, is(AuditTrail.NOOP));
|
||||
|
@ -43,6 +45,7 @@ public class AuditTrailModuleTests extends ElasticsearchTestCase {
|
|||
public void testLogfile() throws Exception {
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put("shield.audit.enabled", true)
|
||||
.put("client.type", "node")
|
||||
.build();
|
||||
Injector injector = Guice.createInjector(new SettingsModule(settings), new AuditTrailModule(settings));
|
||||
AuditTrail auditTrail = injector.getInstance(AuditTrail.class);
|
||||
|
@ -58,6 +61,7 @@ public class AuditTrailModuleTests extends ElasticsearchTestCase {
|
|||
Settings settings = ImmutableSettings.builder()
|
||||
.put("shield.audit.enabled", true)
|
||||
.put("shield.audit.outputs" , "foo")
|
||||
.put("client.type", "node")
|
||||
.build();
|
||||
try {
|
||||
Guice.createInjector(new SettingsModule(settings), new AuditTrailModule(settings));
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.elasticsearch.shield.audit;
|
|||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
|
@ -17,6 +18,7 @@ import org.junit.Test;
|
|||
import java.util.Set;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockingDetails;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
|
@ -29,6 +31,7 @@ public class AuditTrailServiceTests extends ElasticsearchTestCase {
|
|||
|
||||
private AuthenticationToken token;
|
||||
private TransportMessage message;
|
||||
private RestRequest restRequest;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
|
@ -40,6 +43,7 @@ public class AuditTrailServiceTests extends ElasticsearchTestCase {
|
|||
service = new AuditTrailService(ImmutableSettings.EMPTY, auditTrails);
|
||||
token = mock(AuthenticationToken.class);
|
||||
message = mock(TransportMessage.class);
|
||||
restRequest = mock(RestRequest.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -50,6 +54,14 @@ public class AuditTrailServiceTests extends ElasticsearchTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticationFailed_Rest() throws Exception {
|
||||
service.authenticationFailed(token, restRequest);
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
verify(auditTrail).authenticationFailed(token, restRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticationFailed_Realm() throws Exception {
|
||||
service.authenticationFailed("_realm", token, "_action", message);
|
||||
|
@ -58,6 +70,14 @@ public class AuditTrailServiceTests extends ElasticsearchTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticationFailed_Rest_Realm() throws Exception {
|
||||
service.authenticationFailed("_realm", token, restRequest);
|
||||
for (AuditTrail auditTrail : auditTrails) {
|
||||
verify(auditTrail).authenticationFailed("_realm", token, restRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnonymousAccess() throws Exception {
|
||||
service.anonymousAccess("_action", message);
|
||||
|
|
|
@ -6,17 +6,21 @@
|
|||
package org.elasticsearch.shield.audit.logfile;
|
||||
|
||||
import org.elasticsearch.common.transport.LocalTransportAddress;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.AuthenticationToken;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.transport.TransportMessage;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.List;
|
||||
|
||||
import static org.elasticsearch.shield.audit.logfile.CapturingLogger.Level;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -63,6 +67,29 @@ public class LoggingAuditTrailTests extends ElasticsearchTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticationFailed_Rest() throws Exception {
|
||||
for (Level level : Level.values()) {
|
||||
RestRequest request = mock(RestRequest.class);
|
||||
when(request.getRemoteAddress()).thenReturn(new InetSocketAddress("_hostname", 9200));
|
||||
when(request.uri()).thenReturn("_uri");
|
||||
when(request.toString()).thenReturn("rest_request");
|
||||
CapturingLogger logger = new CapturingLogger(level);
|
||||
LoggingAuditTrail auditTrail = new LoggingAuditTrail(logger);
|
||||
auditTrail.authenticationFailed(new MockToken(), request);
|
||||
switch (level) {
|
||||
case ERROR:
|
||||
case WARN:
|
||||
case INFO:
|
||||
assertMsg(logger, Level.ERROR, "AUTHENTICATION_FAILED\thost=[_hostname:9200], URI=[_uri], principal=[_principal]");
|
||||
break;
|
||||
case DEBUG:
|
||||
case TRACE:
|
||||
assertMsg(logger, Level.DEBUG, "AUTHENTICATION_FAILED\thost=[_hostname:9200], URI=[_uri], principal=[_principal], request=[rest_request]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticationFailed_Realm() throws Exception {
|
||||
for (Level level : Level.values()) {
|
||||
|
@ -82,6 +109,29 @@ public class LoggingAuditTrailTests extends ElasticsearchTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticationFailed_Realm_Rest() throws Exception {
|
||||
for (Level level : Level.values()) {
|
||||
RestRequest request = mock(RestRequest.class);
|
||||
when(request.getRemoteAddress()).thenReturn(new InetSocketAddress("_hostname", 9200));
|
||||
when(request.uri()).thenReturn("_uri");
|
||||
when(request.toString()).thenReturn("rest_request");
|
||||
CapturingLogger logger = new CapturingLogger(level);
|
||||
LoggingAuditTrail auditTrail = new LoggingAuditTrail(logger);
|
||||
auditTrail.authenticationFailed("_realm", new MockToken(), request);
|
||||
switch (level) {
|
||||
case ERROR:
|
||||
case WARN:
|
||||
case INFO:
|
||||
case DEBUG:
|
||||
assertEmptyLog(logger);
|
||||
break;
|
||||
case TRACE:
|
||||
assertMsg(logger, Level.TRACE, "AUTHENTICATION_FAILED[_realm]\thost=[_hostname:9200], URI=[_uri], principal=[_principal], request=[rest_request]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessGranted() throws Exception {
|
||||
for (Level level : Level.values()) {
|
||||
|
@ -144,6 +194,7 @@ public class LoggingAuditTrailTests extends ElasticsearchTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private static class MockToken implements AuthenticationToken {
|
||||
@Override
|
||||
public String principal() {
|
||||
|
@ -155,5 +206,10 @@ public class LoggingAuditTrailTests extends ElasticsearchTestCase {
|
|||
fail("it's not allowed to print the credentials of the auth token");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCredentials() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.User;
|
||||
|
@ -16,6 +18,8 @@ import org.junit.Rule;
|
|||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.elasticsearch.shield.test.ShieldAssertions.assertContainsWWWAuthenticateHeader;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
@ -41,7 +45,7 @@ public class InternalAuthenticationServiceTests extends ElasticsearchTestCase {
|
|||
public void init() throws Exception {
|
||||
token = mock(AuthenticationToken.class);
|
||||
message = new InternalMessage();
|
||||
restRequest = mock(RestRequest.class);
|
||||
restRequest = new InternalRestRequest();
|
||||
firstRealm = mock(Realm.class);
|
||||
when(firstRealm.type()).thenReturn("first");
|
||||
secondRealm = mock(Realm.class);
|
||||
|
@ -161,24 +165,93 @@ public class InternalAuthenticationServiceTests extends ElasticsearchTestCase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testExtractAndRegisterToken_Exists() throws Exception {
|
||||
public void testToken_Rest_Exists() throws Exception {
|
||||
AuthenticationToken token = mock(AuthenticationToken.class);
|
||||
when(firstRealm.token(restRequest)).thenReturn(null);
|
||||
when(secondRealm.token(restRequest)).thenReturn(token);
|
||||
service.extractAndRegisterToken(restRequest);
|
||||
AuthenticationToken foundToken = service.token(restRequest);
|
||||
assertThat(foundToken, is(token));
|
||||
assertThat(restRequest.getFromContext(InternalAuthenticationService.TOKEN_CTX_KEY), equalTo((Object) token));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyToken_Missing() throws Exception {
|
||||
public void testToken_Rest_Missing() throws Exception {
|
||||
thrown.expect(AuthenticationException.class);
|
||||
thrown.expectMessage("Missing authentication token");
|
||||
when(firstRealm.token(restRequest)).thenReturn(null);
|
||||
when(secondRealm.token(restRequest)).thenReturn(null);
|
||||
service.extractAndRegisterToken(restRequest);
|
||||
service.token(restRequest);
|
||||
}
|
||||
|
||||
private static class InternalMessage extends TransportMessage<InternalMessage> {
|
||||
}
|
||||
|
||||
private static class InternalRestRequest extends RestRequest {
|
||||
|
||||
@Override
|
||||
public Method method() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uri() {
|
||||
return "_uri";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String rawPath() {
|
||||
return "_path";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasContent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contentUnsafe() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BytesReference content() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String header(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Map.Entry<String, String>> headers() {
|
||||
return ImmutableMap.<String, String>of().entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasParam(String key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String param(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> params() {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String param(String key, String defaultValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "rest_request";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.elasticsearch.common.inject.Injector;
|
|||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
@ -21,6 +22,7 @@ import java.nio.file.Path;
|
|||
import java.nio.file.Paths;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -38,7 +40,8 @@ public class ESUsersModuleTests extends ElasticsearchTestCase {
|
|||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
Injector injector = Guice.createInjector(new TestModule(users, usersRoles), new ESUsersModule());
|
||||
Settings settings = ImmutableSettings.builder().put("client.type", "node").build();
|
||||
Injector injector = Guice.createInjector(new TestModule(users, usersRoles), new ESUsersModule(settings));
|
||||
ESUsersRealm realm = injector.getInstance(ESUsersRealm.class);
|
||||
assertThat(realm, notNullValue());
|
||||
assertThat(realm.userPasswdStore, notNullValue());
|
||||
|
@ -81,6 +84,7 @@ public class ESUsersModuleTests extends ElasticsearchTestCase {
|
|||
bind(Environment.class).toInstance(env);
|
||||
bind(ThreadPool.class).toInstance(new ThreadPool("test"));
|
||||
bind(ResourceWatcherService.class).asEagerSingleton();
|
||||
bind(RestController.class).toInstance(mock(RestController.class));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,39 +13,54 @@ import org.elasticsearch.client.AdminClient;
|
|||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.client.ClusterAdminClient;
|
||||
import org.elasticsearch.client.IndicesAdminClient;
|
||||
import org.elasticsearch.common.collect.ImmutableSet;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.RestChannel;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.shield.SecurityFilter;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.support.UserPasswdStore;
|
||||
import org.elasticsearch.shield.authc.support.UserRolesStore;
|
||||
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.shield.authc.support.*;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.transport.TransportRequest;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.arrayContaining;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ESUsersRealmTests extends ElasticsearchTestCase {
|
||||
|
||||
private RestController restController;
|
||||
private Client client;
|
||||
private AdminClient adminClient;
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
client = mock(Client.class);
|
||||
adminClient = mock(AdminClient.class);
|
||||
restController = mock(RestController.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestHeaderRegistration() {
|
||||
new ESUsersRealm(ImmutableSettings.EMPTY, mock(UserPasswdStore.class), mock(UserRolesStore.class), restController);
|
||||
verify(restController).registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticate() throws Exception {
|
||||
Settings settings = ImmutableSettings.builder().build();
|
||||
MockUserPasswdStore userPasswdStore = new MockUserPasswdStore("user1", "test123");
|
||||
MockUserRolesStore userRolesStore = new MockUserRolesStore("user1", "role1", "role2");
|
||||
ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore);
|
||||
User user = realm.authenticate(new UsernamePasswordToken("user1", "test123".toCharArray()));
|
||||
ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore, restController);
|
||||
User user = realm.authenticate(new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
|
||||
assertTrue(userPasswdStore.called);
|
||||
assertTrue(userRolesStore.called);
|
||||
assertThat(user, notNullValue());
|
||||
|
@ -60,16 +75,16 @@ public class ESUsersRealmTests extends ElasticsearchTestCase {
|
|||
Settings settings = ImmutableSettings.builder().build();
|
||||
MockUserPasswdStore userPasswdStore = new MockUserPasswdStore("user1", "test123");
|
||||
MockUserRolesStore userRolesStore = new MockUserRolesStore("user1", "role1", "role2");
|
||||
ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore);
|
||||
ESUsersRealm realm = new ESUsersRealm(settings, userPasswdStore, userRolesStore, restController);
|
||||
|
||||
TransportRequest request = new TransportRequest() {};
|
||||
UsernamePasswordToken.putTokenHeader(request, new UsernamePasswordToken("user1", "test123".toCharArray()));
|
||||
UsernamePasswordToken.putTokenHeader(request, new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
|
||||
|
||||
UsernamePasswordToken token = realm.token(request);
|
||||
assertThat(token, notNullValue());
|
||||
assertThat(token.principal(), equalTo("user1"));
|
||||
assertThat(token.credentials(), notNullValue());
|
||||
assertThat(new String(token.credentials()), equalTo("test123"));
|
||||
assertThat(new String(token.credentials().internalChars()), equalTo("test123"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -85,10 +100,10 @@ public class ESUsersRealmTests extends ElasticsearchTestCase {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyPassword(String username, char[] password) {
|
||||
public boolean verifyPassword(String username, SecuredString password) {
|
||||
called = true;
|
||||
assertThat(username, equalTo(this.username));
|
||||
assertThat(new String(password), equalTo(this.password));
|
||||
assertThat(new String(password.internalChars()), equalTo(this.password));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -116,9 +131,8 @@ public class ESUsersRealmTests extends ElasticsearchTestCase {
|
|||
@Test @SuppressWarnings("unchecked")
|
||||
public void testRestHeadersAreCopied() throws Exception {
|
||||
// the required header will be registered only if ESUsersRealm is actually used.
|
||||
new ESUsersRealm(ImmutableSettings.EMPTY, null, null);
|
||||
Client client = mock(Client.class);
|
||||
AdminClient adminClient = mock(AdminClient.class);
|
||||
new ESUsersRealm(ImmutableSettings.EMPTY, null, null, restController);
|
||||
when(restController.relevantHeaders()).thenReturn(ImmutableSet.of(UsernamePasswordToken.BASIC_AUTH_HEADER));
|
||||
when(client.admin()).thenReturn(adminClient);
|
||||
when(adminClient.cluster()).thenReturn(mock(ClusterAdminClient.class));
|
||||
when(adminClient.indices()).thenReturn(mock(IndicesAdminClient.class));
|
||||
|
@ -128,15 +142,16 @@ public class ESUsersRealmTests extends ElasticsearchTestCase {
|
|||
return null;
|
||||
}
|
||||
};
|
||||
RestRequest restRequest = mock(RestRequest.class);
|
||||
final Action action = mock(Action.class);
|
||||
final ActionListener listener = mock(ActionListener.class);
|
||||
BaseRestHandler handler = new BaseRestHandler(ImmutableSettings.EMPTY, client) {
|
||||
BaseRestHandler handler = new BaseRestHandler(ImmutableSettings.EMPTY, restController, client) {
|
||||
@Override
|
||||
protected void handleRequest(RestRequest restRequest, RestChannel channel, Client client) throws Exception {
|
||||
client.execute(action, request, listener);
|
||||
}
|
||||
};
|
||||
RestRequest restRequest = mock(RestRequest.class);
|
||||
|
||||
when(restRequest.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn("foobar");
|
||||
RestChannel channel = mock(RestChannel.class);
|
||||
handler.handleRequest(restRequest, channel);
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.elasticsearch.common.settings.ImmutableSettings;
|
|||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.authc.support.Hasher;
|
||||
import org.elasticsearch.shield.authc.support.SecuredStringTests;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
|
@ -82,20 +83,20 @@ public class FileUserPasswdStoreTests extends ElasticsearchTestCase {
|
|||
}
|
||||
});
|
||||
|
||||
assertTrue(store.verifyPassword("bcrypt", "test123".toCharArray()));
|
||||
assertTrue(store.verifyPassword("bcrypt", SecuredStringTests.build("test123")));
|
||||
|
||||
watcherService.start();
|
||||
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(tmp, Charsets.UTF_8, StandardOpenOption.APPEND)) {
|
||||
writer.newLine();
|
||||
writer.append("foobar:" + new String(Hasher.HTPASSWD.hash("barfoo".toCharArray())));
|
||||
writer.append("foobar:" + new String(Hasher.HTPASSWD.hash(SecuredStringTests.build("barfoo"))));
|
||||
}
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("Waited too long for the updated file to be picked up");
|
||||
}
|
||||
|
||||
assertTrue(store.verifyPassword("foobar", "barfoo".toCharArray()));
|
||||
assertTrue(store.verifyPassword("foobar", SecuredStringTests.build("barfoo")));
|
||||
|
||||
} finally {
|
||||
if (watcherService != null) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.elasticsearch.common.settings.Settings;
|
|||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.authc.esusers.FileUserRolesStore;
|
||||
import org.elasticsearch.shield.authc.support.Hasher;
|
||||
import org.elasticsearch.shield.authc.support.SecuredStringTests;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
@ -44,7 +45,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
assertThat(command, instanceOf(ESUsersTool.Useradd.class));
|
||||
ESUsersTool.Useradd cmd = (ESUsersTool.Useradd) command;
|
||||
assertThat(cmd.username, equalTo("username"));
|
||||
assertThat(new String(cmd.passwd), equalTo("changeme"));
|
||||
assertThat(new String(cmd.passwd.internalChars()), equalTo("changeme"));
|
||||
assertThat(cmd.roles, notNullValue());
|
||||
assertThat(cmd.roles, arrayContaining("r1", "r2", "r3"));
|
||||
}
|
||||
|
@ -69,7 +70,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
assertThat(command, instanceOf(ESUsersTool.Useradd.class));
|
||||
ESUsersTool.Useradd cmd = (ESUsersTool.Useradd) command;
|
||||
assertThat(cmd.username, equalTo("username"));
|
||||
assertThat(new String(cmd.passwd), equalTo("changeme"));
|
||||
assertThat(new String(cmd.passwd.internalChars()), equalTo("changeme"));
|
||||
assertThat(cmd.roles, notNullValue());
|
||||
assertThat(cmd.roles.length, is(0));
|
||||
}
|
||||
|
@ -84,7 +85,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
.put("shield.authc.esusers.files.users_roles", userRolesFile)
|
||||
.build();
|
||||
|
||||
ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new MockTerminal(), "user1", "changeme".toCharArray(), "r1", "r2");
|
||||
ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new MockTerminal(), "user1", SecuredStringTests.build("changeme"), "r1", "r2");
|
||||
|
||||
CliTool.ExitStatus status = execute(cmd, settings);
|
||||
assertThat(status, is(CliTool.ExitStatus.OK));
|
||||
|
@ -97,7 +98,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
String line = lines.get(0);
|
||||
assertThat(line, startsWith("user1:"));
|
||||
String hash = line.substring("user1:".length());
|
||||
assertThat(Hasher.HTPASSWD.verify("changeme".toCharArray(), hash.toCharArray()), is(true));
|
||||
assertThat(Hasher.HTPASSWD.verify(SecuredStringTests.build("changeme"), hash.toCharArray()), is(true));
|
||||
|
||||
assertFileExists(userRolesFile);
|
||||
lines = Files.readLines(userRolesFile, Charsets.UTF_8);
|
||||
|
@ -115,7 +116,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
.put("shield.authc.esusers.files.users_roles", userRolesFile)
|
||||
.build();
|
||||
|
||||
ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new MockTerminal(), "user1", "changeme".toCharArray(), "r1", "r2");
|
||||
ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new MockTerminal(), "user1", SecuredStringTests.build("changeme"), "r1", "r2");
|
||||
|
||||
CliTool.ExitStatus status = execute(cmd, settings);
|
||||
assertThat(status, is(CliTool.ExitStatus.OK));
|
||||
|
@ -131,7 +132,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
for (String line : lines) {
|
||||
if (line.startsWith("user1")) {
|
||||
String hash = line.substring("user1:".length());
|
||||
assertThat(Hasher.HTPASSWD.verify("changeme".toCharArray(), hash.toCharArray()), is(true));
|
||||
assertThat(Hasher.HTPASSWD.verify(SecuredStringTests.build("changeme"), hash.toCharArray()), is(true));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,7 +151,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
.put("shield.authc.esusers.files.users_roles", userRolesFile)
|
||||
.build();
|
||||
|
||||
ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new MockTerminal(), "user1", "changeme".toCharArray(), "r1", "r2");
|
||||
ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new MockTerminal(), "user1", SecuredStringTests.build("changeme"), "r1", "r2");
|
||||
|
||||
CliTool.ExitStatus status = execute(cmd, settings);
|
||||
assertThat(status, is(CliTool.ExitStatus.CODE_ERROR));
|
||||
|
@ -246,7 +247,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
assertThat(command, instanceOf(ESUsersTool.Passwd.class));
|
||||
ESUsersTool.Passwd cmd = (ESUsersTool.Passwd) command;
|
||||
assertThat(cmd.username, equalTo("user1"));
|
||||
assertThat(new String(cmd.passwd), equalTo("changeme"));
|
||||
assertThat(new String(cmd.passwd.internalChars()), equalTo("changeme"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -273,7 +274,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
assertThat(command, instanceOf(ESUsersTool.Passwd.class));
|
||||
ESUsersTool.Passwd cmd = (ESUsersTool.Passwd) command;
|
||||
assertThat(cmd.username, equalTo("user1"));
|
||||
assertThat(new String(cmd.passwd), equalTo("changeme"));
|
||||
assertThat(new String(cmd.passwd.internalChars()), equalTo("changeme"));
|
||||
assertThat(secretRequested.get(), is(true));
|
||||
}
|
||||
|
||||
|
@ -295,7 +296,7 @@ public class ESUsersToolTests extends CliToolTestCase {
|
|||
String line = lines.get(0);
|
||||
assertThat(line, startsWith("user1:"));
|
||||
String hash = line.substring("user1:".length());
|
||||
assertThat(Hasher.HTPASSWD.verify("changeme".toCharArray(), hash.toCharArray()), is(true));
|
||||
assertThat(Hasher.HTPASSWD.verify(SecuredStringTests.build("changeme"), hash.toCharArray()), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.elasticsearch.shield.authc.ldap;
|
|||
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authc.support.SecuredStringTests;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
@ -21,8 +22,6 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
|
|||
public static final String AD_LDAP_URL = "ldap://54.213.145.20:389";
|
||||
public static final String PASSWORD = "4joD8LmWcrEfRa&p";
|
||||
|
||||
public static String SETTINGS_PREFIX = LdapRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.';
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void testAdAuth() {
|
||||
|
@ -31,7 +30,7 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
|
|||
|
||||
String userName = "ironman";
|
||||
|
||||
LdapConnection ldap = connectionFactory.bind(userName, PASSWORD.toCharArray());
|
||||
LdapConnection ldap = connectionFactory.bind(userName, SecuredStringTests.build(PASSWORD));
|
||||
String userDN = ldap.getAuthenticatedUserDn();
|
||||
//System.out.println("userPassword check:"+ldap.checkPassword(userDn, userPass));
|
||||
|
||||
|
@ -47,10 +46,10 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
|
|||
String userTemplate = "cn={0},cn=Users,dc=ad,dc=test,dc=elasticsearch,dc=com";
|
||||
boolean isSubTreeSearch = true;
|
||||
StandardLdapConnectionFactory connectionFactory = new StandardLdapConnectionFactory(
|
||||
LdapConnectionTests.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, isSubTreeSearch));
|
||||
LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, isSubTreeSearch));
|
||||
|
||||
String user = "Tony Stark";
|
||||
LdapConnection ldap = connectionFactory.bind(user, PASSWORD.toCharArray());
|
||||
LdapConnection ldap = connectionFactory.bind(user, SecuredStringTests.build(PASSWORD));
|
||||
|
||||
List<String> groups = ldap.getGroupsFromUserAttrs(ldap.getAuthenticatedUserDn());
|
||||
List<String> groups2 = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
||||
|
@ -62,8 +61,8 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
|
|||
|
||||
public static Settings buildAdSettings(String ldapUrl, String adDomainName) {
|
||||
return ImmutableSettings.builder()
|
||||
.putArray(SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.URLS_SETTING, ldapUrl)
|
||||
.put(SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.AD_DOMAIN_NAME_SETTING, adDomainName)
|
||||
.putArray(LdapTest.SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.URLS_SETTING, ldapUrl)
|
||||
.put(LdapTest.SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.AD_DOMAIN_NAME_SETTING, adDomainName)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.apache.directory.server.i18n.I18n;
|
|||
import org.apache.directory.server.ldap.LdapServer;
|
||||
import org.apache.directory.server.protocol.shared.store.LdifFileLoader;
|
||||
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URISyntaxException;
|
||||
|
@ -40,17 +41,9 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Helper Class to start up an Apache DS LDAP server for testing. Here is a typical use example in tests:
|
||||
* <pre>
|
||||
* static ApacheDsEmbedded ldap = new ApacheDsEmbedded("o=sevenSeas", "seven-seas.ldif");
|
||||
* Helper Class to start up an Apache DS LDAP server for testing.
|
||||
*
|
||||
* @BeforeClass public static void startServer() throws Exception {
|
||||
* ldap.startServer();
|
||||
* }
|
||||
* @AfterClass public static void stopServer() throws Exception {
|
||||
* ldap.stopAndCleanup();
|
||||
* }
|
||||
* </pre>
|
||||
* Use ApacheDsRule instead of this class in tests
|
||||
*/
|
||||
public class ApacheDsEmbedded {
|
||||
/**
|
||||
|
@ -78,8 +71,8 @@ public class ApacheDsEmbedded {
|
|||
*
|
||||
* @throws Exception If something went wrong
|
||||
*/
|
||||
public ApacheDsEmbedded(String baseDN, String ldifFileName, String testName) {
|
||||
this.workDir = new File(System.getProperty("java.io.tmpdir") + "/server-work/" + testName);
|
||||
public ApacheDsEmbedded(String baseDN, String ldifFileName, File workDir) {
|
||||
this.workDir = workDir;
|
||||
this.baseDN = baseDN;
|
||||
this.ldifFileName = ldifFileName;
|
||||
this.port = 10389 + CHILD_JVM_ID;
|
||||
|
@ -118,7 +111,6 @@ public class ApacheDsEmbedded {
|
|||
public void stopAndCleanup() throws Exception {
|
||||
if (server != null) server.stop();
|
||||
if (service != null) service.shutdown();
|
||||
workDir.delete();
|
||||
}
|
||||
|
||||
|
||||
|
@ -262,8 +254,6 @@ public class ApacheDsEmbedded {
|
|||
entryApache.add("dc", "Apache");
|
||||
service.getAdminSession().add(entryApache);
|
||||
}
|
||||
|
||||
// We are all done !
|
||||
}
|
||||
|
||||
private void loadSchema(String ldifFileName) throws URISyntaxException {
|
||||
|
@ -274,30 +264,4 @@ public class ApacheDsEmbedded {
|
|||
LdifFileLoader ldifLoader = new LdifFileLoader(service.getAdminSession(), ldifFile, Collections.EMPTY_LIST);
|
||||
ldifLoader.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main class.
|
||||
*
|
||||
* @param args Not used.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
String baseDir = "o=sevenSeas";
|
||||
String ldifImport = "seven-seas.ldif";
|
||||
// Create the server
|
||||
ApacheDsEmbedded ads = new ApacheDsEmbedded(baseDir, ldifImport, "test");
|
||||
ads.startServer();
|
||||
// Read an entry
|
||||
Entry result = ads.service.getAdminSession().lookup(new Dn(baseDir));
|
||||
|
||||
// And print it if available
|
||||
System.out.println("Found entry : " + result);
|
||||
|
||||
// optionally we can start a server too
|
||||
ads.stopAndCleanup();
|
||||
} catch (Exception e) {
|
||||
// Ok, we have something wrong going on ...
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.ldap;
|
||||
|
||||
import org.elasticsearch.common.logging.ESLogger;
|
||||
import org.elasticsearch.common.logging.Loggers;
|
||||
import org.junit.rules.ExternalResource;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ApacheDsRule extends ExternalResource {
|
||||
|
||||
private final ESLogger logger = Loggers.getLogger(getClass());
|
||||
|
||||
private ApacheDsEmbedded ldap;
|
||||
private final TemporaryFolder temporaryFolder;
|
||||
|
||||
public ApacheDsRule(TemporaryFolder temporaryFolder) {
|
||||
this.temporaryFolder = temporaryFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void before() throws Throwable {
|
||||
ldap = new ApacheDsEmbedded("o=sevenSeas", "seven-seas.ldif", temporaryFolder.newFolder());
|
||||
ldap.startServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void after() {
|
||||
try {
|
||||
ldap.stopAndCleanup();
|
||||
} catch (Exception e) {
|
||||
logger.error("failed to stop and cleanup the embedded ldap server", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return ldap.getUrl();
|
||||
}
|
||||
}
|
|
@ -4,12 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.shield.authc.ldap;
|
||||
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
import org.elasticsearch.shield.authc.support.SecuredStringTests;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -17,23 +17,11 @@ import java.util.Map;
|
|||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
public class LdapConnectionTests extends ElasticsearchTestCase {
|
||||
public static String SETTINGS_PREFIX = LdapRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.';
|
||||
|
||||
static ApacheDsEmbedded ldap = new ApacheDsEmbedded("o=sevenSeas", "seven-seas.ldif", LdapConnectionTests.class.getName());
|
||||
|
||||
@BeforeClass
|
||||
public static void startServer() throws Exception {
|
||||
ldap.startServer();
|
||||
}
|
||||
@AfterClass
|
||||
public static void stopServer() throws Exception {
|
||||
ldap.stopAndCleanup();
|
||||
}
|
||||
public class LdapConnectionTests extends LdapTest {
|
||||
|
||||
@Test
|
||||
public void testBindWithTemplates() {
|
||||
String[] ldapUrls = new String[]{ldap.getUrl()};
|
||||
String[] ldapUrls = new String[]{apacheDsRule.getUrl()};
|
||||
String groupSearchBase = "o=sevenSeas";
|
||||
boolean isSubTreeSearch = true;
|
||||
String[] userTemplates = new String[]{
|
||||
|
@ -45,17 +33,18 @@ public class LdapConnectionTests extends ElasticsearchTestCase {
|
|||
buildLdapSettings(ldapUrls, userTemplates, groupSearchBase, isSubTreeSearch));
|
||||
|
||||
String user = "Horatio Hornblower";
|
||||
char[] userPass = "pass".toCharArray();
|
||||
SecuredString userPass = SecuredStringTests.build("pass");
|
||||
|
||||
LdapConnection ldap = connectionFactory.bind(user, userPass);
|
||||
Map<String, String[]> attrs = ldap.getUserAttrs(ldap.getAuthenticatedUserDn());
|
||||
|
||||
assertThat( attrs, hasKey("uid"));
|
||||
assertThat(attrs, hasKey("uid"));
|
||||
assertThat( attrs.get("uid"), arrayContaining("hhornblo"));
|
||||
}
|
||||
@Test
|
||||
|
||||
@Test(expected = LdapException.class)
|
||||
public void testBindWithBogusTemplates() {
|
||||
String[] ldapUrl = new String[]{ldap.getUrl()};
|
||||
String[] ldapUrl = new String[]{apacheDsRule.getUrl()};
|
||||
String groupSearchBase = "o=sevenSeas";
|
||||
boolean isSubTreeSearch = true;
|
||||
String[] userTemplates = new String[]{
|
||||
|
@ -67,15 +56,8 @@ public class LdapConnectionTests extends ElasticsearchTestCase {
|
|||
buildLdapSettings(ldapUrl, userTemplates, groupSearchBase, isSubTreeSearch));
|
||||
|
||||
String user = "Horatio Hornblower";
|
||||
char[] userPass = "pass".toCharArray();
|
||||
|
||||
try {
|
||||
LdapConnection ldap = ldapFac.bind(user, userPass);
|
||||
fail("bindWithUserTemplates should have failed");
|
||||
} catch (LdapException le) {
|
||||
|
||||
}
|
||||
|
||||
SecuredString userPass = SecuredStringTests.build("pass");
|
||||
LdapConnection ldap = ldapFac.bind(user, userPass);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -85,10 +67,10 @@ public class LdapConnectionTests extends ElasticsearchTestCase {
|
|||
|
||||
boolean isSubTreeSearch = true;
|
||||
StandardLdapConnectionFactory ldapFac = new StandardLdapConnectionFactory(
|
||||
buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
|
||||
buildLdapSettings(apacheDsRule.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
|
||||
|
||||
String user = "Horatio Hornblower";
|
||||
char[] userPass = "pass".toCharArray();
|
||||
SecuredString userPass = SecuredStringTests.build("pass");
|
||||
|
||||
LdapConnection ldap = ldapFac.bind(user, userPass);
|
||||
List<String> groups = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
||||
|
@ -102,26 +84,13 @@ public class LdapConnectionTests extends ElasticsearchTestCase {
|
|||
String userTemplate = "cn={0},ou=people,o=sevenSeas";
|
||||
boolean isSubTreeSearch = false;
|
||||
StandardLdapConnectionFactory ldapFac = new StandardLdapConnectionFactory(
|
||||
buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
|
||||
buildLdapSettings(apacheDsRule.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
|
||||
|
||||
String user = "Horatio Hornblower";
|
||||
LdapConnection ldap = ldapFac.bind(user, "pass".toCharArray());
|
||||
LdapConnection ldap = ldapFac.bind(user, SecuredStringTests.build("pass"));
|
||||
|
||||
List<String> groups = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
||||
System.out.println("groups:"+groups);
|
||||
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
|
||||
}
|
||||
|
||||
public static Settings buildLdapSettings(String ldapUrl, String userTemplate, String groupSearchBase, boolean isSubTreeSearch) {
|
||||
return buildLdapSettings( new String[]{ldapUrl}, new String[]{userTemplate}, groupSearchBase, isSubTreeSearch );
|
||||
}
|
||||
|
||||
public static Settings buildLdapSettings(String[] ldapUrl, String[] userTemplate, String groupSearchBase, boolean isSubTreeSearch) {
|
||||
return ImmutableSettings.builder()
|
||||
.putArray(SETTINGS_PREFIX + StandardLdapConnectionFactory.URLS_SETTING, ldapUrl)
|
||||
.putArray(SETTINGS_PREFIX + StandardLdapConnectionFactory.USER_DN_TEMPLATES_SETTING, userTemplate)
|
||||
.put(SETTINGS_PREFIX + StandardLdapConnectionFactory.GROUP_SEARCH_BASEDN_SETTING, groupSearchBase)
|
||||
.put(SETTINGS_PREFIX + StandardLdapConnectionFactory.GROUP_SEARCH_SUBTREE_SETTING, isSubTreeSearch).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,13 +8,14 @@ package org.elasticsearch.shield.authc.ldap;
|
|||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
import org.elasticsearch.shield.authc.support.SecuredStringTests;
|
||||
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
|
@ -23,22 +24,26 @@ import static org.mockito.Matchers.any;
|
|||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class LdapRealmTest extends ElasticsearchTestCase {
|
||||
static ApacheDsEmbedded ldap = new ApacheDsEmbedded("o=sevenSeas", "seven-seas.ldif", LdapRealmTest.class.getName());
|
||||
public static String AD_IP = "54.213.145.20";
|
||||
public static String AD_URL = "ldap://" + AD_IP + ":389";
|
||||
public class LdapRealmTest extends LdapTest {
|
||||
|
||||
public static final String AD_IP = "54.213.145.20";
|
||||
public static final String AD_URL = "ldap://" + AD_IP + ":389";
|
||||
|
||||
public static final String VALID_USER_TEMPLATE = "cn={0},ou=people,o=sevenSeas";
|
||||
public static final String VALID_USERNAME = "Thomas Masterman Hardy";
|
||||
public static final String PASSWORD = "pass";
|
||||
|
||||
@BeforeClass
|
||||
public static void startServer() throws Exception {
|
||||
ldap.startServer();
|
||||
private RestController restController;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
restController = mock(RestController.class);
|
||||
}
|
||||
@AfterClass
|
||||
public static void stopServer() throws Exception {
|
||||
ldap.stopAndCleanup();
|
||||
|
||||
@Test
|
||||
public void testRestHeaderRegistration() {
|
||||
new LdapRealm(ImmutableSettings.EMPTY, mock(LdapConnectionFactory.class), mock(LdapGroupToRoleMapper.class), restController);
|
||||
verify(restController).registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -46,11 +51,11 @@ public class LdapRealmTest extends ElasticsearchTestCase {
|
|||
String groupSearchBase = "o=sevenSeas";
|
||||
boolean isSubTreeSearch = true;
|
||||
String userTemplate = VALID_USER_TEMPLATE;
|
||||
Settings settings = LdapConnectionTests.buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch);
|
||||
Settings settings = LdapConnectionTests.buildLdapSettings(apacheDsRule.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch);
|
||||
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(settings);
|
||||
LdapRealm ldap = new LdapRealm(buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
|
||||
LdapRealm ldap = new LdapRealm(buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper(), restController);
|
||||
|
||||
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
|
||||
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
|
||||
assertThat( user, notNullValue());
|
||||
assertThat(user.roles(), arrayContaining("HMS Victory"));
|
||||
}
|
||||
|
@ -61,31 +66,30 @@ public class LdapRealmTest extends ElasticsearchTestCase {
|
|||
boolean isSubTreeSearch = false;
|
||||
String userTemplate = VALID_USER_TEMPLATE;
|
||||
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
|
||||
LdapConnectionTests.buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
|
||||
LdapConnectionTests.buildLdapSettings(apacheDsRule.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
|
||||
|
||||
LdapRealm ldap = new LdapRealm(buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
|
||||
LdapRealm ldap = new LdapRealm(buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper(), restController);
|
||||
|
||||
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
|
||||
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
|
||||
assertThat( user, notNullValue());
|
||||
assertThat( user.roles(), arrayContaining("HMS Victory"));
|
||||
assertThat(user.roles(), arrayContaining("HMS Victory"));
|
||||
}
|
||||
|
||||
@Ignore //this is still failing. not sure why.
|
||||
@Test
|
||||
public void testAuthenticate_caching(){
|
||||
String groupSearchBase = "o=sevenSeas";
|
||||
boolean isSubTreeSearch = true;
|
||||
String userTemplate = VALID_USER_TEMPLATE;
|
||||
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
|
||||
LdapConnectionTests.buildLdapSettings( ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
|
||||
LdapConnectionTests.buildLdapSettings( apacheDsRule.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
|
||||
|
||||
ldapFactory = spy(ldapFactory);
|
||||
LdapRealm ldap = new LdapRealm( buildCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
|
||||
User user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
|
||||
user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
|
||||
LdapRealm ldap = new LdapRealm( buildCachingSettings(), ldapFactory, buildGroupAsRoleMapper(), restController);
|
||||
User user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
|
||||
user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
|
||||
|
||||
//verify one and only one bind -> caching is working
|
||||
verify(ldapFactory, times(1)).bind(anyString(), any(char[].class));
|
||||
verify(ldapFactory, times(1)).bind(anyString(), any(SecuredString.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -94,15 +98,15 @@ public class LdapRealmTest extends ElasticsearchTestCase {
|
|||
boolean isSubTreeSearch = true;
|
||||
String userTemplate = VALID_USER_TEMPLATE;
|
||||
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
|
||||
LdapConnectionTests.buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
|
||||
LdapConnectionTests.buildLdapSettings(apacheDsRule.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
|
||||
|
||||
ldapFactory = spy(ldapFactory);
|
||||
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
|
||||
User user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
|
||||
user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
|
||||
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper(), restController);
|
||||
User user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
|
||||
user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
|
||||
|
||||
//verify two and only two binds -> caching is disabled
|
||||
verify(ldapFactory, times(2)).bind(anyString(), any(char[].class));
|
||||
verify(ldapFactory, times(2)).bind(anyString(), any(SecuredString.class));
|
||||
}
|
||||
|
||||
@Ignore
|
||||
|
@ -114,9 +118,9 @@ public class LdapRealmTest extends ElasticsearchTestCase {
|
|||
ActiveDirectoryConnectionFactory ldapFactory = new ActiveDirectoryConnectionFactory(
|
||||
ActiveDirectoryFactoryTests.buildAdSettings(AD_URL, adDomain));
|
||||
|
||||
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
|
||||
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper(), restController);
|
||||
|
||||
User user = ldap.authenticate( new UsernamePasswordToken("george", "R))Tr0x".toCharArray()));
|
||||
User user = ldap.authenticate( new UsernamePasswordToken("george", SecuredStringTests.build("R))Tr0x")));
|
||||
|
||||
assertThat( user, notNullValue());
|
||||
assertThat( user.roles(), hasItemInArray("upchuckers"));
|
||||
|
@ -128,12 +132,12 @@ public class LdapRealmTest extends ElasticsearchTestCase {
|
|||
//only set the adDomain, and see if it infers the rest correctly
|
||||
String adDomain = AD_IP;
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put(LdapConnectionTests.SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.AD_DOMAIN_NAME_SETTING, adDomain)
|
||||
.put(SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.AD_DOMAIN_NAME_SETTING, adDomain)
|
||||
.build();
|
||||
|
||||
ActiveDirectoryConnectionFactory ldapFactory = new ActiveDirectoryConnectionFactory( settings );
|
||||
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
|
||||
User user = ldap.authenticate( new UsernamePasswordToken("george", "R))Tr0x".toCharArray()));
|
||||
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper(), restController);
|
||||
User user = ldap.authenticate( new UsernamePasswordToken("george", SecuredStringTests.build("R))Tr0x")));
|
||||
|
||||
assertThat( user, notNullValue());
|
||||
assertThat( user.roles(), hasItemInArray("upchuckers"));
|
||||
|
@ -149,8 +153,8 @@ public class LdapRealmTest extends ElasticsearchTestCase {
|
|||
|
||||
private Settings buildCachingSettings() {
|
||||
return ImmutableSettings.builder()
|
||||
.put("shield.authc.ldap."+LdapRealm.CACHE_TTL, 1)
|
||||
.put("shield.authc.ldap."+LdapRealm.CACHE_MAX_USERS, 10)
|
||||
.put("shield.authc.ldap."+LdapRealm.CACHE_TTL, 100000000)
|
||||
.put("shield.authc.ldap." + LdapRealm.CACHE_MAX_USERS, 10)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -162,6 +166,5 @@ public class LdapRealmTest extends ElasticsearchTestCase {
|
|||
return new LdapGroupToRoleMapper(settings,
|
||||
new Environment(settings),
|
||||
new ResourceWatcherService(settings, new ThreadPool("test")));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.shield.authc.ldap;
|
||||
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
@Ignore
|
||||
public abstract class LdapTest extends ElasticsearchTestCase {
|
||||
|
||||
static String SETTINGS_PREFIX = LdapRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.';
|
||||
|
||||
private static final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
protected static final ApacheDsRule apacheDsRule = new ApacheDsRule(temporaryFolder);
|
||||
|
||||
@ClassRule
|
||||
public static final RuleChain ruleChain = RuleChain.outerRule(temporaryFolder).around(apacheDsRule);
|
||||
|
||||
static Settings buildLdapSettings(String ldapUrl, String userTemplate, String groupSearchBase, boolean isSubTreeSearch) {
|
||||
return buildLdapSettings( new String[]{ldapUrl}, new String[]{userTemplate}, groupSearchBase, isSubTreeSearch );
|
||||
}
|
||||
|
||||
static Settings buildLdapSettings(String[] ldapUrl, String[] userTemplate, String groupSearchBase, boolean isSubTreeSearch) {
|
||||
return ImmutableSettings.builder()
|
||||
.putArray(SETTINGS_PREFIX + StandardLdapConnectionFactory.URLS_SETTING, ldapUrl)
|
||||
.putArray(SETTINGS_PREFIX + StandardLdapConnectionFactory.USER_DN_TEMPLATES_SETTING, userTemplate)
|
||||
.put(SETTINGS_PREFIX + StandardLdapConnectionFactory.GROUP_SEARCH_BASEDN_SETTING, groupSearchBase)
|
||||
.put(SETTINGS_PREFIX + StandardLdapConnectionFactory.GROUP_SEARCH_SUBTREE_SETTING, isSubTreeSearch).build();
|
||||
}
|
||||
}
|
|
@ -7,15 +7,16 @@ package org.elasticsearch.shield.authc.support;
|
|||
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.shield.User;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class CachingUsernamePasswordRealmTests {
|
||||
public static class AlwaysAuthenticateCachingRealm extends CachingUsernamePasswordRealm {
|
||||
public class CachingUsernamePasswordRealmTests extends ElasticsearchTestCase {
|
||||
|
||||
static class AlwaysAuthenticateCachingRealm extends CachingUsernamePasswordRealm {
|
||||
public AlwaysAuthenticateCachingRealm() {
|
||||
super(ImmutableSettings.EMPTY);
|
||||
}
|
||||
|
@ -28,11 +29,10 @@ public class CachingUsernamePasswordRealmTests {
|
|||
@Override public String type() { return "test"; }
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCache(){
|
||||
AlwaysAuthenticateCachingRealm realm = new AlwaysAuthenticateCachingRealm();
|
||||
char[] pass = "pass".toCharArray();
|
||||
SecuredString pass = SecuredStringTests.build("pass");
|
||||
realm.authenticate(new UsernamePasswordToken("a", pass));
|
||||
realm.authenticate(new UsernamePasswordToken("b", pass));
|
||||
realm.authenticate(new UsernamePasswordToken("c", pass));
|
||||
|
@ -50,8 +50,8 @@ public class CachingUsernamePasswordRealmTests {
|
|||
AlwaysAuthenticateCachingRealm realm = new AlwaysAuthenticateCachingRealm();
|
||||
|
||||
String user = "testUser";
|
||||
char[] pass1 = "pass".toCharArray();
|
||||
char[] pass2 = "password".toCharArray();
|
||||
SecuredString pass1 = SecuredStringTests.build("pass");
|
||||
SecuredString pass2 = SecuredStringTests.build("password");
|
||||
|
||||
realm.authenticate(new UsernamePasswordToken(user, pass1));
|
||||
realm.authenticate(new UsernamePasswordToken(user, pass1));
|
||||
|
|
|
@ -5,19 +5,18 @@
|
|||
*/
|
||||
package org.elasticsearch.shield.authc.support;
|
||||
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class HasherTests {
|
||||
public class HasherTests extends ElasticsearchTestCase {
|
||||
|
||||
@Test
|
||||
public void testHtpasswdToolGenerated() throws Exception {
|
||||
Hasher hasher = Hasher.HTPASSWD;
|
||||
char[] passwd = "test123".toCharArray();
|
||||
SecuredString passwd = SecuredStringTests.build("test123");
|
||||
assertTrue(hasher.verify(passwd, "$2a$05$zxnP0vdREMxnEpkLCDI2OuSaSk/QEKA2.A42iOpI6U2u.RLLOWm1e".toCharArray()));
|
||||
assertTrue(hasher.verify(passwd, "$2a$10$FMhmFjwU5.qxQ/BsEciS9OqcJVkFMgXMo4uH5CelOR1j4N9zIv67e".toCharArray()));
|
||||
assertTrue(hasher.verify(passwd, "$apr1$R3DdqiAZ$aljIkaIVPSarmDMlJUBBP.".toCharArray()));
|
||||
|
@ -29,7 +28,7 @@ public class HasherTests {
|
|||
@Test
|
||||
public void testHtpasswdSelfGenerated() throws Exception {
|
||||
Hasher hasher = Hasher.HTPASSWD;
|
||||
char[] passwd = "test123".toCharArray();
|
||||
SecuredString passwd = SecuredStringTests.build("test123");
|
||||
assertTrue(hasher.verify(passwd, hasher.hash(passwd)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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.support;
|
||||
|
||||
import com.carrotsearch.ant.tasks.junit4.dependencies.com.google.common.base.Charsets;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
public class SecuredStringTests {
|
||||
public static SecuredString build(String password){
|
||||
return new SecuredString(password.toCharArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessAfterClear(){
|
||||
SecuredString password = new SecuredString("password".toCharArray());
|
||||
SecuredString password2 = new SecuredString("password".toCharArray());
|
||||
|
||||
password.clear();
|
||||
|
||||
try {
|
||||
password.internalChars();
|
||||
fail();
|
||||
} catch(Exception e){}
|
||||
|
||||
try {
|
||||
password.length();
|
||||
fail();
|
||||
} catch(Exception e){}
|
||||
|
||||
try {
|
||||
password.charAt(0);
|
||||
fail();
|
||||
} catch(Exception e){}
|
||||
|
||||
try {
|
||||
password.concat("_suffix");
|
||||
fail();
|
||||
} catch(Exception e){}
|
||||
|
||||
assertNotEquals(password, password2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsHashCode(){
|
||||
SecuredString password = new SecuredString("password".toCharArray());
|
||||
SecuredString password2 = new SecuredString("password".toCharArray());
|
||||
|
||||
assertEquals(password, password2);
|
||||
assertEquals(password.hashCode(), password2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testsEqualsCharSequence(){
|
||||
SecuredString password = new SecuredString("password".toCharArray());
|
||||
StringBuffer password2 = new StringBuffer("password");
|
||||
String password3 = "password";
|
||||
|
||||
assertEquals(password, password2);
|
||||
assertEquals(password, password3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcat() {
|
||||
SecuredString password = new SecuredString("password".toCharArray());
|
||||
SecuredString password2 = new SecuredString("password".toCharArray());
|
||||
|
||||
SecuredString password3 = password.concat(password2);
|
||||
assertThat(password3.length(), equalTo(password.length() + password2.length()));
|
||||
assertThat(password3.internalChars(), equalTo("passwordpassword".toCharArray()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubsequence(){
|
||||
SecuredString password = new SecuredString("password".toCharArray());
|
||||
SecuredString password2 = password.subSequence(4, 8);
|
||||
SecuredString password3 = password.subSequence(0, 4);
|
||||
|
||||
assertThat(password2.internalChars(), equalTo("word".toCharArray()));
|
||||
assertThat(password3.internalChars(), equalTo("pass".toCharArray()));
|
||||
assertThat("ensure original is unmodified", password.internalChars(), equalTo("password".toCharArray()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUFT8(){
|
||||
String password = "эластичный поиск-弾性検索";
|
||||
SecuredString securePass = new SecuredString(password.toCharArray());
|
||||
byte[] utf8 = securePass.utf8Bytes();
|
||||
String password2 = new String(utf8, Charsets.UTF_8);
|
||||
assertThat(password2, equalTo(password));
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ public class UsernamePasswordTokenTests extends ElasticsearchTestCase {
|
|||
@Test
|
||||
public void testPutToken() throws Exception {
|
||||
TransportRequest request = new TransportRequest() {};
|
||||
UsernamePasswordToken.putTokenHeader(request, new UsernamePasswordToken("user1", "test123".toCharArray()));
|
||||
UsernamePasswordToken.putTokenHeader(request, new UsernamePasswordToken("user1", SecuredStringTests.build("test123")));
|
||||
String header = request.getHeader(UsernamePasswordToken.BASIC_AUTH_HEADER);
|
||||
assertThat(header, notNullValue());
|
||||
assertTrue(header.startsWith("Basic "));
|
||||
|
@ -53,7 +53,7 @@ public class UsernamePasswordTokenTests extends ElasticsearchTestCase {
|
|||
UsernamePasswordToken token = UsernamePasswordToken.extractToken(request, null);
|
||||
assertThat(token, notNullValue());
|
||||
assertThat(token.principal(), equalTo("user1"));
|
||||
assertThat(new String(token.credentials()), equalTo("test123"));
|
||||
assertThat(new String(token.credentials().internalChars()), equalTo("test123"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -87,8 +87,8 @@ public class UsernamePasswordTokenTests extends ElasticsearchTestCase {
|
|||
@Test
|
||||
public void testExtractTokenRest() throws Exception {
|
||||
RestRequest request = mock(RestRequest.class);
|
||||
UsernamePasswordToken token = new UsernamePasswordToken("username", "changeme".toCharArray());
|
||||
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn(UsernamePasswordToken.basicAuthHeaderValue("username", "changeme".toCharArray()));
|
||||
UsernamePasswordToken token = new UsernamePasswordToken("username", SecuredStringTests.build("changeme"));
|
||||
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn(UsernamePasswordToken.basicAuthHeaderValue("username", SecuredStringTests.build("changeme")));
|
||||
assertThat(UsernamePasswordToken.extractToken(request, null), equalTo(token));
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ public class FileRolesStoreTests extends ElasticsearchTestCase {
|
|||
@Test
|
||||
public void testParseFile() throws Exception {
|
||||
Path path = Paths.get(getClass().getResource("roles.yml").toURI());
|
||||
Map<String, Permission.Global> roles = FileRolesStore.parseFile(path);
|
||||
Map<String, Permission.Global> roles = FileRolesStore.parseFile(path, logger);
|
||||
assertThat(roles, notNullValue());
|
||||
assertThat(roles.size(), is(3));
|
||||
|
||||
|
@ -155,7 +155,7 @@ public class FileRolesStoreTests extends ElasticsearchTestCase {
|
|||
public void testThatEmptyFileDoesNotResultInLoop() throws Exception {
|
||||
File file = tempFolder.newFile();
|
||||
com.google.common.io.Files.write("#".getBytes(Charsets.UTF_8), file);
|
||||
Map<String, Permission.Global> roles = FileRolesStore.parseFile(file.toPath());
|
||||
Map<String, Permission.Global> roles = FileRolesStore.parseFile(file.toPath(), logger);
|
||||
assertThat(roles.keySet(), is(empty()));
|
||||
}
|
||||
|
||||
|
@ -163,6 +163,6 @@ public class FileRolesStoreTests extends ElasticsearchTestCase {
|
|||
public void testThatInvalidYAMLThrowsElasticsearchException() throws Exception {
|
||||
File file = tempFolder.newFile();
|
||||
com.google.common.io.Files.write("user: cluster: ALL indices: '.*': ALL".getBytes(Charsets.UTF_8), file);
|
||||
FileRolesStore.parseFile(file.toPath());
|
||||
FileRolesStore.parseFile(file.toPath(), logger);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.threadpool.ThreadPool;
|
||||
import org.elasticsearch.watcher.ResourceWatcherService;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class InternalKeyServiceTests extends ElasticsearchTestCase {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
private ResourceWatcherService watcherService;
|
||||
private Settings settings;
|
||||
private Environment env;
|
||||
private File keyFile;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
keyFile = writeFile("system_key", InternalKeyService.generateKey());
|
||||
settings = ImmutableSettings.builder()
|
||||
.put("shield.system_key.file", keyFile.getAbsolutePath())
|
||||
.put("watcher.interval.high", "2s")
|
||||
.build();
|
||||
env = new Environment(settings);
|
||||
ThreadPool threadPool = new ThreadPool("test");
|
||||
watcherService = new ResourceWatcherService(settings, threadPool);
|
||||
watcherService.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSigned() throws Exception {
|
||||
InternalKeyService service = new InternalKeyService(settings, env, watcherService);
|
||||
String text = randomAsciiOfLength(10);
|
||||
String signed = service.sign(text);
|
||||
assertThat(service.signed(signed), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignAndUnsign() throws Exception {
|
||||
InternalKeyService service = new InternalKeyService(settings, env, watcherService);
|
||||
String text = randomAsciiOfLength(10);
|
||||
String signed = service.sign(text);
|
||||
assertThat(text.equals(signed), is(false));
|
||||
String text2 = service.unsignAndVerify(signed);
|
||||
assertThat(text, equalTo(text2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignAndUnsign_NoKeyFile() throws Exception {
|
||||
InternalKeyService service = new InternalKeyService(ImmutableSettings.EMPTY, env, watcherService);
|
||||
String text = randomAsciiOfLength(10);
|
||||
String signed = service.sign(text);
|
||||
assertThat(text, equalTo(signed));
|
||||
text = service.unsignAndVerify(signed);
|
||||
assertThat(text, equalTo(signed));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReloadKey() throws Exception {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
InternalKeyService service = new InternalKeyService(settings, env, watcherService, new InternalKeyService.Listener() {
|
||||
@Override
|
||||
public void onKeyRefresh() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
String text = randomAsciiOfLength(10);
|
||||
String signed = service.sign(text);
|
||||
|
||||
// we need to sleep so to ensure the timestamp of the file will definitely change
|
||||
// and so the resource watcher will pick up the change.
|
||||
sleep(1000);
|
||||
|
||||
Streams.copy(InternalKeyService.generateKey(), keyFile);
|
||||
if (!latch.await(10, TimeUnit.SECONDS)) {
|
||||
fail("waiting too long for test to complete. Expected callback is not called");
|
||||
}
|
||||
String signed2 = service.sign(text);
|
||||
assertThat(signed.equals(signed2), is(false));
|
||||
}
|
||||
|
||||
private File writeFile(String name, byte[] content) throws IOException {
|
||||
File file = tempFolder.newFile(name);
|
||||
Streams.copy(content, file);
|
||||
return file;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.key.tool;
|
||||
|
||||
import org.elasticsearch.common.cli.CliTool;
|
||||
import org.elasticsearch.common.cli.CliToolTestCase;
|
||||
import org.elasticsearch.common.cli.Terminal;
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.shield.key.InternalKeyService;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.elasticsearch.shield.key.tool.SystemKeyTool.Generate;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class SystemKeyToolTests extends CliToolTestCase {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
private Terminal terminal;
|
||||
private Environment env;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
terminal = mock(Terminal.class);
|
||||
env = mock(Environment.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParse_NoArgs() throws Exception {
|
||||
CliTool.Command cmd = new SystemKeyTool().parse("generate", args(""));
|
||||
assertThat(cmd, instanceOf(Generate.class));
|
||||
Generate generate = (Generate) cmd;
|
||||
assertThat(generate.path, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParse_FileArg() throws Exception {
|
||||
Path path = tempFolder.newFile().toPath();
|
||||
CliTool.Command cmd = new SystemKeyTool().parse("generate", args(path.toAbsolutePath().toString()));
|
||||
assertThat(cmd, instanceOf(Generate.class));
|
||||
Generate generate = (Generate) cmd;
|
||||
assertThat(Files.isSameFile(generate.path, path), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerate() throws Exception {
|
||||
Path path = tempFolder.newFile().toPath();
|
||||
Generate generate = new Generate(terminal, path);
|
||||
CliTool.ExitStatus status = generate.execute(ImmutableSettings.EMPTY, env);
|
||||
assertThat(status, is(CliTool.ExitStatus.OK));
|
||||
byte[] bytes = Streams.copyToByteArray(path.toFile());
|
||||
assertThat(bytes.length, is(InternalKeyService.KEY_SIZE / 8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerate_PathInSettings() throws Exception {
|
||||
Path path = tempFolder.newFile().toPath();
|
||||
Settings settings = ImmutableSettings.builder()
|
||||
.put("shield.system_key.file", path.toAbsolutePath().toString())
|
||||
.build();
|
||||
Generate generate = new Generate(terminal, null);
|
||||
CliTool.ExitStatus status = generate.execute(settings, env);
|
||||
assertThat(status, is(CliTool.ExitStatus.OK));
|
||||
byte[] bytes = Streams.copyToByteArray(path.toFile());
|
||||
assertThat(bytes.length, is(InternalKeyService.KEY_SIZE / 8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerate_DefaultPath() throws Exception {
|
||||
File config = tempFolder.newFolder();
|
||||
File shieldConfig = new File(config, ShieldPlugin.NAME);
|
||||
shieldConfig.mkdirs();
|
||||
Path path = new File(shieldConfig, ".system_key").toPath();
|
||||
when(env.configFile()).thenReturn(config);
|
||||
Generate generate = new Generate(terminal, null);
|
||||
CliTool.ExitStatus status = generate.execute(ImmutableSettings.EMPTY, env);
|
||||
assertThat(status, is(CliTool.ExitStatus.OK));
|
||||
byte[] bytes = Streams.copyToByteArray(path.toFile());
|
||||
assertThat(bytes.length, is(InternalKeyService.KEY_SIZE / 8));
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ public class ShieldPluginTests extends ShieldIntegrationTest {
|
|||
logger.info("--> Checking nodes info that shield plugin is loaded");
|
||||
for (NodeInfo nodeInfo : nodeInfos.getNodes()) {
|
||||
assertThat(nodeInfo.getPlugins().getInfos(), hasSize(1));
|
||||
assertThat(nodeInfo.getPlugins().getInfos().get(0).getName(), is(SecurityPlugin.NAME));
|
||||
assertThat(nodeInfo.getPlugins().getInfos().get(0).getName(), is(ShieldPlugin.NAME));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,17 +6,17 @@
|
|||
package org.elasticsearch.shield.test;
|
||||
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.shield.SecurityException;
|
||||
import org.elasticsearch.shield.ShieldException;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
public class ShieldAssertions {
|
||||
|
||||
public static void assertContainsWWWAuthenticateHeader(org.elasticsearch.shield.SecurityException e) {
|
||||
public static void assertContainsWWWAuthenticateHeader(ShieldException e) {
|
||||
assertThat(e.status(), is(RestStatus.UNAUTHORIZED));
|
||||
assertThat(e.getHeaders(), hasKey("WWW-Authenticate"));
|
||||
assertThat(e.getHeaders().get("WWW-Authenticate"), hasSize(1));
|
||||
assertThat(e.getHeaders().get("WWW-Authenticate").get(0), is(SecurityException.HEADERS.get("WWW-Authenticate").get(0)));
|
||||
assertThat(e.getHeaders().get("WWW-Authenticate").get(0), is(ShieldException.HEADERS.get("WWW-Authenticate").get(0)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,15 @@ import com.google.common.base.Charsets;
|
|||
import com.google.common.net.InetAddresses;
|
||||
import org.apache.lucene.util.AbstractRandomizedTest;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.os.OsUtils;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.InetSocketTransportAddress;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.plugins.PluginsService;
|
||||
import org.elasticsearch.shield.plugin.SecurityPlugin;
|
||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.shield.transport.netty.NettySecuredTransport;
|
||||
import org.elasticsearch.test.ElasticsearchIntegrationTest;
|
||||
import org.elasticsearch.transport.Transport;
|
||||
|
@ -61,11 +63,11 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
|
|||
File folder = newFolder();
|
||||
|
||||
ImmutableSettings.Builder builder = ImmutableSettings.builder()
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(getClientUsername(), getClientPassword().toCharArray()))
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(getClientUsername(), getClientPassword()))
|
||||
.put("discovery.zen.ping.multicast.enabled", false)
|
||||
.put("discovery.type", "zen")
|
||||
.put("node.mode", "network")
|
||||
.put("plugin.types", SecurityPlugin.class.getName())
|
||||
.put("plugin.types", ShieldPlugin.class.getName())
|
||||
.put("shield.authc.esusers.files.users", writeFile(folder, "users", CONFIG_STANDARD_USER))
|
||||
.put("shield.authc.esusers.files.users_roles", writeFile(folder, "users_roles", CONFIG_STANDARD_USER_ROLES))
|
||||
.put("shield.authz.store.files.roles", writeFile(folder, "roles.yml", CONFIG_ROLE_ALLOW_ALL))
|
||||
|
@ -84,7 +86,7 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
|
|||
@Override
|
||||
protected Settings transportClientSettings() {
|
||||
return ImmutableSettings.builder()
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(getClientUsername(), getClientPassword().toCharArray()))
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(getClientUsername(), getClientPassword()))
|
||||
.put(TransportModule.TRANSPORT_TYPE_KEY, NettySecuredTransport.class.getName())
|
||||
.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false)
|
||||
.put("node.mode", "network")
|
||||
|
@ -93,9 +95,13 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
|
|||
}
|
||||
|
||||
protected String writeFile(File folder, String name, String content) {
|
||||
return writeFile(folder, name, content.getBytes(Charsets.UTF_8));
|
||||
}
|
||||
|
||||
protected String writeFile(File folder, String name, byte[] content) {
|
||||
Path file = folder.toPath().resolve(name);
|
||||
try {
|
||||
com.google.common.io.Files.write(content.getBytes(Charsets.UTF_8), file.toFile());
|
||||
Streams.copy(content, file.toFile());
|
||||
} catch (IOException e) {
|
||||
throw new ElasticsearchException("Error writing file in test", e);
|
||||
}
|
||||
|
@ -113,8 +119,8 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
|
|||
return DEFAULT_USER_NAME;
|
||||
}
|
||||
|
||||
protected String getClientPassword() {
|
||||
return DEFAULT_PASSWORD;
|
||||
protected SecuredString getClientPassword() {
|
||||
return new SecuredString(DEFAULT_PASSWORD.toCharArray());
|
||||
}
|
||||
|
||||
protected Settings getSSLSettingsForStore(String resourcePathToStore, String password) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.elasticsearch.common.settings.Settings;
|
|||
import org.elasticsearch.common.transport.InetSocketTransportAddress;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.http.HttpServerTransport;
|
||||
import org.elasticsearch.node.internal.InternalNode;
|
||||
import org.elasticsearch.shield.test.ShieldIntegrationTest;
|
||||
import org.elasticsearch.transport.Transport;
|
||||
import org.junit.Test;
|
||||
|
@ -37,6 +38,7 @@ public class IpFilteringIntegrationTests extends ShieldIntegrationTest {
|
|||
@Override
|
||||
protected Settings nodeSettings(int nodeOrdinal) {
|
||||
ImmutableSettings.Builder builder = settingsBuilder().put(super.nodeSettings(nodeOrdinal));
|
||||
builder.put(InternalNode.HTTP_ENABLED, true);
|
||||
// either deny all or do not have a configuration file, as this denies by default
|
||||
if (getRandom().nextBoolean()) {
|
||||
File folder = newFolder();
|
||||
|
@ -59,7 +61,7 @@ public class IpFilteringIntegrationTests extends ShieldIntegrationTest {
|
|||
|
||||
@Test(expected = SocketException.class)
|
||||
public void testThatIpFilteringIsIntegratedIntoNettyPipelineViaTransportClient() throws Exception {
|
||||
TransportAddress transportAddress = (InetSocketTransportAddress) internalCluster().getDataNodeInstance(Transport.class).boundAddress().boundAddress();
|
||||
TransportAddress transportAddress = internalCluster().getDataNodeInstance(Transport.class).boundAddress().boundAddress();
|
||||
assertThat(transportAddress, is(instanceOf(InetSocketTransportAddress.class)));
|
||||
InetSocketTransportAddress inetSocketTransportAddress = (InetSocketTransportAddress) transportAddress;
|
||||
trySocketConnection(inetSocketTransportAddress.address());
|
||||
|
|
|
@ -13,12 +13,14 @@ import org.elasticsearch.client.transport.NoNodeAvailableException;
|
|||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.net.InetAddresses;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.InetSocketTransportAddress;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.http.HttpServerTransport;
|
||||
import org.elasticsearch.node.Node;
|
||||
import org.elasticsearch.node.NodeBuilder;
|
||||
import org.elasticsearch.node.internal.InternalNode;
|
||||
import org.elasticsearch.plugins.PluginsService;
|
||||
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.shield.test.ShieldIntegrationTest;
|
||||
|
@ -40,6 +42,12 @@ import static org.hamcrest.Matchers.*;
|
|||
|
||||
public class SslIntegrationTests extends ShieldIntegrationTest {
|
||||
|
||||
@Override
|
||||
protected Settings nodeSettings(int nodeOrdinal) {
|
||||
return ImmutableSettings.builder().put(super.nodeSettings(nodeOrdinal))
|
||||
.put(InternalNode.HTTP_ENABLED, true).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThatInternallyCreatedTransportClientCanConnect() throws Exception {
|
||||
Client transportClient = internalCluster().transportClient();
|
||||
|
@ -89,7 +97,7 @@ public class SslIntegrationTests extends ShieldIntegrationTest {
|
|||
.put("name", "programmatic_node")
|
||||
.put("cluster.name", internalCluster().getClusterName())
|
||||
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(getClientUsername(), getClientPassword().toCharArray()))
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(getClientUsername(), getClientPassword()))
|
||||
.put(TransportModule.TRANSPORT_TYPE_KEY, NettySecuredTransport.class.getName())
|
||||
.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false)
|
||||
|
||||
|
@ -114,7 +122,7 @@ public class SslIntegrationTests extends ShieldIntegrationTest {
|
|||
.put("discovery.type", "zen")
|
||||
.putArray("discovery.zen.ping.unicast.hosts", getUnicastHostAddress())
|
||||
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(getClientUsername(), getClientPassword().toCharArray()))
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(getClientUsername(), getClientPassword()))
|
||||
.put(TransportModule.TRANSPORT_TYPE_KEY, NettySecuredTransport.class.getName())
|
||||
.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false)
|
||||
.put("shield.transport.n2n.ip_filter.file", writeFile(newFolder(), "ip_filter.yml", ShieldIntegrationTest.CONFIG_IPFILTER_ALLOW_ALL))
|
||||
|
@ -165,7 +173,7 @@ public class SslIntegrationTests extends ShieldIntegrationTest {
|
|||
String url = String.format(Locale.ROOT, "https://%s:%s/", InetAddresses.toUriString(inetSocketTransportAddress.address().getAddress()), inetSocketTransportAddress.address().getPort());
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestProperty("Authorization", UsernamePasswordToken.basicAuthHeaderValue(DEFAULT_USER_NAME, DEFAULT_PASSWORD.toCharArray()));
|
||||
connection.setRequestProperty("Authorization", UsernamePasswordToken.basicAuthHeaderValue(getClientUsername(), getClientPassword()));
|
||||
connection.connect();
|
||||
|
||||
assertThat(connection.getResponseCode(), is(200));
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.elasticsearch.common.settings.Settings;
|
|||
import org.elasticsearch.common.transport.InetSocketTransportAddress;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.http.HttpServerTransport;
|
||||
import org.elasticsearch.node.internal.InternalNode;
|
||||
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
|
||||
import org.elasticsearch.shield.test.ShieldIntegrationTest;
|
||||
import org.junit.Test;
|
||||
|
@ -49,6 +50,7 @@ public class SslRequireAuthTests extends ShieldIntegrationTest {
|
|||
protected Settings nodeSettings(int nodeOrdinal) {
|
||||
return settingsBuilder()
|
||||
.put(super.nodeSettings(nodeOrdinal))
|
||||
.put(InternalNode.HTTP_ENABLED, true)
|
||||
.put(getSSLSettingsForStore("certs/simple/testnode.jks", "testnode"))
|
||||
.put("shield.transport.ssl.require.client.auth", true)
|
||||
.put("shield.http.ssl.require.client.auth", true)
|
||||
|
@ -128,7 +130,7 @@ public class SslRequireAuthTests extends ShieldIntegrationTest {
|
|||
String url = String.format(Locale.ROOT, "https://%s:%s/", InetAddresses.toUriString(inetSocketTransportAddress.address().getAddress()), inetSocketTransportAddress.address().getPort());
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestProperty("Authorization", UsernamePasswordToken.basicAuthHeaderValue(DEFAULT_USER_NAME, DEFAULT_PASSWORD.toCharArray()));
|
||||
connection.setRequestProperty("Authorization", UsernamePasswordToken.basicAuthHeaderValue(getClientUsername(), getClientPassword()));
|
||||
connection.connect();
|
||||
|
||||
assertThat(connection.getResponseCode(), is(200));
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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.test;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.SysGlobals;
|
||||
import com.carrotsearch.randomizedtesting.annotations.Name;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.os.OsUtils;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.plugins.PluginsService;
|
||||
import org.elasticsearch.shield.authc.support.SecuredStringTests;
|
||||
import org.elasticsearch.shield.key.InternalKeyService;
|
||||
import org.elasticsearch.shield.plugin.ShieldPlugin;
|
||||
import org.elasticsearch.shield.transport.netty.NettySecuredTransport;
|
||||
import org.elasticsearch.test.rest.ElasticsearchRestTests;
|
||||
import org.elasticsearch.test.rest.RestTestCandidate;
|
||||
import org.elasticsearch.transport.TransportModule;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ShieldRestTests extends ElasticsearchRestTests {
|
||||
|
||||
public static final int CHILD_JVM_ID = Integer.parseInt(System.getProperty(SysGlobals.CHILDVM_SYSPROP_JVM_ID, "0"));
|
||||
public static final int BASE_PORT = 33000 + CHILD_JVM_ID * 100;
|
||||
public static final String BASE_PORT_RANGE = BASE_PORT + "-" + (BASE_PORT+10) ;
|
||||
|
||||
protected static final boolean ENABLE_TRANSPORT_SSL = true;
|
||||
protected static final boolean SHIELD_AUDIT_ENABLED = false;
|
||||
|
||||
protected static final String DEFAULT_USER_NAME = "test_user";
|
||||
protected static final String DEFAULT_PASSWORD = "changeme";
|
||||
protected static final String DEFAULT_ROLE = "user";
|
||||
|
||||
public static final String CONFIG_IPFILTER_ALLOW_ALL = "allow: all\n";
|
||||
public static final String CONFIG_STANDARD_USER = DEFAULT_USER_NAME + ":{plain}" + DEFAULT_PASSWORD + "\n";
|
||||
public static final String CONFIG_STANDARD_USER_ROLES = DEFAULT_USER_NAME + ":" + DEFAULT_ROLE + "\n";
|
||||
public static final String CONFIG_ROLE_ALLOW_ALL = "user:\n" +
|
||||
" cluster: ALL\n" +
|
||||
" indices:\n" +
|
||||
" '.*': ALL\n";
|
||||
|
||||
static {
|
||||
final byte[] key;
|
||||
try {
|
||||
key = InternalKeyService.generateKey();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
InternalTestCluster.DEFAULT_SETTINGS_SOURCE = new SettingsSource() {
|
||||
|
||||
@Override
|
||||
public Settings node(int nodeOrdinal) {
|
||||
File store;
|
||||
try {
|
||||
store = new File(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/testnode.jks").toURI());
|
||||
assertThat(store.exists(), is(true));
|
||||
} catch (Exception e) {
|
||||
throw new ElasticsearchException("Error reading test node cert", e);
|
||||
}
|
||||
String password = "testnode";
|
||||
|
||||
File folder = createFolder();
|
||||
|
||||
String keyFile = writeFile(folder, "system_key", key);
|
||||
|
||||
ImmutableSettings.Builder builder = ImmutableSettings.builder()
|
||||
.put(InternalKeyService.FILE_SETTING, keyFile)
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(DEFAULT_USER_NAME, SecuredStringTests.build(DEFAULT_PASSWORD)))
|
||||
.put("discovery.zen.ping.multicast.enabled", false)
|
||||
.put("discovery.type", "zen")
|
||||
.put("node.mode", "network")
|
||||
.put("plugin.types", ShieldPlugin.class.getName())
|
||||
.put("shield.authc.esusers.files.users", createFile(folder, "users", CONFIG_STANDARD_USER))
|
||||
.put("shield.authc.esusers.files.users_roles", createFile(folder, "users_roles", CONFIG_STANDARD_USER_ROLES))
|
||||
.put("shield.authz.store.files.roles", createFile(folder, "roles.yml", CONFIG_ROLE_ALLOW_ALL))
|
||||
.put("shield.transport.n2n.ip_filter.file", createFile(folder, "ip_filter.yml", CONFIG_IPFILTER_ALLOW_ALL))
|
||||
.put("shield.transport.ssl", ENABLE_TRANSPORT_SSL)
|
||||
.put("shield.transport.ssl.keystore", store.getPath())
|
||||
.put("shield.transport.ssl.keystore_password", password)
|
||||
.put("shield.transport.ssl.truststore", store.getPath())
|
||||
.put("shield.transport.ssl.truststore_password", password)
|
||||
.put("shield.http.ssl", false)
|
||||
.put("transport.tcp.port", BASE_PORT_RANGE)
|
||||
.putArray("discovery.zen.ping.unicast.hosts", "127.0.0.1:" + BASE_PORT, "127.0.0.1:" + (BASE_PORT + 1), "127.0.0.1:" + (BASE_PORT + 2), "127.0.0.1:" + (BASE_PORT + 3))
|
||||
.put("shield.audit.enabled", SHIELD_AUDIT_ENABLED);
|
||||
|
||||
builder.put("network.host", "127.0.0.1");
|
||||
if (OsUtils.MAC) {
|
||||
builder.put("network.host", randomBoolean() ? "127.0.0.1" : "::1");
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings transportClient() {
|
||||
File store;
|
||||
String password = "testclient";
|
||||
try {
|
||||
store = new File(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient.jks").toURI());
|
||||
assertThat(store.exists(), is(true));
|
||||
} catch (Exception e) {
|
||||
throw new ElasticsearchException("Error reading test client cert", e);
|
||||
}
|
||||
|
||||
File folder = createFolder();
|
||||
|
||||
return ImmutableSettings.builder()
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(DEFAULT_USER_NAME, SecuredStringTests.build(DEFAULT_PASSWORD)))
|
||||
.put(TransportModule.TRANSPORT_TYPE_KEY, NettySecuredTransport.class.getName())
|
||||
.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false)
|
||||
.put("node.mode", "network")
|
||||
.put("shield.transport.n2n.ip_filter.file", createFile(folder, "ip_filter.yml", CONFIG_IPFILTER_ALLOW_ALL))
|
||||
.put("shield.transport.ssl", ENABLE_TRANSPORT_SSL)
|
||||
.put("shield.transport.ssl.keystore", store.getPath())
|
||||
.put("shield.transport.ssl.keystore_password", password)
|
||||
.put("shield.transport.ssl.truststore", store.getPath())
|
||||
.put("shield.transport.ssl.truststore_password", password)
|
||||
.put("cluster.name", internalCluster().getClusterName())
|
||||
.build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ClassRule
|
||||
public static TemporaryFolder tmpFolder = new TemporaryFolder();
|
||||
|
||||
@AfterClass
|
||||
public static void cleanup() {
|
||||
tmpFolder = null;
|
||||
}
|
||||
|
||||
public ShieldRestTests(@Name("yaml") RestTestCandidate testCandidate) {
|
||||
super(testCandidate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Settings restClientSettings() {
|
||||
return ImmutableSettings.builder()
|
||||
.put("request.headers.Authorization", basicAuthHeaderValue(DEFAULT_USER_NAME, SecuredStringTests.build(DEFAULT_PASSWORD))).build();
|
||||
}
|
||||
|
||||
/* static helper methods for the global test class */
|
||||
static File createFolder() {
|
||||
try {
|
||||
return tmpFolder.newFolder();
|
||||
} catch (IOException ioe) {
|
||||
fail("could not create temporary folder");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String createFile(File folder, String name, String content) {
|
||||
return writeFile(folder, name, content.getBytes(Charsets.UTF_8));
|
||||
}
|
||||
|
||||
static String writeFile(File folder, String name, byte[] content) {
|
||||
Path file = folder.toPath().resolve(name);
|
||||
try {
|
||||
Streams.copy(content, file.toFile());
|
||||
} catch (IOException e) {
|
||||
throw new ElasticsearchException("Error writing file in test", e);
|
||||
}
|
||||
return file.toFile().getAbsolutePath();
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ indices:admin/cache/clear
|
|||
indices:admin/close
|
||||
indices:admin/create
|
||||
indices:admin/delete
|
||||
indices:admin/get
|
||||
indices:admin/exists
|
||||
indices:admin/flush
|
||||
indices:admin/mapping/delete
|
||||
|
|
Loading…
Reference in New Issue