diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java index 2152bbe4f6cf8..c6421c4f506fb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java @@ -52,7 +52,7 @@ public ActionRequestValidationException validate() { if (name == null) { validationException = addValidationError("role name is missing", validationException); } - if(applicationPrivileges != null) { + if (applicationPrivileges != null) { for (RoleDescriptor.ApplicationResourcePrivileges privilege : applicationPrivileges) { try { ApplicationPrivilege.validateApplicationNameOrWildcard(privilege.getApplication()); @@ -83,11 +83,11 @@ public void cluster(String... clusterPrivileges) { this.clusterPrivileges = clusterPrivileges; } - void conditionalCluster(ConditionalClusterPrivilege... conditionalClusterPrivileges) { + public void conditionalClusterPrivileges(ConditionalClusterPrivilege... conditionalClusterPrivileges) { this.conditionalClusterPrivileges = conditionalClusterPrivileges; } - void addIndex(RoleDescriptor.IndicesPrivileges... privileges) { + public void addIndicesPrivileges(RoleDescriptor.IndicesPrivileges... privileges) { this.indicesPrivileges.addAll(Arrays.asList(privileges)); } @@ -102,7 +102,7 @@ public void addIndex(String[] indices, String[] privileges, String[] grantedFiel .build()); } - void addApplicationPrivileges(RoleDescriptor.ApplicationResourcePrivileges... privileges) { + public void addApplicationPrivileges(RoleDescriptor.ApplicationResourcePrivileges... privileges) { this.applicationPrivileges.addAll(Arrays.asList(privileges)); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestBuilder.java index 670deb2216bf6..982772a9fa1bf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestBuilder.java @@ -40,8 +40,8 @@ public PutRoleRequestBuilder source(String name, BytesReference source, XContent assert name.equals(descriptor.getName()); request.name(name); request.cluster(descriptor.getClusterPrivileges()); - request.conditionalCluster(descriptor.getConditionalClusterPrivileges()); - request.addIndex(descriptor.getIndicesPrivileges()); + request.conditionalClusterPrivileges(descriptor.getConditionalClusterPrivileges()); + request.addIndicesPrivileges(descriptor.getIndicesPrivileges()); request.addApplicationPrivileges(descriptor.getApplicationPrivileges()); request.runAs(descriptor.getRunAs()); request.metadata(descriptor.getMetadata()); diff --git a/x-pack/plugin/core/src/main/resources/security-index-template.json b/x-pack/plugin/core/src/main/resources/security-index-template.json index 073db1830598f..86552d353ffe0 100644 --- a/x-pack/plugin/core/src/main/resources/security-index-template.json +++ b/x-pack/plugin/core/src/main/resources/security-index-template.json @@ -154,10 +154,6 @@ "index": false, "doc_values": false }, - "role_descriptors" : { - "type" : "object", - "dynamic" : true - }, "version" : { "type" : "integer" }, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java index a68a522f0242c..6e075178e2af4 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java @@ -153,7 +153,7 @@ private PutRoleRequest buildRandomRequest() { if (randomBoolean()) { final String[] appNames = randomArray(1, 4, String[]::new, stringWithInitialLowercase); - request.conditionalCluster(new ConditionalClusterPrivileges.ManageApplicationPrivileges(Sets.newHashSet(appNames))); + request.conditionalClusterPrivileges(new ConditionalClusterPrivileges.ManageApplicationPrivileges(Sets.newHashSet(appNames))); } request.runAs(generateRandomStringArray(4, 3, false, true)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 69b3121acb638..783c8b0d041f2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -8,8 +8,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.DocWriteRequest; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexAction; @@ -28,9 +30,16 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction; +import org.elasticsearch.xpack.core.security.action.role.DeleteRoleRequest; +import org.elasticsearch.xpack.core.security.action.role.PutRoleAction; +import org.elasticsearch.xpack.core.security.action.role.PutRoleRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -40,8 +49,11 @@ import java.security.NoSuchAlgorithmException; import java.time.Clock; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -79,8 +91,8 @@ public class ApiKeyService { private final Hasher hasher; private final boolean enabled; - public ApiKeyService(Settings settings, Clock clock, Client client, - SecurityIndexManager securityIndex, ClusterService clusterService) { + public ApiKeyService(Settings settings, Clock clock, Client client, SecurityIndexManager securityIndex, + ClusterService clusterService) { this.clock = clock; this.client = client; this.securityIndex = securityIndex; @@ -109,7 +121,11 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ "able to use api keys", Version.V_7_0_0); } + final String id = UUIDs.base64UUID(); + final String roleName = "_es_apikey_role_" + id; + final PutRoleRequest putRoleRequest = mergeRoleDescriptors(roleName, request.getRoleDescriptors()); final char[] keyHash = hasher.hash(apiKey); + IndexRequest indexRequest; try (XContentBuilder builder = XContentFactory.jsonBuilder()) { builder.startObject() .field("doc_type", "api_key") @@ -126,7 +142,7 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ } } - builder.array("role_descriptors", request.getRoleDescriptors()) + builder.array("roles", roleName) .field("name", request.getName()) .field("version", version.id) .startObject("creator") @@ -136,24 +152,74 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ authentication.getAuthenticatedBy().getName() : authentication.getLookedUpBy().getName()) .endObject() .endObject(); - final IndexRequest indexRequest = - client.prepareIndex(SecurityIndexManager.SECURITY_INDEX_NAME, TYPE) + indexRequest = client.prepareIndex(SecurityIndexManager.SECURITY_INDEX_NAME, TYPE, id) + .setOpType(DocWriteRequest.OpType.CREATE) .setSource(builder) .setRefreshPolicy(request.getRefreshPolicy()) .request(); - securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> - executeAsyncWithOrigin(client, SECURITY_ORIGIN, IndexAction.INSTANCE, indexRequest, - ActionListener.wrap(indexResponse -> - listener.onResponse(new CreateApiKeyResponse(request.getName(), indexResponse.getId(), apiKey, expiration)), - listener::onFailure))); } catch (IOException e) { listener.onFailure(e); + return; } finally { Arrays.fill(keyHash, (char) 0); } + + if (indexRequest == null) { + throw new IllegalStateException("index request cannot be null"); + } + + client.execute(PutRoleAction.INSTANCE, putRoleRequest, ActionListener.wrap(response -> + executeAsyncWithOrigin(client, SECURITY_ORIGIN, IndexAction.INSTANCE, indexRequest, ActionListener.wrap(indexResponse -> + listener.onResponse(new CreateApiKeyResponse(request.getName(), indexResponse.getId(), apiKey, expiration)), + e -> { + final DeleteRoleRequest deleteRoleRequest = new DeleteRoleRequest(); + deleteRoleRequest.name(roleName); + client.execute(DeleteRoleAction.INSTANCE, deleteRoleRequest, ActionListener.wrap(deleteRoleResponse -> { + if (deleteRoleResponse.found()) { + logger.info("api key creation failed, but successfully cleaned up role: " + roleName); + } else { + logger.info("api key creation failed and no role found with name: " + roleName); + } + }, deleteEx -> logger.error( + new ParameterizedMessage("failed to clean up role [{}] after api key creation failure", roleName), deleteEx))); + listener.onFailure(e); + })), + listener::onFailure)); } } + /** + * Combines the provided role descriptors into a put role request with the specified name + */ + private PutRoleRequest mergeRoleDescriptors(String name, List descriptors) { + final List indicesPrivileges = new ArrayList<>(); + final Map metadata = new HashMap<>(); + final List clusterPrivileges = new ArrayList<>(); + final List runAsPrivileges = new ArrayList<>(); + final List applicationResourcePrivileges = new ArrayList<>(); + final List conditionalClusterPrivileges = new ArrayList<>(); + for (RoleDescriptor descriptor : descriptors) { + indicesPrivileges.addAll(Arrays.asList(descriptor.getIndicesPrivileges())); + metadata.putAll(descriptor.getMetadata().entrySet().stream() + .filter(e -> e.getKey().startsWith(MetadataUtils.RESERVED_PREFIX) == false) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + clusterPrivileges.addAll(Arrays.asList(descriptor.getClusterPrivileges())); + runAsPrivileges.addAll(Arrays.asList(descriptor.getRunAs())); + applicationResourcePrivileges.addAll(Arrays.asList(descriptor.getApplicationPrivileges())); + conditionalClusterPrivileges.addAll(Arrays.asList(descriptor.getConditionalClusterPrivileges())); + } + + final PutRoleRequest request = new PutRoleRequest(); + request.name(name); + request.cluster(clusterPrivileges.toArray(Strings.EMPTY_ARRAY)); + request.addIndicesPrivileges(indicesPrivileges.toArray(new RoleDescriptor.IndicesPrivileges[0])); + request.addApplicationPrivileges(applicationResourcePrivileges.toArray(new RoleDescriptor.ApplicationResourcePrivileges[0])); + request.conditionalClusterPrivileges(conditionalClusterPrivileges.toArray(new ConditionalClusterPrivilege[0])); + request.runAs(runAsPrivileges.toArray(Strings.EMPTY_ARRAY)); + request.metadata(metadata); + return request; + } + /** * Checks for the presence of a {@code Authorization} header with a value that starts with * {@code ApiKey }. If found this will attempt to authenticate the key. @@ -211,12 +277,14 @@ static void validateApiKeyCredentials(Map source, ApiKeyCredenti if (expirationEpochMilli == null || Instant.ofEpochMilli(expirationEpochMilli).isAfter(clock.instant())) { final String principal = Objects.requireNonNull((String) source.get("principal")); final Map metadata = (Map) source.get("metadata"); - final List> roleDescriptors = (List>) source.get("role_descriptors"); - final String[] roleNames = roleDescriptors.stream() - .map(rdSource -> (String) rdSource.get("name")) - .collect(Collectors.toList()) - .toArray(Strings.EMPTY_ARRAY); - final User apiKeyUser = new User(principal, roleNames, null, null, metadata, true); + final List roles; + final Object rolesObj = source.get("roles"); + if (rolesObj instanceof String) { + roles = Collections.singletonList((String) rolesObj); + } else { + roles = (List) rolesObj; + } + final User apiKeyUser = new User(principal, roles.toArray(Strings.EMPTY_ARRAY), null, null, metadata, true); listener.onResponse(AuthenticationResult.success(apiKeyUser)); } else { listener.onResponse(AuthenticationResult.unsuccessful("api key is expired", null)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index b7814df7f7fde..251881c09d154 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -71,7 +71,7 @@ public void testValidateApiKey() throws Exception { sourceMap.put("api_key_hash", new String(hash)); sourceMap.put("principal", "test_user"); sourceMap.put("metadata", Collections.emptyMap()); - sourceMap.put("role_descriptors", Collections.singletonList(Collections.singletonMap("name", "a role"))); + sourceMap.put("roles", Collections.singletonList("a role")); ApiKeyService.ApiKeyCredentials creds =