Browse Source

Add "_storage" internal user (#95694)

This commit adds a new internal user to security. The "_storage" user
(`StorageInternalUser`) is intended for use when performing automatic
refreshes (and, in the future, similar actions) that may not be
permitted by the authenticated user.

Relates: #95506
Tim Vernum 2 years ago
parent
commit
9c61c0c742

+ 5 - 0
docs/changelog/95694.yaml

@@ -0,0 +1,5 @@
+pr: 95694
+summary: Add "_storage" internal user
+area: Security
+type: enhancement
+issues: []

+ 1 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUsers.java

@@ -51,6 +51,7 @@ public class InternalUsers {
             null /* CrossClusterAccessUser has a role descriptor, but it should never be resolved by this class */,
             CrossClusterAccessUser::is
         );
+        defineUser(StorageInternalUser.NAME, StorageInternalUser.INSTANCE, StorageInternalUser.ROLE_DESCRIPTOR, StorageInternalUser::is);
     }
 
     private static void defineUser(String name, User user, @Nullable RoleDescriptor roleDescriptor, Predicate<User> predicate) {

+ 55 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/StorageInternalUser.java

@@ -0,0 +1,55 @@
+/*
+ * 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.action.admin.indices.refresh.RefreshAction;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
+import org.elasticsearch.xpack.core.security.support.MetadataUtils;
+
+/**
+ * "Storage" internal user - used when the indexing/storage subsystem needs to perform actions on specific indices
+ * (that may not be permitted by the authenticated user)
+ */
+public class StorageInternalUser extends User {
+
+    public static final String NAME = UsernamesField.STORAGE_USER_NAME;
+    public static final RoleDescriptor ROLE_DESCRIPTOR = new RoleDescriptor(
+        UsernamesField.STORAGE_ROLE_NAME,
+        new String[] {},
+        new RoleDescriptor.IndicesPrivileges[] {
+            RoleDescriptor.IndicesPrivileges.builder()
+                .indices("*")
+                .privileges(RefreshAction.NAME + "*")
+                .allowRestrictedIndices(true)
+                .build() },
+        new String[] {},
+        MetadataUtils.DEFAULT_RESERVED_METADATA
+    );
+    public static final StorageInternalUser INSTANCE = new StorageInternalUser();
+
+    private StorageInternalUser() {
+        super(NAME, Strings.EMPTY_ARRAY);
+        assert enabled();
+        assert roles() != null && roles().length == 0;
+    }
+
+    @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);
+    }
+
+}

+ 2 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java

@@ -154,7 +154,8 @@ public class User implements ToXContentObject {
             || XPackSecurityUser.is(user)
             || SecurityProfileUser.is(user)
             || AsyncSearchUser.is(user)
-            || CrossClusterAccessUser.is(user);
+            || CrossClusterAccessUser.is(user)
+            || StorageInternalUser.is(user);
     }
 
     /** Write the given {@link User} */

+ 2 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java

