Add 'create_doc' index privilege (#45806) (#47645)

Use case:
User with `create_doc` index privilege will be allowed to only index new documents
either via Index API or Bulk API.

There are two cases that we need to think:
- **User indexing a new document without specifying an Id.**
   For this ES auto generates an Id and now ES version 7.5.0 onwards defaults to `op_type` `create` we just need to authorize on the `op_type`.
- **User indexing a new document with an Id.**
   This is problematic as we do not know whether a document with Id exists or not.
   If the `op_type` is `create` then we can assume the user is trying to add a document, if it exists it is going to throw an error from the index engine.

Given these both cases, we can safely authorize based on the `op_type` value. If the value is `create` then the user with `create_doc` privilege is authorized to index new documents.

In the `AuthorizationService` when authorizing a bulk request, we check the implied action.
This code changes that to append the `:op_type/index` or `:op_type/create`
to indicate the implied index action.
This commit is contained in:
Yogesh Gaikwad 2019-10-07 23:58:44 +11:00 committed by GitHub
parent 7c862fe71f
commit b6d1d2e6ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 128 additions and 7 deletions

View File

@ -345,8 +345,9 @@ public final class Role {
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 };
}
}

View File

@ -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",

View File

@ -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)

View File

@ -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 @@ public class AuthorizationService {
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:

View File

@ -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");
}
}

View File

@ -1193,16 +1193,16 @@ public class AuthorizationServiceTests extends ESTestCase {
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 class AuthorizationServiceTests extends ESTestCase {
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),

View File

@ -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 }