Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicitly require that derived API keys have no privileges (#53647) #53649

Merged
merged 3 commits into from
Mar 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ public int hashCode() {
return result;
}

public boolean isEmpty() {
return clusterPrivileges.length == 0
&& configurableClusterPrivileges.length == 0
&& indicesPrivileges.length == 0
&& applicationPrivileges.length == 0
&& runAs.length == 0
&& metadata.size() == 0;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return toXContent(builder, params, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ protected void doExecute(Task task, CreateApiKeyRequest request, ActionListener<
if (authentication == null) {
listener.onFailure(new IllegalStateException("authentication is required"));
} else {
if (Authentication.AuthenticationType.API_KEY == authentication.getAuthenticationType() && grantsAnyPrivileges(request)) {
listener.onFailure(new IllegalArgumentException(
"creating derived api keys requires an explicit role descriptor that is empty (has no privileges)"));
return;
}
rolesStore.getRoleDescriptors(new HashSet<>(Arrays.asList(authentication.getUser().roles())),
ActionListener.wrap(roleDescriptors -> {
for (RoleDescriptor rd : roleDescriptors) {
Expand All @@ -69,4 +74,10 @@ protected void doExecute(Task task, CreateApiKeyRequest request, ActionListener<
listener::onFailure));
}
}

private boolean grantsAnyPrivileges(CreateApiKeyRequest request) {
return request.getRoleDescriptors() == null
|| request.getRoleDescriptors().isEmpty()
|| false == request.getRoleDescriptors().stream().allMatch(RoleDescriptor::isEmpty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.action.admin.indices.refresh.RefreshAction;
import org.elasticsearch.action.admin.indices.refresh.RefreshRequestBuilder;
import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.action.support.WriteRequest;
Expand Down Expand Up @@ -669,6 +671,80 @@ public void testApiKeyWithManageOwnPrivilegeIsAbleToInvalidateItselfButNotAnyOth
assertThat(invalidateResponse.getErrors().size(), equalTo(0));
}

public void testDerivedKeys() throws ExecutionException, InterruptedException {
final Client client = client().filterWithHeader(Collections.singletonMap("Authorization",
UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER,
SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)));

final CreateApiKeyResponse response = new SecurityClient(client)
.prepareCreateApiKey()
.setName("key-1")
.setRoleDescriptors(Collections.singletonList(
new RoleDescriptor("role", new String[] { "manage_api_key" }, null, null)))
.get();

assertEquals("key-1", response.getName());
assertNotNull(response.getId());
assertNotNull(response.getKey());

// use the first ApiKey for authorized action
final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString(
(response.getId() + ":" + response.getKey().toString()).getBytes(StandardCharsets.UTF_8));
final SecurityClient clientKey1 = new SecurityClient(
client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)));

final String expectedMessage = "creating derived api keys requires an explicit role descriptor that is empty";

final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class,
() -> clientKey1.prepareCreateApiKey().setName("key-2").get());
assertThat(e1.getMessage(), containsString(expectedMessage));

final IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class,
() -> clientKey1.prepareCreateApiKey().setName("key-3")
.setRoleDescriptors(Collections.emptyList()).get());
assertThat(e2.getMessage(), containsString(expectedMessage));

final IllegalArgumentException e3 = expectThrows(IllegalArgumentException.class,
() -> clientKey1.prepareCreateApiKey().setName("key-4")
.setRoleDescriptors(Collections.singletonList(
new RoleDescriptor("role", new String[] {"manage_own_api_key"}, null, null)
)).get());
assertThat(e3.getMessage(), containsString(expectedMessage));

final List<RoleDescriptor> roleDescriptors = randomList(2, 10,
() -> new RoleDescriptor("role", null, null, null));
roleDescriptors.set(randomInt(roleDescriptors.size() - 1),
new RoleDescriptor("role", new String[] {"manage_own_api_key"}, null, null));

final IllegalArgumentException e4 = expectThrows(IllegalArgumentException.class,
() -> clientKey1.prepareCreateApiKey().setName("key-5")
.setRoleDescriptors(roleDescriptors).get());
assertThat(e4.getMessage(), containsString(expectedMessage));

final CreateApiKeyResponse key100Response = clientKey1.prepareCreateApiKey().setName("key-100")
.setRoleDescriptors(Collections.singletonList(
new RoleDescriptor("role", null, null, null)
)).get();
assertEquals("key-100", key100Response.getName());
assertNotNull(key100Response.getId());
assertNotNull(key100Response.getKey());

// Check at the end to allow sometime for the operation to happen. Since an erroneous creation is
// asynchronous so that the document is not available immediately.
assertApiKeyNotCreated(client,"key-2");
assertApiKeyNotCreated(client,"key-3");
assertApiKeyNotCreated(client,"key-4");
assertApiKeyNotCreated(client,"key-5");
}

private void assertApiKeyNotCreated(Client client, String keyName) throws ExecutionException, InterruptedException {
new RefreshRequestBuilder(client, RefreshAction.INSTANCE).setIndices(SECURITY_MAIN_ALIAS).execute().get();
PlainActionFuture<GetApiKeyResponse> getApiKeyResponseListener = new PlainActionFuture<>();
new SecurityClient(client).getApiKey(
GetApiKeyRequest.usingApiKeyName(keyName, false), getApiKeyResponseListener);
assertEquals(0, getApiKeyResponseListener.get().getApiKeyInfos().length);
}

private void verifyGetResponse(int expectedNumberOfApiKeys, List<CreateApiKeyResponse> responses,
GetApiKeyResponse response, Set<String> validApiKeyIds, List<String> invalidatedApiKeyIds) {
verifyGetResponse(SecuritySettingsSource.TEST_SUPERUSER, expectedNumberOfApiKeys, responses, response, validApiKeyIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
Expand Down Expand Up @@ -325,4 +327,55 @@ public void testParseIndicesPrivilegesFailsWhenExceptFieldsAreNotSubsetOfGranted
assertThat(epe, TestMatchers.throwableWithMessage(containsString("f2")));
assertThat(epe, TestMatchers.throwableWithMessage(containsString("f3")));
}

public void testIsEmpty() {
assertTrue(new RoleDescriptor(
randomAlphaOfLengthBetween(1, 10), null, null, null, null, null, null, null)
.isEmpty());

assertTrue(new RoleDescriptor(
randomAlphaOfLengthBetween(1, 10),
new String[0],
new RoleDescriptor.IndicesPrivileges[0],
new RoleDescriptor.ApplicationResourcePrivileges[0],
new ConfigurableClusterPrivilege[0],
new String[0],
new HashMap<>(),
new HashMap<>())
.isEmpty());

final List<Boolean> booleans = Arrays.asList(
randomBoolean(),
randomBoolean(),
randomBoolean(),
randomBoolean(),
randomBoolean(),
randomBoolean());

final RoleDescriptor roleDescriptor = new RoleDescriptor(
randomAlphaOfLengthBetween(1, 10),
booleans.get(0) ? new String[0] : new String[] { "foo" },
booleans.get(1) ?
new RoleDescriptor.IndicesPrivileges[0] :
new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder().indices("idx").privileges("foo").build() },
booleans.get(2) ?
new RoleDescriptor.ApplicationResourcePrivileges[0] :
new RoleDescriptor.ApplicationResourcePrivileges[] {
RoleDescriptor.ApplicationResourcePrivileges.builder()
.application("app").privileges("foo").resources("res").build() },
booleans.get(3) ?
new ConfigurableClusterPrivilege[0] :
new ConfigurableClusterPrivilege[] {
new ConfigurableClusterPrivileges.ManageApplicationPrivileges(Collections.singleton("foo")) },
booleans.get(4) ? new String[0] : new String[] { "foo" },
booleans.get(5) ? new HashMap<>() : Collections.singletonMap("foo", "bar"),
Collections.singletonMap("foo", "bar"));

if (booleans.stream().anyMatch(e -> e.equals(false))) {
assertFalse(roleDescriptor.isEmpty());
} else {
assertTrue(roleDescriptor.isEmpty());
}
}
}