@@ -30,6 +30,8 @@ public final class UsernamesField {
     public static final String APM_ROLE = "apm_system";
     public static final String ASYNC_SEARCH_NAME = "_async_search";
     public static final String ASYNC_SEARCH_ROLE = "_async_search";
+    public static final String STORAGE_USER_NAME = "_storage";
+    public static final String STORAGE_ROLE_NAME = "_storage";
 
     public static final String REMOTE_MONITORING_NAME = "remote_monitoring_user";
     public static final String REMOTE_MONITORING_COLLECTION_ROLE = "remote_monitoring_collector";

+ 6 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/InternalUsersTests.java

@@ -58,6 +58,12 @@ public class InternalUsersTests extends ESTestCase {
         assertThat(e, throwableWithMessage("should never try to get the roles for internal user [_cross_cluster_access]"));
     }
 
+    public void testStorageUser() {
+        assertThat(InternalUsers.getUser("_storage"), is(StorageInternalUser.INSTANCE));
+        assertThat(InternalUsers.getInternalUserName(StorageInternalUser.INSTANCE), is("_storage"));
+        assertThat(InternalUsers.getRoleDescriptor(StorageInternalUser.INSTANCE), is(StorageInternalUser.ROLE_DESCRIPTOR));
+    }
+
     public void testRegularUser() {
         var username = randomAlphaOfLengthBetween(4, 12);
         expectThrows(IllegalStateException.class, () -> InternalUsers.getUser(username));

+ 82 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/StorageInternalUserTests.java

@@ -0,0 +1,82 @@
+/*
+ * 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.apache.lucene.util.automaton.Automaton;
+import org.apache.lucene.util.automaton.CharacterRunAutomaton;
+import org.elasticsearch.Version;
+import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction;
+import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction;
+import org.elasticsearch.action.admin.indices.refresh.RefreshAction;
+import org.elasticsearch.action.admin.indices.refresh.TransportUnpromotableShardRefreshAction;
+import org.elasticsearch.action.bulk.BulkAction;
+import org.elasticsearch.action.get.GetAction;
+import org.elasticsearch.cluster.metadata.IndexAbstraction;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.core.security.authz.permission.ApplicationPermission;
+import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache;
+import org.elasticsearch.xpack.core.security.authz.permission.RemoteIndicesPermission;
+import org.elasticsearch.xpack.core.security.authz.permission.Role;
+import org.elasticsearch.xpack.core.security.authz.permission.RunAsPermission;
+import org.elasticsearch.xpack.core.security.authz.permission.SimpleRole;
+import org.elasticsearch.xpack.core.security.test.TestRestrictedIndices;
+
+import java.util.List;
+
+import static org.elasticsearch.xpack.core.security.test.TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+
+public class StorageInternalUserTests extends ESTestCase {
+
+    public void testRoleDescriptor() {
+        final FieldPermissionsCache fieldPermissionsCache = new FieldPermissionsCache(Settings.EMPTY);
+        final SimpleRole role = Role.buildFromRoleDescriptor(
+            StorageInternalUser.ROLE_DESCRIPTOR,
+            fieldPermissionsCache,
+            TestRestrictedIndices.RESTRICTED_INDICES
+        );
+
+        assertThat(role.cluster().privileges(), hasSize(0));
+        assertThat(role.runAs(), is(RunAsPermission.NONE));
+        assertThat(role.application(), is(ApplicationPermission.NONE));
+        assertThat(role.remoteIndices(), is(RemoteIndicesPermission.NONE));
+
+        final List<String> sampleAllowedActions = List.of(RefreshAction.NAME, TransportUnpromotableShardRefreshAction.NAME);
+        checkIndexAccess(role, randomFrom(sampleAllowedActions), randomAlphaOfLengthBetween(4, 8), true);
+        checkIndexAccess(role, randomFrom(sampleAllowedActions), ".ds-" + randomAlphaOfLengthBetween(4, 8), true);
+        checkIndexAccess(role, randomFrom(sampleAllowedActions), INTERNAL_SECURITY_MAIN_INDEX_7, true);
+
+        final List<String> sampleDeniedActions = List.of(GetAction.NAME, BulkAction.NAME, PutMappingAction.NAME, DeleteIndexAction.NAME);
+        checkIndexAccess(role, randomFrom(sampleDeniedActions), randomAlphaOfLengthBetween(4, 8), false);
+        checkIndexAccess(role, randomFrom(sampleDeniedActions), ".ds-" + randomAlphaOfLengthBetween(4, 8), false);
+        checkIndexAccess(role, randomFrom(sampleDeniedActions), INTERNAL_SECURITY_MAIN_INDEX_7, false);
+    }
+
+    private static void checkIndexAccess(SimpleRole role, String action, String indexName, boolean expectedValue) {
+        assertThat("Role " + role + " should grant " + action, role.indices().check(action), is(expectedValue));
+
+        final Automaton automaton = role.indices().allowedActionsMatcher(indexName);
+        assertThat(
+            "Role " + role + " should grant " + action + " access to " + indexName,
+            new CharacterRunAutomaton(automaton).run(action),
+            is(expectedValue)
+        );
+
+        final IndexMetadata metadata = IndexMetadata.builder(indexName).settings(indexSettings(Version.CURRENT, 1, 1)).build();
+        final IndexAbstraction.ConcreteIndex index = new IndexAbstraction.ConcreteIndex(metadata);
+        assertThat(
+            "Role " + role + " should grant " + action + " access to " + indexName,
+            role.allowedIndicesMatcher(action).test(index),
+            is(expectedValue)
+        );
+    }
+
+}