From a97bd3332e9b05c6c09cd43fde3e9c0e64be0fa9 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Thu, 21 Apr 2022 21:28:50 +1000 Subject: [PATCH] User Profile - Add new action origin and internal user (#86026) Profile documents are stored in a separate system index from the main security index. Hence a more scoped origin and internal user is better than the all powerful _xpack_security user. This PR adds _security_profile user that has privileges only over the profile index and updates all profile related actions to use it. --- docs/changelog/86026.yaml | 5 ++ .../xpack/core/ClientHelper.java | 1 + .../user/InternalUserSerializationHelper.java | 5 ++ .../security/user/SecurityProfileUser.java | 64 +++++++++++++++++ .../xpack/core/security/user/User.java | 7 +- .../core/security/user/UsernamesField.java | 2 + .../authc/AuthenticationTestHelper.java | 10 ++- .../security/authz/AuthorizationUtils.java | 5 ++ .../authz/store/CompositeRolesStore.java | 12 +++- .../security/profile/ProfileService.java | 47 +++++++------ .../support/SecuritySystemIndices.java | 3 +- .../authz/AuthorizationUtilsTests.java | 5 ++ .../authz/store/CompositeRolesStoreTests.java | 30 ++++++++ .../security/profile/ProfileServiceTests.java | 68 +++++++++++++++++++ 14 files changed, 240 insertions(+), 24 deletions(-) create mode 100644 docs/changelog/86026.yaml create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SecurityProfileUser.java diff --git a/docs/changelog/86026.yaml b/docs/changelog/86026.yaml new file mode 100644 index 0000000000000..c8357cc427da9 --- /dev/null +++ b/docs/changelog/86026.yaml @@ -0,0 +1,5 @@ +pr: 86026 +summary: User Profile - Add new action origin and internal user +area: Security +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java index 08705dafb8ab7..c194c5973857a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java @@ -174,6 +174,7 @@ private static String maybeRewriteSingleAuthenticationHeaderForVersion( @Deprecated public static final String ACTION_ORIGIN_TRANSIENT_NAME = ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME; public static final String SECURITY_ORIGIN = "security"; + public static final String SECURITY_PROFILE_ORIGIN = "security_profile"; public static final String WATCHER_ORIGIN = "watcher"; public static final String ML_ORIGIN = "ml"; public static final String INDEX_LIFECYCLE_ORIGIN = "index_lifecycle"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUserSerializationHelper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUserSerializationHelper.java index 409773a13d78b..8ed059ae708a5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUserSerializationHelper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUserSerializationHelper.java @@ -22,6 +22,8 @@ public static User readFrom(StreamInput input) throws IOException { return XPackUser.INSTANCE; } else if (XPackSecurityUser.is(username)) { return XPackSecurityUser.INSTANCE; + } else if (SecurityProfileUser.is(username)) { + return SecurityProfileUser.INSTANCE; } else if (AsyncSearchUser.is(username)) { return AsyncSearchUser.INSTANCE; } @@ -40,6 +42,9 @@ public static void writeTo(User user, StreamOutput output) throws IOException { } else if (XPackSecurityUser.is(user)) { output.writeBoolean(true); output.writeString(XPackSecurityUser.NAME); + } else if (SecurityProfileUser.is(user)) { + output.writeBoolean(true); + output.writeString(SecurityProfileUser.NAME); } else if (AsyncSearchUser.is(user)) { output.writeBoolean(true); output.writeString(AsyncSearchUser.NAME); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SecurityProfileUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SecurityProfileUser.java new file mode 100644 index 0000000000000..9001892abb8b7 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SecurityProfileUser.java @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.core.security.user; + +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; + +import java.util.Map; + +/** + * internal user that manages the security profile index. Has no cluster permission. + */ +public class SecurityProfileUser extends User { + + public static final String NAME = UsernamesField.SECURITY_PROFILE_NAME; + public static final SecurityProfileUser INSTANCE = new SecurityProfileUser(); + private static final String ROLE_NAME = UsernamesField.SECURITY_PROFILE_ROLE; + public static final RoleDescriptor ROLE_DESCRIPTOR = new RoleDescriptor( + ROLE_NAME, + null, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices(".security-profile", "/\\.security-profile-[0-9].*/") + .privileges("all") + .allowRestrictedIndices(true) + .build() }, + null, + null, + null, + MetadataUtils.DEFAULT_RESERVED_METADATA, + Map.of() + ); + + private SecurityProfileUser() { + super(NAME, ROLE_NAME); + // the following traits, and especially the run-as one, go with all the internal users + // TODO abstract in a base `InternalUser` class + assert false == isRunAs() : "cannot run-as the system user"; + assert enabled(); + assert roles() != null && roles().length == 1; + } + + @Override + public boolean equals(Object o) { + return INSTANCE == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + public static boolean is(User user) { + return INSTANCE.equals(user); + } + + public static boolean is(String principal) { + return NAME.equals(principal); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java index 39912a72f8f6b..1ec0494fbdeef 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java @@ -228,13 +228,18 @@ public static void writeTo(User user, StreamOutput output) throws IOException { } public static boolean isInternal(User user) { - return SystemUser.is(user) || XPackUser.is(user) || XPackSecurityUser.is(user) || AsyncSearchUser.is(user); + return SystemUser.is(user) + || XPackUser.is(user) + || XPackSecurityUser.is(user) + || SecurityProfileUser.is(user) + || AsyncSearchUser.is(user); } public static boolean isInternalUsername(String username) { return SystemUser.NAME.equals(username) || XPackUser.NAME.equals(username) || XPackSecurityUser.NAME.equals(username) + || SecurityProfileUser.NAME.equals(username) || AsyncSearchUser.NAME.equals(username); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java index 9ba7c01eb69e2..5153660bc9806 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java @@ -16,6 +16,8 @@ public final class UsernamesField { public static final String SYSTEM_ROLE = "_system"; public static final String XPACK_SECURITY_NAME = "_xpack_security"; public static final String XPACK_SECURITY_ROLE = "_xpack_security"; + public static final String SECURITY_PROFILE_NAME = "_security_profile"; + public static final String SECURITY_PROFILE_ROLE = "_security_profile"; public static final String XPACK_NAME = "_xpack"; public static final String XPACK_ROLE = "_xpack"; public static final String LOGSTASH_NAME = "logstash_system"; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java index f5d1fd8870aad..428bc5a39f899 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java @@ -26,6 +26,7 @@ import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; +import org.elasticsearch.xpack.core.security.user.SecurityProfileUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; @@ -227,7 +228,13 @@ public AuthenticationTestBuilder anonymous(User user) { public AuthenticationTestBuilder internal() { return internal( - ESTestCase.randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, AsyncSearchUser.INSTANCE) + ESTestCase.randomFrom( + SystemUser.INSTANCE, + XPackUser.INSTANCE, + XPackSecurityUser.INSTANCE, + SecurityProfileUser.INSTANCE, + AsyncSearchUser.INSTANCE + ) ); } @@ -404,6 +411,7 @@ public Authentication build(boolean runAsIfNotAlready) { SystemUser.INSTANCE, XPackUser.INSTANCE, XPackSecurityUser.INSTANCE, + SecurityProfileUser.INSTANCE, AsyncSearchUser.INSTANCE ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java index 7b3f44e9b984b..6725fb14d00bb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java @@ -15,6 +15,7 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; +import org.elasticsearch.xpack.core.security.user.SecurityProfileUser; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; @@ -36,6 +37,7 @@ import static org.elasticsearch.xpack.core.ClientHelper.ROLLUP_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.SEARCHABLE_SNAPSHOTS_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_PROFILE_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.STACK_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.TRANSFORM_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.WATCHER_ORIGIN; @@ -117,6 +119,9 @@ public static void switchUserBasedOnActionOriginAndExecute( case SECURITY_ORIGIN: securityContext.executeAsInternalUser(XPackSecurityUser.INSTANCE, Version.CURRENT, consumer); break; + case SECURITY_PROFILE_ORIGIN: + securityContext.executeAsInternalUser(SecurityProfileUser.INSTANCE, Version.CURRENT, consumer); + break; case WATCHER_ORIGIN: case ML_ORIGIN: case MONITORING_ORIGIN: diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index bba75559dc23d..21bf9b5e6aef4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -48,6 +48,7 @@ import org.elasticsearch.xpack.core.security.support.CacheIteratorHelper; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; +import org.elasticsearch.xpack.core.security.user.SecurityProfileUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; @@ -104,6 +105,7 @@ public class CompositeRolesStore { private final RoleDescriptorStore roleReferenceResolver; private final Role superuserRole; private final Role xpackSecurityRole; + private final Role securityProfileRole; private final Role xpackUserRole; private final Role asyncSearchUserRole; private final RestrictedIndices restrictedIndices; @@ -154,6 +156,7 @@ public void providersChanged() { this.superuserRole = Role.builder(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR, fieldPermissionsCache, this.restrictedIndices) .build(); xpackSecurityRole = Role.builder(XPackSecurityUser.ROLE_DESCRIPTOR, fieldPermissionsCache, this.restrictedIndices).build(); + securityProfileRole = Role.builder(SecurityProfileUser.ROLE_DESCRIPTOR, fieldPermissionsCache, this.restrictedIndices).build(); xpackUserRole = Role.builder(XPackUser.ROLE_DESCRIPTOR, fieldPermissionsCache, this.restrictedIndices).build(); asyncSearchUserRole = Role.builder(AsyncSearchUser.ROLE_DESCRIPTOR, fieldPermissionsCache, this.restrictedIndices).build(); @@ -220,6 +223,9 @@ Role tryGetRoleForInternalUser(Subject subject) { if (XPackSecurityUser.is(user)) { return xpackSecurityRole; } + if (SecurityProfileUser.is(user)) { + return securityProfileRole; + } if (AsyncSearchUser.is(user)) { return asyncSearchUserRole; } @@ -364,7 +370,8 @@ public void getRoleDescriptorsList(Subject subject, ActionListener tryGetRoleDescriptorForInternalUser(Subject subject) { + // Package private for testing + static Optional tryGetRoleDescriptorForInternalUser(Subject subject) { final User user = subject.getUser(); if (SystemUser.is(user)) { throw new IllegalArgumentException( @@ -377,6 +384,9 @@ private static Optional tryGetRoleDescriptorForInternalUser(Subj if (XPackSecurityUser.is(user)) { return Optional.of(XPackSecurityUser.ROLE_DESCRIPTOR); } + if (SecurityProfileUser.is(user)) { + return Optional.of(SecurityProfileUser.ROLE_DESCRIPTOR); + } if (AsyncSearchUser.is(user)) { return Optional.of(AsyncSearchUser.ROLE_DESCRIPTOR); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/profile/ProfileService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/profile/ProfileService.java index 7f172accfd3e7..58ab2a85c6bdb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/profile/ProfileService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/profile/ProfileService.java @@ -76,7 +76,7 @@ import java.util.stream.Collectors; import static org.elasticsearch.action.bulk.TransportSingleItemBulkWriteAction.toSingleItemBulkRequest; -import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_PROFILE_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; import static org.elasticsearch.xpack.core.security.authc.Authentication.isFileOrNativeRealm; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_PROFILE_ALIAS; @@ -192,7 +192,7 @@ public void suggestProfile(SuggestProfilesRequest request, ActionListener executeAsyncWithOrigin( client, - SECURITY_ORIGIN, + SECURITY_PROFILE_ORIGIN, SearchAction.INSTANCE, searchRequest, ActionListener.wrap(searchResponse -> { @@ -282,20 +282,26 @@ private void getVersionedDocument(String uid, ActionListener final GetRequest getRequest = new GetRequest(SECURITY_PROFILE_ALIAS, uidToDocId(uid)); frozenProfileIndex.checkIndexVersionThenExecute( listener::onFailure, - () -> executeAsyncWithOrigin(client, SECURITY_ORIGIN, GetAction.INSTANCE, getRequest, ActionListener.wrap(response -> { - if (false == response.isExists()) { - logger.debug("profile with uid [{}] does not exist", uid); - listener.onResponse(null); - return; - } - listener.onResponse( - new VersionedDocument( - buildProfileDocument(response.getSourceAsBytesRef()), - response.getPrimaryTerm(), - response.getSeqNo() - ) - ); - }, listener::onFailure)) + () -> executeAsyncWithOrigin( + client, + SECURITY_PROFILE_ORIGIN, + GetAction.INSTANCE, + getRequest, + ActionListener.wrap(response -> { + if (false == response.isExists()) { + logger.debug("profile with uid [{}] does not exist", uid); + listener.onResponse(null); + return; + } + listener.onResponse( + new VersionedDocument( + buildProfileDocument(response.getSourceAsBytesRef()), + response.getPrimaryTerm(), + response.getSeqNo() + ) + ); + }, listener::onFailure) + ) ); }); } @@ -335,7 +341,7 @@ void searchVersionedDocumentForSubject(Subject subject, ActionListener executeAsyncWithOrigin( client, - SECURITY_ORIGIN, + SECURITY_PROFILE_ORIGIN, SearchAction.INSTANCE, searchRequest, ActionListener.wrap(searchResponse -> { @@ -407,7 +413,7 @@ private void createNewProfile(Subject subject, String uid, ActionListener executeAsyncWithOrigin( client, - SECURITY_ORIGIN, + SECURITY_PROFILE_ORIGIN, BulkAction.INSTANCE, bulkRequest, TransportSingleItemBulkWriteAction.wrapBulkResponse(ActionListener.wrap(indexResponse -> { @@ -553,12 +559,13 @@ private UpdateRequest buildUpdateRequest( return updateRequestBuilder.request(); } - private void doUpdate(UpdateRequest updateRequest, ActionListener listener) { + // Package private for testing + void doUpdate(UpdateRequest updateRequest, ActionListener listener) { profileIndex.prepareIndexIfNeededThenExecute( listener::onFailure, () -> executeAsyncWithOrigin( client, - SECURITY_ORIGIN, + SECURITY_PROFILE_ORIGIN, UpdateAction.INSTANCE, updateRequest, ActionListener.wrap(updateResponse -> { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecuritySystemIndices.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecuritySystemIndices.java index 8b3126724eb67..db71f595cff76 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecuritySystemIndices.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecuritySystemIndices.java @@ -27,6 +27,7 @@ import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_PROFILE_ORIGIN; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_VERSION_STRING; /** @@ -736,7 +737,7 @@ private SystemIndexDescriptor getSecurityProfileIndexDescriptor() { .setAliasName(SECURITY_PROFILE_ALIAS) .setIndexFormat(INTERNAL_PROFILE_INDEX_FORMAT) .setVersionMetaKey(SECURITY_VERSION_STRING) - .setOrigin(SECURITY_ORIGIN) + .setOrigin(SECURITY_PROFILE_ORIGIN) .setThreadPools(ExecutorNames.CRITICAL_SYSTEM_INDEX_THREAD_POOLS) .build(); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java index 43c5f1e8b6c31..10b588920b4e8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; +import org.elasticsearch.xpack.core.security.user.SecurityProfileUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; @@ -111,6 +112,10 @@ public void testSwitchAndExecuteXpackSecurityUser() throws Exception { assertSwitchBasedOnOriginAndExecute(ClientHelper.SECURITY_ORIGIN, XPackSecurityUser.INSTANCE); } + public void testSwitchAndExecuteSecurityProfileUser() throws Exception { + assertSwitchBasedOnOriginAndExecute(ClientHelper.SECURITY_PROFILE_ORIGIN, SecurityProfileUser.INSTANCE); + } + public void testSwitchAndExecuteXpackUser() throws Exception { for (String origin : Arrays.asList( ClientHelper.ML_ORIGIN, 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 b05fd0c3a509b..262253605ef6d 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 @@ -81,6 +81,7 @@ import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.AsyncSearchUser; +import org.elasticsearch.xpack.core.security.user.SecurityProfileUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; @@ -131,6 +132,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; @@ -1910,6 +1912,30 @@ public void testXPackSecurityUserCanAccessAnyIndex() { } } + public void testSecurityProfileUserHasAccessForOnlyProfileIndex() { + for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) { + Predicate predicate = getSecurityProfileRole().indices().allowedIndicesMatcher(action); + + List.of( + ".security-profile", + ".security-profile-8", + ".security-profile-" + randomIntBetween(0, 16) + randomAlphaOfLengthBetween(0, 10) + ).forEach(name -> assertThat(predicate.test(mockIndexAbstraction(name)), is(true))); + + List.of( + ".security-profile" + randomAlphaOfLengthBetween(1, 10), + ".security-profile-" + randomAlphaOfLengthBetween(1, 10), + ".security", + ".security-" + randomIntBetween(0, 16) + randomAlphaOfLengthBetween(0, 10), + "." + randomAlphaOfLengthBetween(1, 20) + ).forEach(name -> assertThat(predicate.test(mockIndexAbstraction(name)), is(false))); + } + + final Subject subject = mock(Subject.class); + when(subject.getUser()).thenReturn(SecurityProfileUser.INSTANCE); + assertThat(CompositeRolesStore.tryGetRoleDescriptorForInternalUser(subject).get().getClusterPrivileges(), emptyArray()); + } + public void testXPackUserCanAccessNonRestrictedIndices() { for (String action : Arrays.asList(GetAction.NAME, DeleteAction.NAME, SearchAction.NAME, IndexAction.NAME)) { Predicate predicate = getXPackUserRole().indices().allowedIndicesMatcher(action); @@ -2047,6 +2073,10 @@ private Role getXPackSecurityRole() { return getInternalUserRole(XPackSecurityUser.INSTANCE); } + private Role getSecurityProfileRole() { + return getInternalUserRole(SecurityProfileUser.INSTANCE); + } + private Role getXPackUserRole() { return getInternalUserRole(XPackUser.INSTANCE); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/profile/ProfileServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/profile/ProfileServiceTests.java index f86b3cd153770..232e539928ca4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/profile/ProfileServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/profile/ProfileServiceTests.java @@ -9,13 +9,21 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.bulk.BulkAction; +import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.get.GetAction; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchAction; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.update.UpdateAction; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.internal.Client; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -37,6 +45,7 @@ import org.elasticsearch.xpack.core.security.action.profile.Profile; import org.elasticsearch.xpack.core.security.action.profile.SuggestProfilesRequest; import org.elasticsearch.xpack.core.security.action.profile.SuggestProfilesRequestTests; +import org.elasticsearch.xpack.core.security.action.profile.SuggestProfilesResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; import org.elasticsearch.xpack.core.security.authc.AuthenticationTests; @@ -57,13 +66,16 @@ import java.util.Map; import java.util.Set; +import static org.elasticsearch.common.util.concurrent.ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME; import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; +import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_PROFILE_ORIGIN; import static org.elasticsearch.xpack.security.Security.SECURITY_CRYPTO_THREAD_POOL_NAME; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_PROFILE_ALIAS; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; @@ -145,6 +157,7 @@ public void stopThreadPool() { public void testGetProfileByUid() { final String uid = randomAlphaOfLength(20); doAnswer(invocation -> { + assertThat(threadPool.getThreadContext().getTransient(ACTION_ORIGIN_TRANSIENT_NAME), equalTo(SECURITY_PROFILE_ORIGIN)); final GetRequest getRequest = (GetRequest) invocation.getArguments()[1]; @SuppressWarnings("unchecked") final ActionListener listener = (ActionListener) invocation.getArguments()[2]; @@ -297,6 +310,61 @@ public void testBuildSearchRequest() { } } + // Note this method is to test the origin is switched security_profile for all profile related actions. + // The actual result of the action is not relevant as long as the action is performed with the correct origin. + // Therefore, exceptions (used in this test) work as good as full successful responses. + public void testSecurityProfileOrigin() { + // Activate profile + doAnswer(invocation -> { + assertThat(threadPool.getThreadContext().getTransient(ACTION_ORIGIN_TRANSIENT_NAME), equalTo(SECURITY_PROFILE_ORIGIN)); + @SuppressWarnings("unchecked") + final ActionListener listener = (ActionListener) invocation.getArguments()[2]; + listener.onResponse(SearchResponse.empty(() -> 1L, SearchResponse.Clusters.EMPTY)); + return null; + }).when(client).execute(eq(SearchAction.INSTANCE), any(SearchRequest.class), anyActionListener()); + + when(client.prepareIndex(SECURITY_PROFILE_ALIAS)).thenReturn( + new IndexRequestBuilder(client, IndexAction.INSTANCE, SECURITY_PROFILE_ALIAS) + ); + + final RuntimeException expectedException = new RuntimeException("expected"); + doAnswer(invocation -> { + assertThat(threadPool.getThreadContext().getTransient(ACTION_ORIGIN_TRANSIENT_NAME), equalTo(SECURITY_PROFILE_ORIGIN)); + final ActionListener listener = (ActionListener) invocation.getArguments()[2]; + listener.onFailure(expectedException); + return null; + }).when(client).execute(eq(BulkAction.INSTANCE), any(BulkRequest.class), anyActionListener()); + + final PlainActionFuture future1 = new PlainActionFuture<>(); + profileService.activateProfile(AuthenticationTestHelper.builder().realm().build(), future1); + final RuntimeException e1 = expectThrows(RuntimeException.class, future1::actionGet); + assertThat(e1, is(expectedException)); + + // Update + doAnswer(invocation -> { + assertThat(threadPool.getThreadContext().getTransient(ACTION_ORIGIN_TRANSIENT_NAME), equalTo(SECURITY_PROFILE_ORIGIN)); + final ActionListener listener = (ActionListener) invocation.getArguments()[2]; + listener.onFailure(expectedException); + return null; + }).when(client).execute(eq(UpdateAction.INSTANCE), any(UpdateRequest.class), anyActionListener()); + final PlainActionFuture future2 = new PlainActionFuture<>(); + profileService.doUpdate(mock(UpdateRequest.class), future2); + final RuntimeException e2 = expectThrows(RuntimeException.class, future2::actionGet); + assertThat(e2, is(expectedException)); + + // Suggest + doAnswer(invocation -> { + assertThat(threadPool.getThreadContext().getTransient(ACTION_ORIGIN_TRANSIENT_NAME), equalTo(SECURITY_PROFILE_ORIGIN)); + final ActionListener listener = (ActionListener) invocation.getArguments()[2]; + listener.onFailure(expectedException); + return null; + }).when(client).execute(eq(SearchAction.INSTANCE), any(SearchRequest.class), anyActionListener()); + final PlainActionFuture future3 = new PlainActionFuture<>(); + profileService.suggestProfile(new SuggestProfilesRequest(Set.of(), "", 1, null), future3); + final RuntimeException e3 = expectThrows(RuntimeException.class, future3::actionGet); + assertThat(e3, is(expectedException)); + } + private void mockGetRequest(String uid, long lastSynchronized) { final String source = SAMPLE_PROFILE_DOCUMENT_TEMPLATE.formatted(uid, lastSynchronized);