diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java index e8e1a104d8c2c..c1eae86c9f18a 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java @@ -345,8 +345,9 @@ public static class IndexPrivilegeName { public static final String VIEW_INDEX_METADATA = "view_index_metadata"; public static final String MANAGE_FOLLOW_INDEX = "manage_follow_index"; public static final String MANAGE_ILM = "manage_ilm"; + public static final String CREATE_DOC = "create_doc"; public static final String[] ALL_ARRAY = new String[] { NONE, ALL, READ, READ_CROSS, CREATE, INDEX, DELETE, WRITE, MONITOR, MANAGE, - DELETE_INDEX, CREATE_INDEX, VIEW_INDEX_METADATA, MANAGE_FOLLOW_INDEX, MANAGE_ILM }; + DELETE_INDEX, CREATE_INDEX, VIEW_INDEX_METADATA, MANAGE_FOLLOW_INDEX, MANAGE_ILM, CREATE_DOC }; } } diff --git a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc index 7da8bdce87b3a..3c954a8d44bbd 100644 --- a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc @@ -96,6 +96,7 @@ A successful call returns an object with "cluster" and "index" fields. "index" : [ "all", "create", + "create_doc", "create_index", "delete", "delete_index", diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index 3448fe7509c95..0061ed4ce2c14 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -51,6 +51,8 @@ public final class IndexPrivilege extends Privilege { ClusterSearchShardsAction.NAME); private static final Automaton CREATE_AUTOMATON = patterns("indices:data/write/index*", "indices:data/write/bulk*", PutMappingAction.NAME); + private static final Automaton CREATE_DOC_AUTOMATON = patterns("indices:data/write/index", "indices:data/write/index[*", + "indices:data/write/index:op_type/create", "indices:data/write/bulk*", PutMappingAction.NAME); private static final Automaton INDEX_AUTOMATON = patterns("indices:data/write/index*", "indices:data/write/bulk*", "indices:data/write/update*", PutMappingAction.NAME); private static final Automaton DELETE_AUTOMATON = patterns("indices:data/write/delete*", "indices:data/write/bulk*"); @@ -77,6 +79,7 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege INDEX = new IndexPrivilege("index", INDEX_AUTOMATON); public static final IndexPrivilege DELETE = new IndexPrivilege("delete", DELETE_AUTOMATON); public static final IndexPrivilege WRITE = new IndexPrivilege("write", WRITE_AUTOMATON); + public static final IndexPrivilege CREATE_DOC = new IndexPrivilege("create_doc", CREATE_DOC_AUTOMATON); public static final IndexPrivilege MONITOR = new IndexPrivilege("monitor", MONITOR_AUTOMATON); public static final IndexPrivilege MANAGE = new IndexPrivilege("manage", MANAGE_AUTOMATON); public static final IndexPrivilege DELETE_INDEX = new IndexPrivilege("delete_index", DELETE_INDEX_AUTOMATON); @@ -97,6 +100,7 @@ public final class IndexPrivilege extends Privilege { .put("delete", DELETE) .put("write", WRITE) .put("create", CREATE) + .put("create_doc", CREATE_DOC) .put("delete_index", DELETE_INDEX) .put("view_index_metadata", VIEW_METADATA) .put("read_cross_cluster", READ_CROSS_CLUSTER) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index bd81d6db4743a..a9a971a091422 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -93,6 +93,8 @@ public class AuthorizationService { public static final String AUTHORIZATION_INFO_KEY = "_authz_info"; private static final AuthorizationInfo SYSTEM_AUTHZ_INFO = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { SystemUser.ROLE_NAME }); + private static final String IMPLIED_INDEX_ACTION = IndexAction.NAME + ":op_type/index"; + private static final String IMPLIED_CREATE_ACTION = IndexAction.NAME + ":op_type/create"; private static final Logger logger = LogManager.getLogger(AuthorizationService.class); @@ -536,8 +538,9 @@ private static String getAction(BulkItemRequest item) { final DocWriteRequest docWriteRequest = item.request(); switch (docWriteRequest.opType()) { case INDEX: + return IMPLIED_INDEX_ACTION; case CREATE: - return IndexAction.NAME; + return IMPLIED_CREATE_ACTION; case UPDATE: return UpdateAction.NAME; case DELETE: diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/CreateDocsIndexPrivilegeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/CreateDocsIndexPrivilegeTests.java new file mode 100644 index 0000000000000..edc9e7e4fd94b --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/CreateDocsIndexPrivilegeTests.java @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.integration; + +import org.elasticsearch.client.Request; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.junit.Before; + +import java.io.IOException; + +public class CreateDocsIndexPrivilegeTests extends AbstractPrivilegeTestCase { + private static final String INDEX_NAME = "index-1"; + private static final String CREATE_DOC_USER = "create_doc_user"; + private String jsonDoc = "{ \"name\" : \"elasticsearch\", \"body\": \"foo bar\" }"; + private static final String ROLES = + "all_indices_role:\n" + + " indices:\n" + + " - names: '*'\n" + + " privileges: [ all ]\n" + + "create_doc_role:\n" + + " indices:\n" + + " - names: '*'\n" + + " privileges: [ create_doc ]\n"; + + private static final String USERS_ROLES = + "all_indices_role:admin\n" + + "create_doc_role:" + CREATE_DOC_USER + "\n"; + + @Override + protected boolean addMockHttpTransport() { + return false; // enable http + } + + @Override + protected String configRoles() { + return super.configRoles() + "\n" + ROLES; + } + + @Override + protected String configUsers() { + final String usersPasswdHashed = new String(Hasher.resolve( + randomFrom("pbkdf2", "pbkdf2_1000", "bcrypt", "bcrypt9")).hash(new SecureString("passwd".toCharArray()))); + + return super.configUsers() + + "admin:" + usersPasswdHashed + "\n" + + CREATE_DOC_USER + ":" + usersPasswdHashed + "\n"; + } + + @Override + protected String configUsersRoles() { + return super.configUsersRoles() + USERS_ROLES; + } + + @Before + public void insertBaseDocumentsAsAdmin() throws Exception { + Request request = new Request("PUT", "/" + INDEX_NAME + "/_doc/1"); + request.setJsonEntity(jsonDoc); + request.addParameter("refresh", "true"); + assertAccessIsAllowed("admin", request); + } + + public void testCreateDocUserCanIndexNewDocumentsWithAutoGeneratedId() throws IOException { + assertAccessIsAllowed(CREATE_DOC_USER, "POST", "/" + INDEX_NAME + "/_doc", "{ \"foo\" : \"bar\" }"); + } + + public void testCreateDocUserCanIndexNewDocumentsWithExternalIdAndOpTypeIsCreate() throws IOException { + assertAccessIsAllowed(CREATE_DOC_USER, randomFrom("PUT", "POST"), "/" + INDEX_NAME + "/_doc/2?op_type=create", "{ \"foo\" : " + + "\"bar\" }"); + } + + public void testCreateDocUserIsDeniedToIndexNewDocumentsWithExternalIdAndOpTypeIsIndex() throws IOException { + assertAccessIsDenied(CREATE_DOC_USER, randomFrom("PUT", "POST"), "/" + INDEX_NAME + "/_doc/3", "{ \"foo\" : \"bar\" }"); + } + + public void testCreateDocUserIsDeniedToIndexUpdatesToExistingDocument() throws IOException { + assertAccessIsDenied(CREATE_DOC_USER, "POST", "/" + INDEX_NAME + "/_doc/1/_update", "{ \"doc\" : { \"foo\" : \"baz\" } }"); + assertAccessIsDenied(CREATE_DOC_USER, "PUT", "/" + INDEX_NAME + "/_doc/1", "{ \"foo\" : \"baz\" }"); + } + + public void testCreateDocUserCanIndexNewDocumentsWithAutoGeneratedIdUsingBulkApi() throws IOException { + assertAccessIsAllowed(CREATE_DOC_USER, randomFrom("PUT", "POST"), + "/" + INDEX_NAME + "/_bulk", "{ \"index\" : { } }\n{ \"foo\" : \"bar\" }\n"); + } + + public void testCreateDocUserCanIndexNewDocumentsWithAutoGeneratedIdAndOpTypeCreateUsingBulkApi() throws IOException { + assertAccessIsAllowed(CREATE_DOC_USER, randomFrom("PUT", "POST"), + "/" + INDEX_NAME + "/_bulk", "{ \"create\" : { } }\n{ \"foo\" : \"bar\" }\n"); + } + + public void testCreateDocUserCanIndexNewDocumentsWithExternalIdAndOpTypeIsCreateUsingBulkApi() throws IOException { + assertAccessIsAllowed(CREATE_DOC_USER, randomFrom("PUT", "POST"), + "/" + INDEX_NAME + "/_bulk", "{ \"create\" : { \"_id\" : \"4\" } }\n{ \"foo\" : \"bar\" }\n"); + } + + public void testCreateDocUserIsDeniedToIndexNewDocumentsWithExternalIdAndOpTypeIsIndexUsingBulkApi() throws IOException { + assertBodyHasAccessIsDenied(CREATE_DOC_USER, randomFrom("PUT", "POST"), + "/" + INDEX_NAME + "/_bulk", "{ \"index\" : { \"_id\" : \"5\" } }\n{ \"foo\" : \"bar\" }\n"); + } + + public void testCreateDocUserIsDeniedToIndexUpdatesToExistingDocumentUsingBulkApi() throws IOException { + assertBodyHasAccessIsDenied(CREATE_DOC_USER, randomFrom("PUT", "POST"), + "/" + INDEX_NAME + "/_bulk", "{ \"index\" : { \"_id\" : \"1\" } }\n{ \"doc\" : {\"foo\" : \"bazbaz\"} }\n"); + assertBodyHasAccessIsDenied(CREATE_DOC_USER, randomFrom("PUT", "POST"), + "/" + INDEX_NAME + "/_bulk", "{ \"update\" : { \"_id\" : \"1\" } }\n{ \"doc\" : {\"foo\" : \"bazbaz\"} }\n"); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 7de0ed7a1553f..2d0b5a391e0a1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -1193,16 +1193,16 @@ public void testAuthorizationOfIndividualBulkItems() throws IOException { eq(DeleteAction.NAME), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()), eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() })); verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication), - eq(IndexAction.NAME), eq("concrete-index"), eq(BulkItemRequest.class.getSimpleName()), + eq(IndexAction.NAME + ":op_type/index"), eq("concrete-index"), eq(BulkItemRequest.class.getSimpleName()), eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() })); verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication), - eq(IndexAction.NAME), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()), + eq(IndexAction.NAME + ":op_type/index"), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()), eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() })); verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_DENIED), eq(authentication), eq(DeleteAction.NAME), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()), eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() })); verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_DENIED), eq(authentication), - eq(IndexAction.NAME), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()), + eq(IndexAction.NAME + ":op_type/index"), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()), eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() })); verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), authzInfoRoles(new String[] { role.getName() })); // bulk request is allowed @@ -1236,7 +1236,7 @@ public void testAuthorizationOfIndividualBulkItemsWithDateMath() throws IOExcept eq(DeleteAction.NAME), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()), eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() })); verify(auditTrail, times(2)).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication), - eq(IndexAction.NAME), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()), + eq(IndexAction.NAME + ":op_type/index"), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()), eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() })); // bulk request is allowed verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request), diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml index dd36e6e603080..9ac2fdf23c9af 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml @@ -16,4 +16,4 @@ setup: # I would much prefer we could just check that specific entries are in the array, but we don't have # an assertion for that - length: { "cluster" : 30 } - - length: { "index" : 16 } + - length: { "index" : 17 }