Browse Source

Add elastic/enterprise-search-server service account (#83325)

* Add elastic/enterprise-search-server service account

* Remove overlapping index privileges

* Linting

* Remove cluster privilege already covered by manage

* Skip test

* Reorder assertions in test

* Update docs/changelog/83325.yaml
Ioana Tagirta 3 years ago
parent
commit
8487b0344a

+ 5 - 0
docs/changelog/83325.yaml

@@ -0,0 +1,5 @@
+pr: 83325
+summary: Add elastic/enterprise-search-server service account
+area: Authorization
+type: enhancement
+issues: []

+ 3 - 0
x-pack/docs/en/security/authentication/service-accounts.asciidoc

@@ -51,6 +51,9 @@ communicate with {es}.
 `elastic/kibana`:: The service account used by {kib} to communicate with
 {es}.
 
+`elastic/enterprise-search-server`:: The service account used by Enterprise Search
+to communicate with {es}.
+
 // tag::service-accounts-usage[]
 IMPORTANT: Do not attempt to use service accounts for authenticating individual
 users. Service accounts can only be authenticated with service tokens, which are

+ 1 - 0
x-pack/plugin/build.gradle

@@ -113,6 +113,7 @@ tasks.named("yamlRestTestV7CompatTransform").configure{ task ->
   task.skipTest("indices.freeze/20_stats/Translog stats on frozen indices", "#70192 -- the freeze index API is removed from 8.0")
   task.skipTest("indices.freeze/10_basic/Basic", "#70192 -- the freeze index API is removed from 8.0")
   task.skipTest("indices.freeze/10_basic/Test index options", "#70192 -- the freeze index API is removed from 8.0")
+  task.skipTest("service_accounts/10_basic/Test get service accounts", "new service accounts are added")
 
   task.replaceValueInMatch("_type", "_doc")
   task.addAllowedWarningRegex("\\[types removal\\].*")

+ 49 - 0
x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java

@@ -144,6 +144,42 @@ public class ServiceAccountIT extends ESRestTestCase {
             }
           }""";
 
+    private static final String ELASTIC_ENTERPRISE_SEARCH_SERVER_ROLE_DESCRIPTOR = """
+        {
+            "cluster": [
+                "manage",
+                "manage_security"
+            ],
+            "indices": [
+                {
+                    "names": [
+                        ".ent-search-*",
+                        ".monitoring-ent-search-*",
+                        "metricbeat-ent-search-*",
+                        "enterprise-search-*",
+                        "logs-app_search.analytics-default",
+                        "logs-enterprise_search.api-default",
+                        "logs-app_search.search_relevance_suggestions-default",
+                        "logs-crawler-default",
+                        "logs-workplace_search.analytics-default",
+                        "logs-workplace_search.content_events-default"
+                    ],
+                    "privileges": [
+                        "manage",
+                        "read",
+                        "write"
+                    ],
+                    "allow_restricted_indices": false
+                }
+            ],
+            "applications": [],
+            "run_as": [],
+            "metadata": {},
+            "transient_metadata": {
+                "enabled": true
+            }
+        }""";
+
     @BeforeClass
     public static void init() throws URISyntaxException, FileNotFoundException {
         URL resource = ServiceAccountIT.class.getResource("/ssl/ca.crt");
@@ -199,6 +235,19 @@ public class ServiceAccountIT extends ESRestTestCase {
             )
         );
 
+        final Request getServiceAccountRequestEnterpriseSearchService = new Request(
+            "GET",
+            "_security/service/elastic/enterprise-search-server"
+        );
+        final Response getServiceAccountResponseEnterpriseSearchService = client().performRequest(
+            getServiceAccountRequestEnterpriseSearchService
+        );
+        assertServiceAccountRoleDescriptor(
+            getServiceAccountResponseEnterpriseSearchService,
+            "elastic/enterprise-search-server",
+            ELASTIC_ENTERPRISE_SEARCH_SERVER_ROLE_DESCRIPTOR
+        );
+
         final String requestPath = "_security/service/" + randomFrom("foo", "elastic/foo", "foo/bar");
         final Request getServiceAccountRequest4 = new Request("GET", requestPath);
         final Response getServiceAccountResponse4 = client().performRequest(getServiceAccountRequest4);

+ 30 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java

@@ -22,6 +22,35 @@ final class ElasticServiceAccounts {
 
     static final String NAMESPACE = "elastic";
 
+    private static final ServiceAccount ENTERPRISE_SEARCH_ACCOUNT = new ElasticServiceAccount(
+        "enterprise-search-server",
+        new RoleDescriptor(
+            NAMESPACE + "/enterprise-search-server",
+            new String[] { "manage", "manage_security" },
+            new RoleDescriptor.IndicesPrivileges[] {
+                RoleDescriptor.IndicesPrivileges.builder()
+                    .indices(
+                        ".ent-search-*",
+                        ".monitoring-ent-search-*",
+                        "metricbeat-ent-search-*",
+                        "enterprise-search-*",
+                        "logs-app_search.analytics-default",
+                        "logs-enterprise_search.api-default",
+                        "logs-app_search.search_relevance_suggestions-default",
+                        "logs-crawler-default",
+                        "logs-workplace_search.analytics-default",
+                        "logs-workplace_search.content_events-default"
+                    )
+                    .privileges("manage", "read", "write")
+                    .build() },
+            null,
+            null,
+            null,
+            null,
+            null
+        )
+    );
+
     private static final ServiceAccount FLEET_ACCOUNT = new ElasticServiceAccount(
         "fleet-server",
         new RoleDescriptor(
@@ -71,7 +100,7 @@ final class ElasticServiceAccounts {
         ReservedRolesStore.kibanaSystemRoleDescriptor(NAMESPACE + "/kibana")
     );
 
-    static final Map<String, ServiceAccount> ACCOUNTS = List.of(FLEET_ACCOUNT, KIBANA_SYSTEM_ACCOUNT)
+    static final Map<String, ServiceAccount> ACCOUNTS = List.of(ENTERPRISE_SEARCH_ACCOUNT, FLEET_ACCOUNT, KIBANA_SYSTEM_ACCOUNT)
         .stream()
         .collect(Collectors.toMap(a -> a.id().asPrincipal(), Function.identity()));
 

+ 2 - 2
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java

@@ -45,12 +45,12 @@ public class TransportGetServiceAccountActionTests extends ESTestCase {
         final PlainActionFuture<GetServiceAccountResponse> future1 = new PlainActionFuture<>();
         transportGetServiceAccountAction.doExecute(mock(Task.class), request1, future1);
         final GetServiceAccountResponse getServiceAccountResponse1 = future1.actionGet();
-        assertThat(getServiceAccountResponse1.getServiceAccountInfos().length, equalTo(2));
+        assertThat(getServiceAccountResponse1.getServiceAccountInfos().length, equalTo(3));
         assertThat(
             Arrays.stream(getServiceAccountResponse1.getServiceAccountInfos())
                 .map(ServiceAccountInfo::getPrincipal)
                 .collect(Collectors.toList()),
-            containsInAnyOrder("elastic/fleet-server", "elastic/kibana")
+            containsInAnyOrder("elastic/enterprise-search-server", "elastic/fleet-server", "elastic/kibana")
         );
 
         final GetServiceAccountRequest request2 = new GetServiceAccountRequest("elastic", "fleet-server");

+ 79 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java

@@ -7,12 +7,18 @@
 
 package org.elasticsearch.xpack.security.authc.service;
 
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction;
+import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction;
 import org.elasticsearch.action.admin.indices.create.AutoCreateAction;
 import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
 import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction;
 import org.elasticsearch.action.admin.indices.mapping.put.AutoPutMappingAction;
+import org.elasticsearch.action.admin.indices.refresh.RefreshAction;
 import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsAction;
 import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction;
+import org.elasticsearch.action.admin.indices.template.delete.DeleteIndexTemplateAction;
+import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesAction;
+import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateAction;
 import org.elasticsearch.action.bulk.BulkAction;
 import org.elasticsearch.action.delete.DeleteAction;
 import org.elasticsearch.action.get.GetAction;
@@ -25,6 +31,8 @@ import org.elasticsearch.cluster.metadata.IndexAbstraction;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.transport.TransportRequest;
+import org.elasticsearch.xpack.core.ilm.action.GetLifecycleAction;
+import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction;
 import org.elasticsearch.xpack.core.ml.action.CloseJobAction;
 import org.elasticsearch.xpack.core.ml.action.DeleteCalendarAction;
 import org.elasticsearch.xpack.core.ml.action.DeleteCalendarEventAction;
@@ -87,12 +95,15 @@ import org.elasticsearch.xpack.core.ml.action.UpdateModelSnapshotAction;
 import org.elasticsearch.xpack.core.ml.action.UpdateProcessAction;
 import org.elasticsearch.xpack.core.ml.action.ValidateDetectorAction;
 import org.elasticsearch.xpack.core.ml.action.ValidateJobConfigAction;
+import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction;
 import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction;
 import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest;
 import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyAction;
 import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyRequest;
 import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction;
 import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyRequest;
+import org.elasticsearch.xpack.core.security.action.role.PutRoleAction;
+import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
 import org.elasticsearch.xpack.core.security.authc.Authentication;
 import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
 import org.elasticsearch.xpack.core.security.authz.permission.Role;
@@ -289,6 +300,74 @@ public class ElasticServiceAccountsTests extends ESTestCase {
         );
     }
 
+    public void testElasticEnterpriseSearchServerAccount() {
+        final Role role = Role.builder(
+            ElasticServiceAccounts.ACCOUNTS.get("elastic/enterprise-search-server").roleDescriptor(),
+            null,
+            RESTRICTED_INDICES_AUTOMATON
+        ).build();
+
+        final Authentication authentication = mock(Authentication.class);
+        final TransportRequest request = mock(TransportRequest.class);
+
+        // manage
+        assertThat(role.cluster().check(ClusterUpdateSettingsAction.NAME, request, authentication), is(true));
+
+        // manage_security
+        assertThat(
+            role.cluster()
+                .check(CreateApiKeyAction.NAME, new CreateApiKeyRequest(randomAlphaOfLengthBetween(3, 8), null, null), authentication),
+            is(true)
+        );
+        assertThat(role.cluster().check(GetApiKeyAction.NAME, GetApiKeyRequest.forOwnedApiKeys(), authentication), is(true));
+        assertThat(role.cluster().check(InvalidateApiKeyAction.NAME, InvalidateApiKeyRequest.forOwnedApiKeys(), authentication), is(true));
+
+        assertThat(role.cluster().check(PutUserAction.NAME, request, authentication), is(true));
+        assertThat(role.cluster().check(PutRoleAction.NAME, request, authentication), is(true));
+
+        // manage_index_templates
+        assertThat(role.cluster().check(PutIndexTemplateAction.NAME, request, authentication), is(true));
+        assertThat(role.cluster().check(GetIndexTemplatesAction.NAME, request, authentication), is(true));
+        assertThat(role.cluster().check(DeleteIndexTemplateAction.NAME, request, authentication), is(true));
+
+        // monitoring
+        assertThat(role.cluster().check(MonitoringBulkAction.NAME, request, authentication), is(true));
+        assertThat(role.cluster().check(ClusterHealthAction.NAME, request, authentication), is(true));
+
+        // manage_ilm
+        assertThat(role.cluster().check(GetLifecycleAction.NAME, request, authentication), is(true));
+        assertThat(role.cluster().check(PutLifecycleAction.NAME, request, authentication), is(true));
+
+        List.of(
+            ".ent-search-" + randomAlphaOfLengthBetween(1, 20),
+            ".monitoring-ent-search-" + randomAlphaOfLengthBetween(1, 20),
+            "metricbeat-ent-search-" + randomAlphaOfLengthBetween(1, 20),
+            "enterprise-search-" + randomAlphaOfLengthBetween(1, 20),
+            "logs-app_search.analytics-default",
+            "logs-enterprise_search.api-default",
+            "logs-app_search.search_relevance_suggestions-default",
+            "logs-crawler-default",
+            "logs-workplace_search.analytics-default",
+            "logs-workplace_search.content_events-default"
+        ).forEach(index -> {
+            final IndexAbstraction enterpriseSearchIndex = mockIndexAbstraction(index);
+            assertThat(role.indices().allowedIndicesMatcher(AutoCreateAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(BulkAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(MultiGetAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(IndicesStatsAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher(RefreshAction.NAME).test(enterpriseSearchIndex), is(true));
+            assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(enterpriseSearchIndex), is(false));
+        });
+    }
+
     private IndexAbstraction mockIndexAbstraction(String name) {
         IndexAbstraction mock = mock(IndexAbstraction.class);
         when(mock.getName()).thenReturn(name);

+ 4 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java

@@ -96,7 +96,10 @@ public class ServiceAccountServiceTests extends ESTestCase {
     }
 
     public void testGetServiceAccountPrincipals() {
-        assertThat(ServiceAccountService.getServiceAccountPrincipals(), containsInAnyOrder("elastic/fleet-server", "elastic/kibana"));
+        assertThat(
+            ServiceAccountService.getServiceAccountPrincipals(),
+            containsInAnyOrder("elastic/enterprise-search-server", "elastic/fleet-server", "elastic/kibana")
+        );
     }
 
     public void testTryParseToken() throws IOException, IllegalAccessException {

+ 21 - 2
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml

@@ -20,18 +20,27 @@ teardown:
         name: api-token-kibana
         ignore: 404
 
+  - do:
+      security.delete_service_token:
+        namespace: elastic
+        service: enterprise-search-server
+        name: api-token-enterprise-search-server
+        ignore: 404
+
 ---
 "Test get service accounts":
   - do:
       security.get_service_accounts: {}
-  - length: { '': 2 }
+  - length: { '': 3 }
+  - is_true: "elastic/enterprise-search-server"
   - is_true: "elastic/fleet-server"
   - is_true: "elastic/kibana"
 
   - do:
       security.get_service_accounts:
         namespace: elastic
-  - length: { '': 2 }
+  - length: { '': 3 }
+  - is_true: "elastic/enterprise-search-server"
   - is_true: "elastic/fleet-server"
   - is_true: "elastic/kibana"
 
@@ -66,6 +75,16 @@ teardown:
   - match: { "token.name": "api-token-kibana" }
   - set: { "token.value": service_token_kibana }
 
+  - do:
+      security.create_service_token:
+        namespace: elastic
+        service: enterprise-search-server
+        name: api-token-enterprise-search-server
+
+  - is_true: created
+  - match: { "token.name": "api-token-enterprise-search-server" }
+  - set: { "token.value": service_token_enterprise_search_server }
+
   - do:
       headers:
         Authorization: Bearer ${service_token_fleet}