diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java index d328ff795c559..a48a7e3a105cd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java @@ -133,12 +133,15 @@ private List buildRoleReferencesForApiKey() { final RoleReference.ApiKeyRoleReference limitedByRoleReference = new RoleReference.ApiKeyRoleReference( apiKeyId, limitedByRoleDescriptorsBytes, - "apikey_limited_role" + RoleReference.ApiKeyRoleType.LIMITED_BY ); if (isEmptyRoleDescriptorsBytes(roleDescriptorsBytes)) { return List.of(limitedByRoleReference); } - return List.of(new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, "apikey_role"), limitedByRoleReference); + return List.of( + new RoleReference.ApiKeyRoleReference(apiKeyId, roleDescriptorsBytes, RoleReference.ApiKeyRoleType.ASSIGNED), + limitedByRoleReference + ); } private boolean isEmptyRoleDescriptorsBytes(BytesReference roleDescriptorsBytes) { @@ -155,13 +158,13 @@ private List buildRolesReferenceForApiKeyBwc() { final RoleReference.BwcApiKeyRoleReference limitedByRoleReference = new RoleReference.BwcApiKeyRoleReference( apiKeyId, limitedByRoleDescriptorsMap, - "_limited_role_desc" + RoleReference.ApiKeyRoleType.LIMITED_BY ); if (roleDescriptorsMap == null || roleDescriptorsMap.isEmpty()) { return List.of(limitedByRoleReference); } else { return List.of( - new RoleReference.BwcApiKeyRoleReference(apiKeyId, roleDescriptorsMap, "_role_desc"), + new RoleReference.BwcApiKeyRoleReference(apiKeyId, roleDescriptorsMap, RoleReference.ApiKeyRoleType.ASSIGNED), limitedByRoleReference ); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReference.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReference.java index 7f5715f4afb5d..9ceeb724b4202 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReference.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/RoleReference.java @@ -77,13 +77,13 @@ final class ApiKeyRoleReference implements RoleReference { private final String apiKeyId; private final BytesReference roleDescriptorsBytes; - private final String roleKeySource; + private final ApiKeyRoleType roleType; private RoleKey id = null; - public ApiKeyRoleReference(String apiKeyId, BytesReference roleDescriptorsBytes, String roleKeySource) { + public ApiKeyRoleReference(String apiKeyId, BytesReference roleDescriptorsBytes, ApiKeyRoleType roleType) { this.apiKeyId = apiKeyId; this.roleDescriptorsBytes = roleDescriptorsBytes; - this.roleKeySource = roleKeySource; + this.roleType = roleType; } @Override @@ -93,7 +93,7 @@ public RoleKey id() { final String roleDescriptorsHash = MessageDigests.toHexString( MessageDigests.digest(roleDescriptorsBytes, MessageDigests.sha256()) ); - id = new RoleKey(Set.of("apikey:" + roleDescriptorsHash), roleKeySource); + id = new RoleKey(Set.of("apikey:" + roleDescriptorsHash), "apikey_" + roleType); } return id; } @@ -110,6 +110,10 @@ public String getApiKeyId() { public BytesReference getRoleDescriptorsBytes() { return roleDescriptorsBytes; } + + public ApiKeyRoleType getRoleType() { + return roleType; + } } /** @@ -118,18 +122,18 @@ public BytesReference getRoleDescriptorsBytes() { final class BwcApiKeyRoleReference implements RoleReference { private final String apiKeyId; private final Map roleDescriptorsMap; - private final String roleKeySourceSuffix; + private final ApiKeyRoleType roleType; - public BwcApiKeyRoleReference(String apiKeyId, Map roleDescriptorsMap, String roleKeySourceSuffix) { + public BwcApiKeyRoleReference(String apiKeyId, Map roleDescriptorsMap, ApiKeyRoleType roleType) { this.apiKeyId = apiKeyId; this.roleDescriptorsMap = roleDescriptorsMap; - this.roleKeySourceSuffix = roleKeySourceSuffix; + this.roleType = roleType; } @Override public RoleKey id() { // Since api key id is unique, it is sufficient and more correct to use it as the names - return new RoleKey(Set.of(apiKeyId), "bwc_api_key" + roleKeySourceSuffix); + return new RoleKey(Set.of(apiKeyId), "bwc_api_key_" + roleType); } @Override @@ -144,6 +148,10 @@ public String getApiKeyId() { public Map getRoleDescriptorsMap() { return roleDescriptorsMap; } + + public ApiKeyRoleType getRoleType() { + return roleType; + } } /** @@ -170,4 +178,18 @@ public void resolve(RoleReferenceResolver resolver, ActionListener parseRoleDescriptors(final String apiKeyId, final Map roleDescriptors) { - if (roleDescriptors == null) { + public List parseRoleDescriptors( + final String apiKeyId, + final Map roleDescriptorsMap, + RoleReference.ApiKeyRoleType roleType + ) { + if (roleDescriptorsMap == null) { return null; } - return roleDescriptors.entrySet().stream().map(entry -> { + final List roleDescriptors = roleDescriptorsMap.entrySet().stream().map(entry -> { final String name = entry.getKey(); @SuppressWarnings("unchecked") final Map rdMap = (Map) entry.getValue(); @@ -543,9 +550,16 @@ public List parseRoleDescriptors(final String apiKeyId, final Ma throw new UncheckedIOException(e); } }).collect(Collectors.toList()); + return roleType == RoleReference.ApiKeyRoleType.LIMITED_BY + ? maybeReplaceSuperuserRoleDescriptor(apiKeyId, roleDescriptors) + : roleDescriptors; } - public List parseRoleDescriptorsBytes(final String apiKeyId, BytesReference bytesReference) { + public List parseRoleDescriptorsBytes( + final String apiKeyId, + BytesReference bytesReference, + RoleReference.ApiKeyRoleType roleType + ) { if (bytesReference == null) { return Collections.emptyList(); } @@ -568,7 +582,42 @@ public List parseRoleDescriptorsBytes(final String apiKeyId, Byt } catch (IOException e) { throw new UncheckedIOException(e); } - return Collections.unmodifiableList(roleDescriptors); + return roleType == RoleReference.ApiKeyRoleType.LIMITED_BY + ? maybeReplaceSuperuserRoleDescriptor(apiKeyId, roleDescriptors) + : roleDescriptors; + } + + // package private for tests + static final RoleDescriptor LEGACY_SUPERUSER_ROLE_DESCRIPTOR = new RoleDescriptor( + "superuser", + new String[] { "all" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").allowRestrictedIndices(true).build() }, + new RoleDescriptor.ApplicationResourcePrivileges[] { + RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build() }, + null, + new String[] { "*" }, + MetadataUtils.DEFAULT_RESERVED_METADATA, + Collections.emptyMap() + ); + + // This method should only be called to replace the superuser role descriptor for the limited-by roles of an API Key. + // We do not replace assigned roles because they are created explicitly by users. + // Before #82049, it is possible to specify a role descriptor for API keys that is identical to the builtin superuser role + // (including the _reserved metadata field). + private List maybeReplaceSuperuserRoleDescriptor(String apiKeyId, List roleDescriptors) { + // Scan through all the roles because superuser can be one of the roles that a user has. Unlike building the Role object, + // capturing role descriptors does not preempt for superuser. + return roleDescriptors.stream().map(rd -> { + // Since we are only replacing limited-by roles and all limited-by roles are looked up with role providers, + // it is technically possible to just check the name of superuser and the _reserved metadata field. + // But the gain is not much since role resolving is cached and comparing the whole role descriptor is still safer. + if (rd.equals(LEGACY_SUPERUSER_ROLE_DESCRIPTOR)) { + logger.debug("replacing superuser role for API key [{}]", apiKeyId); + return ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR; + } + return rd; + }).toList(); } /** diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleDescriptorStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleDescriptorStore.java index f1d1dc57d925a..4b43173ff0e17 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleDescriptorStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/RoleDescriptorStore.java @@ -99,7 +99,8 @@ public void resolveApiKeyRoleReference( ) { final List roleDescriptors = apiKeyService.parseRoleDescriptorsBytes( apiKeyRoleReference.getApiKeyId(), - apiKeyRoleReference.getRoleDescriptorsBytes() + apiKeyRoleReference.getRoleDescriptorsBytes(), + apiKeyRoleReference.getRoleType() ); final RolesRetrievalResult rolesRetrievalResult = new RolesRetrievalResult(); rolesRetrievalResult.addDescriptors(Set.copyOf(roleDescriptors)); @@ -113,7 +114,8 @@ public void resolveBwcApiKeyRoleReference( ) { final List roleDescriptors = apiKeyService.parseRoleDescriptors( bwcApiKeyRoleReference.getApiKeyId(), - bwcApiKeyRoleReference.getRoleDescriptorsMap() + bwcApiKeyRoleReference.getRoleDescriptorsMap(), + bwcApiKeyRoleReference.getRoleType() ); final RolesRetrievalResult rolesRetrievalResult = new RolesRetrievalResult(); rolesRetrievalResult.addDescriptors(Set.copyOf(roleDescriptors)); 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 127966610b0fa..821281b9c5368 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 @@ -66,6 +66,7 @@ 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.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.store.RoleReference; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyCredentials; import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyDoc; @@ -111,6 +112,7 @@ import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.INTERNAL_SECURITY_MAIN_INDEX_7; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; import static org.elasticsearch.xpack.security.Security.SECURITY_CRYPTO_THREAD_POOL_NAME; +import static org.elasticsearch.xpack.security.authc.ApiKeyService.LEGACY_SUPERUSER_ROLE_DESCRIPTOR; import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; @@ -578,8 +580,8 @@ public void testParseRoleDescriptorsMap() throws Exception { }).when(privilegesStore).getPrivileges(any(Collection.class), any(Collection.class), anyActionListener()); ApiKeyService service = createApiKeyService(Settings.EMPTY); - assertThat(service.parseRoleDescriptors(apiKeyId, null), nullValue()); - assertThat(service.parseRoleDescriptors(apiKeyId, Collections.emptyMap()), emptyIterable()); + assertThat(service.parseRoleDescriptors(apiKeyId, null, randomApiKeyRoleType()), nullValue()); + assertThat(service.parseRoleDescriptors(apiKeyId, Collections.emptyMap(), randomApiKeyRoleType()), emptyIterable()); final RoleDescriptor roleARoleDescriptor = new RoleDescriptor( "a role", @@ -597,7 +599,7 @@ public void testParseRoleDescriptorsMap() throws Exception { ); } - List roleDescriptors = service.parseRoleDescriptors(apiKeyId, Map.of("a role", roleARDMap)); + List roleDescriptors = service.parseRoleDescriptors(apiKeyId, Map.of("a role", roleARDMap), randomApiKeyRoleType()); assertThat(roleDescriptors, hasSize(1)); assertThat(roleDescriptors.get(0), equalTo(roleARoleDescriptor)); @@ -609,19 +611,45 @@ public void testParseRoleDescriptorsMap() throws Exception { false ); } - roleDescriptors = service.parseRoleDescriptors(apiKeyId, Map.of(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap)); + roleDescriptors = service.parseRoleDescriptors( + apiKeyId, + Map.of(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap), + randomApiKeyRoleType() + ); assertThat(roleDescriptors, hasSize(1)); assertThat(roleDescriptors.get(0), equalTo(SUPERUSER_ROLE_DESCRIPTOR)); + + final Map legacySuperUserRdMap; + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + legacySuperUserRdMap = XContentHelper.convertToMap( + XContentType.JSON.xContent(), + BytesReference.bytes(LEGACY_SUPERUSER_ROLE_DESCRIPTOR.toXContent(builder, ToXContent.EMPTY_PARAMS, true)).streamInput(), + false + ); + } + final RoleReference.ApiKeyRoleType apiKeyRoleType = randomApiKeyRoleType(); + roleDescriptors = service.parseRoleDescriptors( + apiKeyId, + Map.of(LEGACY_SUPERUSER_ROLE_DESCRIPTOR.getName(), legacySuperUserRdMap), + apiKeyRoleType + ); + assertThat(roleDescriptors, hasSize(1)); + assertThat( + roleDescriptors.get(0), + equalTo( + apiKeyRoleType == RoleReference.ApiKeyRoleType.LIMITED_BY ? SUPERUSER_ROLE_DESCRIPTOR : LEGACY_SUPERUSER_ROLE_DESCRIPTOR + ) + ); } public void testParseRoleDescriptors() { ApiKeyService service = createApiKeyService(Settings.EMPTY); final String apiKeyId = randomAlphaOfLength(12); - List roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, null); + List roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, null, randomApiKeyRoleType()); assertTrue(roleDescriptors.isEmpty()); BytesReference roleBytes = new BytesArray("{\"a role\": {\"cluster\": [\"all\"]}}"); - roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, roleBytes); + roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, roleBytes, randomApiKeyRoleType()); assertEquals(1, roleDescriptors.size()); assertEquals("a role", roleDescriptors.get(0).getName()); assertArrayEquals(new String[] { "all" }, roleDescriptors.get(0).getClusterPrivileges()); @@ -635,12 +663,19 @@ public void testParseRoleDescriptors() { + "\"privileges\":[\"*\"],\"resources\":[\"*\"]}],\"run_as\":[\"*\"],\"metadata\":{\"_reserved\":true}," + "\"transient_metadata\":{}}}\n" ); - roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, roleBytes); + final RoleReference.ApiKeyRoleType apiKeyRoleType = randomApiKeyRoleType(); + roleDescriptors = service.parseRoleDescriptorsBytes(apiKeyId, roleBytes, apiKeyRoleType); assertEquals(2, roleDescriptors.size()); assertEquals( Set.of("reporting_user", "superuser"), roleDescriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toSet()) ); + assertThat( + roleDescriptors.get(1), + equalTo( + apiKeyRoleType == RoleReference.ApiKeyRoleType.LIMITED_BY ? SUPERUSER_ROLE_DESCRIPTOR : LEGACY_SUPERUSER_ROLE_DESCRIPTOR + ) + ); } public void testApiKeyServiceDisabled() throws Exception { @@ -1037,7 +1072,11 @@ public void testApiKeyDocCache() throws IOException, ExecutionException, Interru final BytesReference limitedByRoleDescriptorsBytes = service.getRoleDescriptorsBytesCache() .get(cachedApiKeyDoc.limitedByRoleDescriptorsHash); assertNotNull(limitedByRoleDescriptorsBytes); - final List limitedByRoleDescriptors = service.parseRoleDescriptorsBytes(docId, limitedByRoleDescriptorsBytes); + final List limitedByRoleDescriptors = service.parseRoleDescriptorsBytes( + docId, + limitedByRoleDescriptorsBytes, + RoleReference.ApiKeyRoleType.LIMITED_BY + ); assertEquals(1, limitedByRoleDescriptors.size()); assertEquals(SUPERUSER_ROLE_DESCRIPTOR, limitedByRoleDescriptors.get(0)); if (metadata == null) { @@ -1610,4 +1649,8 @@ private void checkAuthApiKeyMetadata(Object metadata, AuthenticationResult ); } } + + private RoleReference.ApiKeyRoleType randomApiKeyRoleType() { + return randomFrom(RoleReference.ApiKeyRoleType.values()); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 6d4cf5d5ea6fa..9e812a54c46d0 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -1374,9 +1374,9 @@ public void testApiKeyAuthUsesApiKeyService() throws Exception { assertThat(effectiveRoleDescriptors.get(), is(nullValue())); if (version == Version.CURRENT) { - verify(apiKeyService, times(1)).parseRoleDescriptorsBytes(anyString(), any(BytesReference.class)); + verify(apiKeyService, times(1)).parseRoleDescriptorsBytes(anyString(), any(BytesReference.class), any()); } else { - verify(apiKeyService, times(1)).parseRoleDescriptors(anyString(), anyMap()); + verify(apiKeyService, times(1)).parseRoleDescriptors(anyString(), anyMap(), any()); } assertThat(role.names().length, is(1)); assertThat(role.names()[0], containsString("user_role_")); @@ -1450,20 +1450,24 @@ public void testApiKeyAuthUsesApiKeyServiceWithScopedRole() throws Exception { if (version == Version.CURRENT) { verify(apiKeyService).parseRoleDescriptorsBytes( apiKeyId, - (BytesReference) authentication.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY) + (BytesReference) authentication.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY), + RoleReference.ApiKeyRoleType.ASSIGNED ); verify(apiKeyService).parseRoleDescriptorsBytes( apiKeyId, - (BytesReference) authentication.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) + (BytesReference) authentication.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY), + RoleReference.ApiKeyRoleType.LIMITED_BY ); } else { verify(apiKeyService).parseRoleDescriptors( apiKeyId, - (Map) authentication.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY) + (Map) authentication.getMetadata().get(API_KEY_ROLE_DESCRIPTORS_KEY), + RoleReference.ApiKeyRoleType.ASSIGNED ); verify(apiKeyService).parseRoleDescriptors( apiKeyId, - (Map) authentication.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY) + (Map) authentication.getMetadata().get(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY), + RoleReference.ApiKeyRoleType.LIMITED_BY ); } assertThat(role.names().length, is(1)); @@ -1510,7 +1514,7 @@ public void testGetRolesForRunAs() { final PlainActionFuture future1 = new PlainActionFuture<>(); compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication1).getAuthenticatingSubject(), future1); future1.actionGet(); - verify(apiKeyService).parseRoleDescriptorsBytes(apiKeyId, limitedByRoleDescriptorBytes); + verify(apiKeyService).parseRoleDescriptorsBytes(apiKeyId, limitedByRoleDescriptorBytes, RoleReference.ApiKeyRoleType.LIMITED_BY); // Service account run as final User authenticatedUser2 = new User("elastic/some-service"); @@ -1702,8 +1706,8 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() { compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication).getEffectiveSubject(), roleFuture); roleFuture.actionGet(); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); - verify(apiKeyService).parseRoleDescriptorsBytes("key-id-1", roleBytes); - verify(apiKeyService).parseRoleDescriptorsBytes("key-id-1", limitedByRoleBytes); + verify(apiKeyService).parseRoleDescriptorsBytes("key-id-1", roleBytes, RoleReference.ApiKeyRoleType.ASSIGNED); + verify(apiKeyService).parseRoleDescriptorsBytes("key-id-1", limitedByRoleBytes, RoleReference.ApiKeyRoleType.LIMITED_BY); // Different API key with the same roles should read from cache final Map metadata2 = new HashMap<>(); @@ -1723,7 +1727,7 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() { compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication).getEffectiveSubject(), roleFuture); roleFuture.actionGet(); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); - verify(apiKeyService, never()).parseRoleDescriptorsBytes(eq("key-id-2"), any(BytesReference.class)); + verify(apiKeyService, never()).parseRoleDescriptorsBytes(eq("key-id-2"), any(BytesReference.class), any()); // Different API key with the same limitedBy role should read from cache, new role should be built final BytesArray anotherRoleBytes = new BytesArray("{\"b role\": {\"cluster\": [\"manage_security\"]}}"); @@ -1744,7 +1748,7 @@ public void testCacheEntryIsReusedForIdenticalApiKeyRoles() { compositeRolesStore.getRole(AuthenticationContext.fromAuthentication(authentication).getEffectiveSubject(), roleFuture); roleFuture.actionGet(); assertThat(effectiveRoleDescriptors.get(), is(nullValue())); - verify(apiKeyService).parseRoleDescriptorsBytes("key-id-3", anotherRoleBytes); + verify(apiKeyService).parseRoleDescriptorsBytes("key-id-3", anotherRoleBytes, RoleReference.ApiKeyRoleType.ASSIGNED); } private Authentication createAuthentication() { diff --git a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java index 790b524085e89..a40f5959b2bd2 100644 --- a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java +++ b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java @@ -17,6 +17,7 @@ import org.elasticsearch.client.RestClient; import org.elasticsearch.cluster.metadata.DataStreamTestHelper; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.support.XContentMapValues; @@ -32,6 +33,7 @@ import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicy; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleStats; import org.hamcrest.Matcher; @@ -329,6 +331,90 @@ public void testServiceAccountApiKey() throws IOException { } } + public void testApiKeySuperuser() throws IOException { + if (isRunningAgainstOldCluster()) { + final Request createUserRequest = new Request("PUT", "/_security/user/api_key_super_creator"); + createUserRequest.setJsonEntity(""" + { + "password" : "l0ng-r4nd0m-p@ssw0rd", + "roles" : [ "superuser", "monitoring_user" ] + }"""); + client().performRequest(createUserRequest); + + // Create API key + final Request createApiKeyRequest = new Request("PUT", "/_security/api_key"); + createApiKeyRequest.setOptions( + RequestOptions.DEFAULT.toBuilder() + .addHeader( + "Authorization", + UsernamePasswordToken.basicAuthHeaderValue( + "api_key_super_creator", + new SecureString("l0ng-r4nd0m-p@ssw0rd".toCharArray()) + ) + ) + ); + createApiKeyRequest.setJsonEntity(""" + { + "name": "super_legacy_key" + }"""); + final Map createApiKeyResponse = entityAsMap(client().performRequest(createApiKeyRequest)); + final byte[] keyBytes = (createApiKeyResponse.get("id") + ":" + createApiKeyResponse.get("api_key")).getBytes( + StandardCharsets.UTF_8 + ); + final String apiKeyAuthHeader = "ApiKey " + Base64.getEncoder().encodeToString(keyBytes); + // Save the API key info across restart + final Request saveApiKeyRequest = new Request("PUT", "/api_keys/_doc/super_legacy_key"); + saveApiKeyRequest.setJsonEntity("{\"auth_header\":\"" + apiKeyAuthHeader + "\"}"); + assertOK(client().performRequest(saveApiKeyRequest)); + + if (getOldClusterVersion().before(Version.V_8_0_0)) { + final Request indexRequest = new Request("POST", ".security/_doc"); + indexRequest.setJsonEntity(""" + { + "doc_type": "foo" + }"""); + indexRequest.setOptions( + expectWarnings( + "this request accesses system indices: [.security-7], but in a future major " + + "version, direct access to system indices will be prevented by default" + ).toBuilder().addHeader("Authorization", apiKeyAuthHeader) + ); + assertOK(client().performRequest(indexRequest)); + } + } else { + final Request getRequest = new Request("GET", "/api_keys/_doc/super_legacy_key"); + final Map getResponseMap = responseAsMap(client().performRequest(getRequest)); + @SuppressWarnings("unchecked") + final String apiKeyAuthHeader = ((Map) getResponseMap.get("_source")).get("auth_header"); + + // read is ok + final Request searchRequest = new Request("GET", ".security/_search"); + searchRequest.setOptions( + expectWarnings( + "this request accesses system indices: [.security-7], but in a future major " + + "version, direct access to system indices will be prevented by default" + ).toBuilder().addHeader("Authorization", apiKeyAuthHeader) + ); + assertOK(client().performRequest(searchRequest)); + + // write must not be allowed + final Request indexRequest = new Request("POST", ".security/_doc"); + indexRequest.setJsonEntity(""" + { + "doc_type": "foo" + }"""); + indexRequest.setOptions( + expectWarnings( + "this request accesses system indices: [.security-7], but in a future major " + + "version, direct access to system indices will be prevented by default" + ).toBuilder().addHeader("Authorization", apiKeyAuthHeader) + ); + final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(indexRequest)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + assertThat(e.getMessage(), containsString("is unauthorized")); + } + } + /** * Tests that a RollUp job created on a old cluster is correctly restarted after the upgrade. */