Added user authentication on rest requests

The authc service will now authenticate the user on the rest layer as well, meaning there will only be a single authentication process no matter what is then entry point to ES (for example, if a rest handler executes two internal requests... like some of the _cat APIs, there'll still be a single authentication process)

 In addition, the audit logs will now log REST authentication failures such that the remote address and the rest endpoint will show up in the logs as well.

Original commit: elastic/x-pack-elasticsearch@07af440147
This commit is contained in:
uboness 2014-09-30 00:35:38 +02:00
parent bd38b5237c
commit 637a9e773c
10 changed files with 276 additions and 24 deletions

View File

@ -51,7 +51,7 @@ public class SecurityFilter extends AbstractComponent {
this.auditTrail = auditTrail;
}
User 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
@ -63,6 +63,11 @@ public class SecurityFilter extends AbstractComponent {
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 {
@ -124,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);
}
}
@ -140,7 +145,7 @@ public class SecurityFilter extends AbstractComponent {
@Override
public void inboundRequest(String action, TransportRequest request) {
filter.process(action, request);
filter.authenticateAndAuthorize(action, request);
}
}
@ -156,7 +161,7 @@ public class SecurityFilter extends AbstractComponent {
@Override
public void apply(String action, ActionRequest request, ActionListener listener, ActionFilterChain chain) {
try {
User user = filter.process(action, request);
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) {

View File

@ -5,6 +5,7 @@
*/
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;
@ -32,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) {
}
@ -55,8 +64,12 @@ 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);

View File

@ -8,6 +8,7 @@ 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;
@ -54,6 +55,20 @@ public class AuditTrailService extends AbstractComponent implements AuditTrail {
}
}
@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 (AuditTrail auditTrail : auditTrails) {

View File

@ -9,6 +9,7 @@ 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;
@ -56,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()) {
@ -63,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()) {

View File

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

View File

@ -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");
@ -120,6 +121,29 @@ public class InternalAuthenticationService extends AbstractComponent implements
} finally {
token.clearCredentials();
}
}
@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();
}
}
}

View File

@ -29,7 +29,6 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.internal.stubbing.answers.DoesNothing;
import static org.mockito.Mockito.*;
@ -66,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);
}
@ -77,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);
}
@ -89,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
@ -98,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);
@ -111,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
@ -120,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
@ -130,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);
}
@ -143,7 +163,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
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(filter).authenticateAndAuthorize("_action", request);
verify(chain).proceed(eq("_action"), eq(request), isA(SecurityFilter.SigningListener.class));
}
@ -155,7 +175,7 @@ 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);
@ -187,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);
}
@ -200,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);
}

View File

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

View File

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

View File

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