Modified the initial authentication logic

When a http request arrives, we first verify that it carries an authentication token (if it doesn't we throw an authentication exception). Beyond that, any action request that arrives, if it doesn't have an authentication token we assume system user identity. The rationale behind it is that  if a request comes in via the transport, then the sending peer authenticated with a client auth cert (the cert acts as the guarantee here that the actor can be assumed as System)... otherwise, the request can come from the local node and triggered by the system (e.g. gateway recovery)

The System user only has permissions to internal apis (it doesn't have full access/permission to all the apis). when a System identity is assumed, the authorization service will grant/deny the request based on whether the request is an internal api or not.

Aso fixed the known actions (to be insync with 1.x branch)

Closes elastic/elasticsearch#45

Original commit: elastic/x-pack-elasticsearch@be27cb0e1b
This commit is contained in:
uboness 2014-08-27 17:07:30 -07:00
parent eb29414077
commit 956aeb53f4
17 changed files with 202 additions and 40 deletions

View File

@ -11,6 +11,7 @@ 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.authc.AuthenticationService;
import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.shield.authc.system.SystemRealm;
@ -27,18 +28,39 @@ public class SecurityFilter extends AbstractComponent {
private final AuthorizationService authzService;
@Inject
public SecurityFilter(Settings settings, AuthenticationService authcService, AuthorizationService authzService) {
public SecurityFilter(Settings settings, AuthenticationService authcService, AuthorizationService authzService, RestController restController) {
super(settings);
this.authcService = authcService;
this.authzService = authzService;
restController.registerFilter(new Rest(this));
}
void process(String action, TransportRequest request, AuthenticationToken defaultToken) {
AuthenticationToken token = authcService.token(action, request, defaultToken);
void process(String action, TransportRequest request) {
AuthenticationToken token = authcService.token(action, request, SystemRealm.TOKEN);
User user = authcService.authenticate(action, request, token);
authzService.authorize(user, action, request);
}
public static class Rest extends RestFilter {
private final SecurityFilter filter;
public Rest(SecurityFilter filter) {
this.filter = filter;
}
@Override
public int order() {
return Integer.MIN_VALUE;
}
@Override
public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) throws Exception {
filter.authcService.verifyToken(request);
filterChain.continueProcessing(request, channel);
}
}
public static class Transport extends TransportFilter.Base {
private final SecurityFilter filter;
@ -50,7 +72,7 @@ public class SecurityFilter extends AbstractComponent {
@Override
public void inboundRequest(String action, TransportRequest request) {
filter.process(action, request, SystemRealm.TOKEN);
filter.process(action, request);
}
}
@ -66,7 +88,7 @@ public class SecurityFilter extends AbstractComponent {
@Override
public void process(String action, ActionRequest request, ActionListener listener, ActionFilterChain chain) {
try {
filter.process(action, request, null);
filter.process(action, request);
} catch (Throwable t) {
listener.onFailure(t);
return;

View File

@ -5,6 +5,7 @@
*/
package org.elasticsearch.shield.authc;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.User;
import org.elasticsearch.transport.TransportMessage;
@ -13,6 +14,12 @@ import org.elasticsearch.transport.TransportMessage;
*/
public interface AuthenticationService {
/**
* Inspects the given rest request and verifies it carries an authentication token, if it doesn't
* an {@link AuthenticationException} is thrown
*/
void verifyToken(RestRequest request) throws AuthenticationException;
/**
* Extracts the authenticate token from the given message. If no recognized auth token is associated
* with the message, an AuthenticationException is thrown.

View File

@ -9,6 +9,7 @@ import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.internal.Nullable;
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.transport.TransportMessage;
@ -33,6 +34,16 @@ public class InternalAuthenticationService extends AbstractComponent implements
this.auditTrail = auditTrail;
}
@Override
public void verifyToken(RestRequest request) throws AuthenticationException {
for (Realm realm : realms) {
if (realm.hasToken(request)) {
return;
}
}
throw new AuthenticationException("Missing authentication token");
}
@Override
public AuthenticationToken token(String action, TransportMessage<?> message) {
return token(action, message, null);

View File

@ -5,6 +5,7 @@
*/
package org.elasticsearch.shield.authc;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.User;
import org.elasticsearch.transport.TransportMessage;
@ -20,6 +21,8 @@ public interface Realm<T extends AuthenticationToken> {
*/
String type();
boolean hasToken(RestRequest request);
/**
* Attempts to extract a authentication token from the request. If an appropriate token is found
* {@link #authenticate(AuthenticationToken)} will be called for an authentication attempt. If no

View File

@ -10,6 +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.RestRequest;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.shield.authc.Realm;
@ -40,6 +41,11 @@ public class ESUsersRealm extends AbstractComponent implements Realm<UsernamePas
return TYPE;
}
@Override
public boolean hasToken(RestRequest request) {
return UsernamePasswordToken.hasToken(request);
}
@Override
public UsernamePasswordToken token(TransportMessage<?> message) {
return UsernamePasswordToken.extractToken(message, null);

View File

@ -10,6 +10,7 @@ import org.elasticsearch.common.cache.CacheBuilder;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.AuthenticationException;
import org.elasticsearch.shield.authc.AuthenticationToken;
@ -42,6 +43,11 @@ public abstract class CachingUsernamePasswordRealm extends AbstractComponent imp
}
}
@Override
public boolean hasToken(RestRequest request) {
return UsernamePasswordToken.hasToken(request);
}
@Override
public UsernamePasswordToken token(TransportMessage<?> message) {
return UsernamePasswordToken.extractToken(message, null);

View File

@ -7,6 +7,7 @@ package org.elasticsearch.shield.authc.support;
import org.apache.commons.codec.binary.Base64;
import org.elasticsearch.common.base.Charsets;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.authc.AuthenticationException;
import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.transport.TransportMessage;
@ -43,6 +44,11 @@ public class UsernamePasswordToken implements AuthenticationToken {
return password;
}
public static boolean hasToken(RestRequest request) {
String header = request.header(BASIC_AUTH_HEADER);
return header != null && BASIC_AUTH_PATTERN.matcher(header).matches();
}
public static UsernamePasswordToken extractToken(TransportMessage<?> message, UsernamePasswordToken defaultToken) {
UsernamePasswordToken token = (UsernamePasswordToken) message.context().get(TOKEN_KEY);
if (token != null) {
@ -65,6 +71,9 @@ public class UsernamePasswordToken implements AuthenticationToken {
String userpasswd = new String(Base64.decodeBase64(matcher.group(1)), Charsets.UTF_8);
int i = userpasswd.indexOf(':');
if (i < 0) {
throw new AuthenticationException("Invalid basic authentication header value");
}
token = new UsernamePasswordToken(userpasswd.substring(0, i), userpasswd.substring(i+1).toCharArray());
message.context().put(TOKEN_KEY, token);
return token;

View File

@ -6,6 +6,7 @@
package org.elasticsearch.shield.authc.system;
import org.elasticsearch.common.inject.AbstractModule;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.shield.authc.Realm;
@ -33,6 +34,11 @@ public class SystemRealm implements Realm<AuthenticationToken> {
return "system";
}
@Override
public boolean hasToken(RestRequest request) {
return false; // system calls never come from rest interface
}
@Override
public AuthenticationToken token(TransportMessage<?> message) {
// as far as this realm is concerned, there's never a system token

View File

@ -9,6 +9,10 @@ import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
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.authc.AuthenticationException;
import org.elasticsearch.shield.authc.AuthenticationService;
import org.elasticsearch.shield.authc.AuthenticationToken;
@ -36,34 +40,24 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
private SecurityFilter filter;
private AuthenticationService authcService;
private AuthorizationService authzService;
private RestController restController;
@Before
public void init() throws Exception {
authcService = mock(AuthenticationService.class);
authzService = mock(AuthorizationService.class);
filter = new SecurityFilter(ImmutableSettings.EMPTY, authcService, authzService);
restController = mock(RestController.class);
filter = new SecurityFilter(ImmutableSettings.EMPTY, authcService, authzService, restController);
}
@Test
public void testProcess_WithoutDefaultToken() throws Exception {
public void testProcess() throws Exception {
TransportRequest request = new InternalRequest();
AuthenticationToken token = mock(AuthenticationToken.class);
User user = new User.Simple("_username", "r1");
when(authcService.token("_action", request, null)).thenReturn(token);
when(authcService.token("_action", request, SystemRealm.TOKEN)).thenReturn(token);
when(authcService.authenticate("_action", request, token)).thenReturn(user);
filter.process("_action", request, null);
verify(authzService).authorize(user, "_action", request);
}
@Test
public void testProcess_WithDefaultToken() throws Exception {
TransportRequest request = new InternalRequest();
AuthenticationToken token = mock(AuthenticationToken.class);
AuthenticationToken defaultToken = mock(AuthenticationToken.class);
User user = new User.Simple("_username", "r1");
when(authcService.token("_action", request, defaultToken)).thenReturn(token);
when(authcService.authenticate("_action", request, token)).thenReturn(user);
filter.process("_action", request, defaultToken);
filter.process("_action", request);
verify(authzService).authorize(user, "_action", request);
}
@ -73,9 +67,9 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
thrown.expectMessage("failed authc");
TransportRequest request = new InternalRequest();
AuthenticationToken token = mock(AuthenticationToken.class);
when(authcService.token("_action", request, null)).thenReturn(token);
when(authcService.token("_action", request, SystemRealm.TOKEN)).thenReturn(token);
when(authcService.authenticate("_action", request, token)).thenThrow(new AuthenticationException("failed authc"));
filter.process("_action", request, null);
filter.process("_action", request);
}
@Test
@ -83,8 +77,8 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
thrown.expect(AuthenticationException.class);
thrown.expectMessage("failed authc");
TransportRequest request = new InternalRequest();
when(authcService.token("_action", request, null)).thenThrow(new AuthenticationException("failed authc"));
filter.process("_action", request, null);
when(authcService.token("_action", request, SystemRealm.TOKEN)).thenThrow(new AuthenticationException("failed authc"));
filter.process("_action", request);
}
@Test
@ -94,10 +88,10 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
TransportRequest request = new InternalRequest();
AuthenticationToken token = mock(AuthenticationToken.class);
User user = new User.Simple("_username", "r1");
when(authcService.token("_action", request, null)).thenReturn(token);
when(authcService.token("_action", request, SystemRealm.TOKEN)).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, null);
filter.process("_action", request);
}
@Test
@ -106,7 +100,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, SystemRealm.TOKEN);
verify(filter).process("_action", request);
}
@Test
@ -116,7 +110,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, SystemRealm.TOKEN);
doThrow(new RuntimeException("process-error")).when(filter).process("_action", request);
transport.inboundRequest("_action", request);
}
@ -128,7 +122,7 @@ public class SecurityFilterTests extends ElasticsearchTestCase {
ActionListener listener = mock(ActionListener.class);
ActionFilterChain chain = mock(ActionFilterChain.class);
action.process("_action", request, listener, chain);
verify(filter).process("_action", request, null);
verify(filter).process("_action", request);
verify(chain).continueProcessing("_action", request, listener);
}
@ -140,12 +134,35 @@ 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, null);
doThrow(exception).when(filter).process("_action", request);
action.process("_action", request, listener, chain);
verify(listener).onFailure(exception);
verifyNoMoreInteractions(chain);
}
@Test
public void testRest_WithToken() throws Exception {
SecurityFilter.Rest rest = new SecurityFilter.Rest(filter);
RestRequest request = mock(RestRequest.class);
RestChannel channel = mock(RestChannel.class);
RestFilterChain chain = mock(RestFilterChain.class);
rest.process(request, channel, chain);
verify(authcService).verifyToken(request);
}
@Test
public void testRest_WithoutToken() throws Exception {
AuthenticationException exception = new AuthenticationException("no token");
thrown.expect(AuthenticationException.class);
thrown.expectMessage("no token");
SecurityFilter.Rest rest = new SecurityFilter.Rest(filter);
RestRequest request = mock(RestRequest.class);
RestChannel channel = mock(RestChannel.class);
RestFilterChain chain = mock(RestFilterChain.class);
doThrow(exception).when(authcService).verifyToken(request);
rest.process(request, channel, chain);
}
private static class InternalRequest extends TransportRequest {
}
}

View File

@ -6,13 +6,16 @@
package org.elasticsearch.shield.authc;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.audit.AuditTrail;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.transport.TransportMessage;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.elasticsearch.shield.test.ShieldAssertions.assertContainsWWWAuthenticateHeader;
import static org.hamcrest.Matchers.*;
@ -24,8 +27,12 @@ import static org.mockito.Mockito.*;
*/
public class InternalAuthenticationServiceTests extends ElasticsearchTestCase {
@Rule
public ExpectedException thrown = ExpectedException.none();
InternalAuthenticationService service;
TransportMessage message;
RestRequest restRequest;
Realm firstRealm;
Realm secondRealm;
AuditTrail auditTrail;
@ -35,6 +42,7 @@ public class InternalAuthenticationServiceTests extends ElasticsearchTestCase {
public void init() throws Exception {
token = mock(AuthenticationToken.class);
message = new InternalMessage();
restRequest = mock(RestRequest.class);
firstRealm = mock(Realm.class);
when(firstRealm.type()).thenReturn("first");
secondRealm = mock(Realm.class);
@ -153,6 +161,22 @@ public class InternalAuthenticationServiceTests extends ElasticsearchTestCase {
assertThat(message.context().get(InternalAuthenticationService.USER_CTX_KEY), is((Object) user));
}
@Test
public void testVerifyToken_Exists() throws Exception {
when(firstRealm.hasToken(restRequest)).thenReturn(false);
when(secondRealm.hasToken(restRequest)).thenReturn(true);
service.verifyToken(restRequest);
}
@Test
public void testVerifyToken_Missing() throws Exception {
thrown.expect(AuthenticationException.class);
thrown.expectMessage("Missing authentication token");
when(firstRealm.hasToken(restRequest)).thenReturn(false);
when(secondRealm.hasToken(restRequest)).thenReturn(false);
service.verifyToken(restRequest);
}
private static class InternalMessage extends TransportMessage<InternalMessage> {
}

View File

@ -6,6 +6,7 @@
package org.elasticsearch.shield.authc.support;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.User;
import org.junit.Test;
@ -25,7 +26,12 @@ public class CachingUsernamePasswordRealmTests {
return new User.Simple(token.principal(), "testRole1", "testRole2");
}
@Override public String type() { return "test"; };
@Override public String type() { return "test"; }
@Override
public boolean hasToken(RestRequest request) {
return true;
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.shield.authc.support;
import com.google.common.base.Charsets;
import org.apache.commons.codec.binary.Base64;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.shield.authc.AuthenticationException;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.transport.TransportRequest;
@ -14,6 +15,8 @@ import org.junit.Test;
import static org.elasticsearch.shield.test.ShieldAssertions.assertContainsWWWAuthenticateHeader;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
*
@ -52,6 +55,21 @@ public class UsernamePasswordTokenTests extends ElasticsearchTestCase {
assertThat(token, is(token2));
}
@Test
public void testExtractToken_Invalid() throws Exception {
String[] invalidValues = { "Basic", "Basic ", "Basic f" };
for (String value : invalidValues) {
TransportRequest request = new TransportRequest() {};
request.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, value);
try {
UsernamePasswordToken.extractToken(request, null);
fail("Expected an authentication exception for invalid basic auth token [" + value + "]");
} catch (AuthenticationException ae) {
// expected
}
}
}
@Test
public void testThatAuthorizationExceptionContainsResponseHeaders() {
TransportRequest request = new TransportRequest() {};
@ -64,4 +82,27 @@ public class UsernamePasswordTokenTests extends ElasticsearchTestCase {
assertContainsWWWAuthenticateHeader(e);
}
}
@Test
public void testHasToken() throws Exception {
RestRequest request = mock(RestRequest.class);
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn("Basic foobar");
assertThat(UsernamePasswordToken.hasToken(request), is(true));
}
@Test
public void testHasToken_Missing() throws Exception {
RestRequest request = mock(RestRequest.class);
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn(null);
assertThat(UsernamePasswordToken.hasToken(request), is(false));
}
@Test
public void testHasToken_WithInvalidToken() throws Exception {
RestRequest request = mock(RestRequest.class);
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn("invalid");
assertThat(UsernamePasswordToken.hasToken(request), is(false));
when(request.header(UsernamePasswordToken.BASIC_AUTH_HEADER)).thenReturn("Basic");
assertThat(UsernamePasswordToken.hasToken(request), is(false));
}
}

View File

@ -41,9 +41,9 @@ import static org.hamcrest.Matchers.is;
@ClusterScope(scope = Scope.SUITE, numDataNodes = 1, numClientNodes = 0)
public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest {
private static final String DEFAULT_USER_NAME = "test_user";
private static final String DEFAULT_PASSWORD = "changeme";
private static final String DEFAULT_ROLE = "user";
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";

View File

@ -20,6 +20,7 @@ import org.elasticsearch.http.HttpServerTransport;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeBuilder;
import org.elasticsearch.plugins.PluginsService;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.shield.test.ShieldIntegrationTest;
import org.elasticsearch.shield.transport.netty.NettySecuredTransport;
import org.elasticsearch.transport.Transport;
@ -164,6 +165,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.connect();
assertThat(connection.getResponseCode(), is(200));

View File

@ -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.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.shield.test.ShieldIntegrationTest;
import org.junit.Test;
@ -127,6 +128,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.connect();
assertThat(connection.getResponseCode(), is(200));

View File

@ -36,9 +36,9 @@ indices:admin/optimize
indices:admin/refresh
indices:admin/settings/update
indices:admin/shards/search_shards
cluster:admin/template/delete
cluster:admin/template/get
cluster:admin/template/put
indices:admin/template/delete
indices:admin/template/get
indices:admin/template/put
indices:admin/types/exists
indices:admin/validate/query
indices:admin/warmers/delete