|
@@ -0,0 +1,230 @@
|
|
|
+/*
|
|
|
+ * 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.xpack.security.action;
|
|
|
+
|
|
|
+import org.elasticsearch.ElasticsearchSecurityException;
|
|
|
+import org.elasticsearch.ElasticsearchStatusException;
|
|
|
+import org.elasticsearch.action.ActionListener;
|
|
|
+import org.elasticsearch.action.support.ActionFilters;
|
|
|
+import org.elasticsearch.action.support.PlainActionFuture;
|
|
|
+import org.elasticsearch.common.settings.SecureString;
|
|
|
+import org.elasticsearch.common.util.concurrent.ThreadContext;
|
|
|
+import org.elasticsearch.test.ESTestCase;
|
|
|
+import org.elasticsearch.threadpool.TestThreadPool;
|
|
|
+import org.elasticsearch.threadpool.ThreadPool;
|
|
|
+import org.elasticsearch.transport.TransportService;
|
|
|
+import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest;
|
|
|
+import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse;
|
|
|
+import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction;
|
|
|
+import org.elasticsearch.xpack.core.security.action.GrantApiKeyRequest;
|
|
|
+import org.elasticsearch.xpack.core.security.authc.Authentication;
|
|
|
+import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken;
|
|
|
+import org.elasticsearch.xpack.core.security.user.User;
|
|
|
+import org.elasticsearch.xpack.security.authc.AuthenticationService;
|
|
|
+import org.elasticsearch.xpack.security.authc.TokenServiceMock;
|
|
|
+import org.elasticsearch.xpack.security.authc.support.ApiKeyGenerator;
|
|
|
+import org.elasticsearch.xpack.security.test.SecurityMocks;
|
|
|
+import org.junit.After;
|
|
|
+import org.junit.Before;
|
|
|
+
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
|
|
|
+import static org.hamcrest.Matchers.arrayWithSize;
|
|
|
+import static org.hamcrest.Matchers.equalTo;
|
|
|
+import static org.hamcrest.Matchers.instanceOf;
|
|
|
+import static org.hamcrest.Matchers.sameInstance;
|
|
|
+import static org.mockito.Matchers.any;
|
|
|
+import static org.mockito.Matchers.eq;
|
|
|
+import static org.mockito.Matchers.same;
|
|
|
+import static org.mockito.Mockito.doAnswer;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.verifyZeroInteractions;
|
|
|
+
|
|
|
+public class TransportGrantApiKeyActionTests extends ESTestCase {
|
|
|
+
|
|
|
+ private TransportGrantApiKeyAction action;
|
|
|
+ private ApiKeyGenerator apiKeyGenerator;
|
|
|
+ private AuthenticationService authenticationService;
|
|
|
+ private TokenServiceMock tokenServiceMock;
|
|
|
+ private ThreadPool threadPool;
|
|
|
+
|
|
|
+ @Before
|
|
|
+ public void setupMocks() throws Exception {
|
|
|
+ apiKeyGenerator = mock(ApiKeyGenerator.class);
|
|
|
+ authenticationService = mock(AuthenticationService.class);
|
|
|
+
|
|
|
+ threadPool = new TestThreadPool("TP-" + getTestName());
|
|
|
+ tokenServiceMock = SecurityMocks.tokenService(true, threadPool);
|
|
|
+ final ThreadContext threadContext = threadPool.getThreadContext();
|
|
|
+
|
|
|
+ action = new TransportGrantApiKeyAction(mock(TransportService.class), mock(ActionFilters.class), threadContext,
|
|
|
+ apiKeyGenerator, authenticationService, tokenServiceMock.tokenService);
|
|
|
+ }
|
|
|
+
|
|
|
+ @After
|
|
|
+ public void cleanup() {
|
|
|
+ threadPool.shutdown();
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testGrantApiKeyWithUsernamePassword() throws Exception {
|
|
|
+ final String username = randomAlphaOfLengthBetween(4, 12);
|
|
|
+ final SecureString password = new SecureString(randomAlphaOfLengthBetween(8, 24).toCharArray());
|
|
|
+ final Authentication authentication = buildAuthentication(username);
|
|
|
+
|
|
|
+ final GrantApiKeyRequest request = mockRequest();
|
|
|
+ request.getGrant().setType("password");
|
|
|
+ request.getGrant().setUsername(username);
|
|
|
+ request.getGrant().setPassword(password);
|
|
|
+
|
|
|
+ final CreateApiKeyResponse response = mockResponse(request);
|
|
|
+
|
|
|
+ doAnswer(inv -> {
|
|
|
+ final Object[] args = inv.getArguments();
|
|
|
+ assertThat(args, arrayWithSize(4));
|
|
|
+
|
|
|
+ assertThat(args[0], equalTo(GrantApiKeyAction.NAME));
|
|
|
+ assertThat(args[1], sameInstance(request));
|
|
|
+ assertThat(args[2], instanceOf(UsernamePasswordToken.class));
|
|
|
+ UsernamePasswordToken token = (UsernamePasswordToken) args[2];
|
|
|
+ assertThat(token.principal(), equalTo(username));
|
|
|
+ assertThat(token.credentials(), equalTo(password));
|
|
|
+
|
|
|
+ ActionListener<Authentication> listener = (ActionListener<Authentication>) args[args.length - 1];
|
|
|
+ listener.onResponse(authentication);
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }).when(authenticationService)
|
|
|
+ .authenticate(eq(GrantApiKeyAction.NAME), same(request), any(UsernamePasswordToken.class), any(ActionListener.class));
|
|
|
+
|
|
|
+ setupApiKeyGenerator(authentication, request, response);
|
|
|
+
|
|
|
+ final PlainActionFuture<CreateApiKeyResponse> future = new PlainActionFuture<>();
|
|
|
+ action.doExecute(null, request, future);
|
|
|
+
|
|
|
+ assertThat(future.actionGet(), sameInstance(response));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testGrantApiKeyWithInvalidUsernamePassword() throws Exception {
|
|
|
+ final String username = randomAlphaOfLengthBetween(4, 12);
|
|
|
+ final SecureString password = new SecureString(randomAlphaOfLengthBetween(8, 24).toCharArray());
|
|
|
+ final Authentication authentication = buildAuthentication(username);
|
|
|
+
|
|
|
+ final GrantApiKeyRequest request = mockRequest();
|
|
|
+ request.getGrant().setType("password");
|
|
|
+ request.getGrant().setUsername(username);
|
|
|
+ request.getGrant().setPassword(password);
|
|
|
+
|
|
|
+ final CreateApiKeyResponse response = mockResponse(request);
|
|
|
+
|
|
|
+ doAnswer(inv -> {
|
|
|
+ final Object[] args = inv.getArguments();
|
|
|
+ assertThat(args, arrayWithSize(4));
|
|
|
+
|
|
|
+ assertThat(args[0], equalTo(GrantApiKeyAction.NAME));
|
|
|
+ assertThat(args[1], sameInstance(request));
|
|
|
+ assertThat(args[2], instanceOf(UsernamePasswordToken.class));
|
|
|
+ UsernamePasswordToken token = (UsernamePasswordToken) args[2];
|
|
|
+ assertThat(token.principal(), equalTo(username));
|
|
|
+ assertThat(token.credentials(), equalTo(password));
|
|
|
+
|
|
|
+ ActionListener<Authentication> listener = (ActionListener<Authentication>) args[args.length - 1];
|
|
|
+ listener.onFailure(new ElasticsearchSecurityException("authentication failed for testing"));
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }).when(authenticationService)
|
|
|
+ .authenticate(eq(GrantApiKeyAction.NAME), same(request), any(UsernamePasswordToken.class), any(ActionListener.class));
|
|
|
+
|
|
|
+ setupApiKeyGenerator(authentication, request, response);
|
|
|
+
|
|
|
+ final PlainActionFuture<CreateApiKeyResponse> future = new PlainActionFuture<>();
|
|
|
+ action.doExecute(null, request, future);
|
|
|
+
|
|
|
+ final ElasticsearchStatusException exception = expectThrows(ElasticsearchStatusException.class, future::actionGet);
|
|
|
+ assertThat(exception, throwableWithMessage("authentication failed for testing"));
|
|
|
+
|
|
|
+ verifyZeroInteractions(apiKeyGenerator);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testGrantApiKeyWithAccessToken() throws Exception {
|
|
|
+ final String username = randomAlphaOfLengthBetween(4, 12);
|
|
|
+ final TokenServiceMock.MockToken token = tokenServiceMock.mockAccessToken();
|
|
|
+ final Authentication authentication = buildAuthentication(username);
|
|
|
+
|
|
|
+ final GrantApiKeyRequest request = mockRequest();
|
|
|
+ request.getGrant().setType("access_token");
|
|
|
+ request.getGrant().setAccessToken(token.encodedToken);
|
|
|
+
|
|
|
+ final CreateApiKeyResponse response = mockResponse(request);
|
|
|
+
|
|
|
+ tokenServiceMock.defineToken(token, authentication);
|
|
|
+ setupApiKeyGenerator(authentication, request, response);
|
|
|
+
|
|
|
+ final PlainActionFuture<CreateApiKeyResponse> future = new PlainActionFuture<>();
|
|
|
+ action.doExecute(null, request, future);
|
|
|
+
|
|
|
+ assertThat(future.actionGet(), sameInstance(response));
|
|
|
+ verifyZeroInteractions(authenticationService);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testGrantApiKeyWithInvalidatedAccessToken() throws Exception {
|
|
|
+ final String username = randomAlphaOfLengthBetween(4, 12);
|
|
|
+ final TokenServiceMock.MockToken token = tokenServiceMock.mockAccessToken();
|
|
|
+ final Authentication authentication = buildAuthentication(username);
|
|
|
+
|
|
|
+ final GrantApiKeyRequest request = mockRequest();
|
|
|
+ request.getGrant().setType("access_token");
|
|
|
+ request.getGrant().setAccessToken(token.encodedToken);
|
|
|
+
|
|
|
+ final CreateApiKeyResponse response = mockResponse(request);
|
|
|
+
|
|
|
+ tokenServiceMock.defineToken(token, authentication, false);
|
|
|
+ setupApiKeyGenerator(authentication, request, response);
|
|
|
+
|
|
|
+ final PlainActionFuture<CreateApiKeyResponse> future = new PlainActionFuture<>();
|
|
|
+ action.doExecute(null, request, future);
|
|
|
+
|
|
|
+ final ElasticsearchStatusException exception = expectThrows(ElasticsearchStatusException.class, future::actionGet);
|
|
|
+ assertThat(exception, throwableWithMessage("token expired"));
|
|
|
+
|
|
|
+ verifyZeroInteractions(authenticationService);
|
|
|
+ verifyZeroInteractions(apiKeyGenerator);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Authentication buildAuthentication(String username) {
|
|
|
+ return new Authentication(new User(username),
|
|
|
+ new Authentication.RealmRef("realm_name", "realm_type", "node_name"), null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CreateApiKeyResponse mockResponse(GrantApiKeyRequest request) {
|
|
|
+ return new CreateApiKeyResponse(request.getApiKeyRequest().getName(),
|
|
|
+ randomAlphaOfLength(12), new SecureString(randomAlphaOfLength(18).toCharArray()), null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private GrantApiKeyRequest mockRequest() {
|
|
|
+ final String keyName = randomAlphaOfLengthBetween(6, 32);
|
|
|
+ final GrantApiKeyRequest request = new GrantApiKeyRequest();
|
|
|
+ request.setApiKeyRequest(new CreateApiKeyRequest(keyName, List.of(), null));
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void setupApiKeyGenerator(Authentication authentication, GrantApiKeyRequest request, CreateApiKeyResponse response) {
|
|
|
+ doAnswer(inv -> {
|
|
|
+ final Object[] args = inv.getArguments();
|
|
|
+ assertThat(args, arrayWithSize(3));
|
|
|
+
|
|
|
+ assertThat(args[0], equalTo(authentication));
|
|
|
+ assertThat(args[1], sameInstance(request.getApiKeyRequest()));
|
|
|
+
|
|
|
+ ActionListener<CreateApiKeyResponse> listener = (ActionListener<CreateApiKeyResponse>) args[args.length - 1];
|
|
|
+ listener.onResponse(response);
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }).when(apiKeyGenerator).generateApiKey(any(Authentication.class), any(CreateApiKeyRequest.class), any(ActionListener.class));
|
|
|
+ }
|
|
|
+
|
|
|
+}
|