Skip to content

Commit b6d1d2e

Browse files
authored
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.
1 parent 7c862fe commit b6d1d2e

File tree

7 files changed

+128
-7
lines changed

7 files changed

+128
-7
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,9 @@ public static class IndexPrivilegeName {
345345
public static final String VIEW_INDEX_METADATA = "view_index_metadata";
346346
public static final String MANAGE_FOLLOW_INDEX = "manage_follow_index";
347347
public static final String MANAGE_ILM = "manage_ilm";
348+
public static final String CREATE_DOC = "create_doc";
348349
public static final String[] ALL_ARRAY = new String[] { NONE, ALL, READ, READ_CROSS, CREATE, INDEX, DELETE, WRITE, MONITOR, MANAGE,
349-
DELETE_INDEX, CREATE_INDEX, VIEW_INDEX_METADATA, MANAGE_FOLLOW_INDEX, MANAGE_ILM };
350+
DELETE_INDEX, CREATE_INDEX, VIEW_INDEX_METADATA, MANAGE_FOLLOW_INDEX, MANAGE_ILM, CREATE_DOC };
350351
}
351352

352353
}

x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ A successful call returns an object with "cluster" and "index" fields.
9696
"index" : [
9797
"all",
9898
"create",
99+
"create_doc",
99100
"create_index",
100101
"delete",
101102
"delete_index",

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public final class IndexPrivilege extends Privilege {
5151
ClusterSearchShardsAction.NAME);
5252
private static final Automaton CREATE_AUTOMATON = patterns("indices:data/write/index*", "indices:data/write/bulk*",
5353
PutMappingAction.NAME);
54+
private static final Automaton CREATE_DOC_AUTOMATON = patterns("indices:data/write/index", "indices:data/write/index[*",
55+
"indices:data/write/index:op_type/create", "indices:data/write/bulk*", PutMappingAction.NAME);
5456
private static final Automaton INDEX_AUTOMATON =
5557
patterns("indices:data/write/index*", "indices:data/write/bulk*", "indices:data/write/update*", PutMappingAction.NAME);
5658
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 {
7779
public static final IndexPrivilege INDEX = new IndexPrivilege("index", INDEX_AUTOMATON);
7880
public static final IndexPrivilege DELETE = new IndexPrivilege("delete", DELETE_AUTOMATON);
7981
public static final IndexPrivilege WRITE = new IndexPrivilege("write", WRITE_AUTOMATON);
82+
public static final IndexPrivilege CREATE_DOC = new IndexPrivilege("create_doc", CREATE_DOC_AUTOMATON);
8083
public static final IndexPrivilege MONITOR = new IndexPrivilege("monitor", MONITOR_AUTOMATON);
8184
public static final IndexPrivilege MANAGE = new IndexPrivilege("manage", MANAGE_AUTOMATON);
8285
public static final IndexPrivilege DELETE_INDEX = new IndexPrivilege("delete_index", DELETE_INDEX_AUTOMATON);
@@ -97,6 +100,7 @@ public final class IndexPrivilege extends Privilege {
97100
.put("delete", DELETE)
98101
.put("write", WRITE)
99102
.put("create", CREATE)
103+
.put("create_doc", CREATE_DOC)
100104
.put("delete_index", DELETE_INDEX)
101105
.put("view_index_metadata", VIEW_METADATA)
102106
.put("read_cross_cluster", READ_CROSS_CLUSTER)

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ public class AuthorizationService {
9393
public static final String AUTHORIZATION_INFO_KEY = "_authz_info";
9494
private static final AuthorizationInfo SYSTEM_AUTHZ_INFO =
9595
() -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, new String[] { SystemUser.ROLE_NAME });
96+
private static final String IMPLIED_INDEX_ACTION = IndexAction.NAME + ":op_type/index";
97+
private static final String IMPLIED_CREATE_ACTION = IndexAction.NAME + ":op_type/create";
9698

9799
private static final Logger logger = LogManager.getLogger(AuthorizationService.class);
98100

@@ -536,8 +538,9 @@ private static String getAction(BulkItemRequest item) {
536538
final DocWriteRequest<?> docWriteRequest = item.request();
537539
switch (docWriteRequest.opType()) {
538540
case INDEX:
541+
return IMPLIED_INDEX_ACTION;
539542
case CREATE:
540-
return IndexAction.NAME;
543+
return IMPLIED_CREATE_ACTION;
541544
case UPDATE:
542545
return UpdateAction.NAME;
543546
case DELETE:
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.integration;
8+
9+
import org.elasticsearch.client.Request;
10+
import org.elasticsearch.common.settings.SecureString;
11+
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
12+
import org.junit.Before;
13+
14+
import java.io.IOException;
15+
16+
public class CreateDocsIndexPrivilegeTests extends AbstractPrivilegeTestCase {
17+
private static final String INDEX_NAME = "index-1";
18+
private static final String CREATE_DOC_USER = "create_doc_user";
19+
private String jsonDoc = "{ \"name\" : \"elasticsearch\", \"body\": \"foo bar\" }";
20+
private static final String ROLES =
21+
"all_indices_role:\n" +
22+
" indices:\n" +
23+
" - names: '*'\n" +
24+
" privileges: [ all ]\n" +
25+
"create_doc_role:\n" +
26+
" indices:\n" +
27+
" - names: '*'\n" +
28+
" privileges: [ create_doc ]\n";
29+
30+
private static final String USERS_ROLES =
31+
"all_indices_role:admin\n" +
32+
"create_doc_role:" + CREATE_DOC_USER + "\n";
33+
34+
@Override
35+
protected boolean addMockHttpTransport() {
36+
return false; // enable http
37+
}
38+
39+
@Override
40+
protected String configRoles() {
41+
return super.configRoles() + "\n" + ROLES;
42+
}
43+
44+
@Override
45+
protected String configUsers() {
46+
final String usersPasswdHashed = new String(Hasher.resolve(
47+
randomFrom("pbkdf2", "pbkdf2_1000", "bcrypt", "bcrypt9")).hash(new SecureString("passwd".toCharArray())));
48+
49+
return super.configUsers() +
50+
"admin:" + usersPasswdHashed + "\n" +
51+
CREATE_DOC_USER + ":" + usersPasswdHashed + "\n";
52+
}
53+
54+
@Override
55+
protected String configUsersRoles() {
56+
return super.configUsersRoles() + USERS_ROLES;
57+
}
58+
59+
@Before
60+
public void insertBaseDocumentsAsAdmin() throws Exception {
61+
Request request = new Request("PUT", "/" + INDEX_NAME + "/_doc/1");
62+
request.setJsonEntity(jsonDoc);
63+
request.addParameter("refresh", "true");
64+
assertAccessIsAllowed("admin", request);
65+
}
66+
67+
public void testCreateDocUserCanIndexNewDocumentsWithAutoGeneratedId() throws IOException {
68+
assertAccessIsAllowed(CREATE_DOC_USER, "POST", "/" + INDEX_NAME + "/_doc", "{ \"foo\" : \"bar\" }");
69+
}
70+
71+
public void testCreateDocUserCanIndexNewDocumentsWithExternalIdAndOpTypeIsCreate() throws IOException {
72+
assertAccessIsAllowed(CREATE_DOC_USER, randomFrom("PUT", "POST"), "/" + INDEX_NAME + "/_doc/2?op_type=create", "{ \"foo\" : " +
73+
"\"bar\" }");
74+
}
75+
76+
public void testCreateDocUserIsDeniedToIndexNewDocumentsWithExternalIdAndOpTypeIsIndex() throws IOException {
77+
assertAccessIsDenied(CREATE_DOC_USER, randomFrom("PUT", "POST"), "/" + INDEX_NAME + "/_doc/3", "{ \"foo\" : \"bar\" }");
78+
}
79+
80+
public void testCreateDocUserIsDeniedToIndexUpdatesToExistingDocument() throws IOException {
81+
assertAccessIsDenied(CREATE_DOC_USER, "POST", "/" + INDEX_NAME + "/_doc/1/_update", "{ \"doc\" : { \"foo\" : \"baz\" } }");
82+
assertAccessIsDenied(CREATE_DOC_USER, "PUT", "/" + INDEX_NAME + "/_doc/1", "{ \"foo\" : \"baz\" }");
83+
}
84+
85+
public void testCreateDocUserCanIndexNewDocumentsWithAutoGeneratedIdUsingBulkApi() throws IOException {
86+
assertAccessIsAllowed(CREATE_DOC_USER, randomFrom("PUT", "POST"),
87+
"/" + INDEX_NAME + "/_bulk", "{ \"index\" : { } }\n{ \"foo\" : \"bar\" }\n");
88+
}
89+
90+
public void testCreateDocUserCanIndexNewDocumentsWithAutoGeneratedIdAndOpTypeCreateUsingBulkApi() throws IOException {
91+
assertAccessIsAllowed(CREATE_DOC_USER, randomFrom("PUT", "POST"),
92+
"/" + INDEX_NAME + "/_bulk", "{ \"create\" : { } }\n{ \"foo\" : \"bar\" }\n");
93+
}
94+
95+
public void testCreateDocUserCanIndexNewDocumentsWithExternalIdAndOpTypeIsCreateUsingBulkApi() throws IOException {
96+
assertAccessIsAllowed(CREATE_DOC_USER, randomFrom("PUT", "POST"),
97+
"/" + INDEX_NAME + "/_bulk", "{ \"create\" : { \"_id\" : \"4\" } }\n{ \"foo\" : \"bar\" }\n");
98+
}
99+
100+
public void testCreateDocUserIsDeniedToIndexNewDocumentsWithExternalIdAndOpTypeIsIndexUsingBulkApi() throws IOException {
101+
assertBodyHasAccessIsDenied(CREATE_DOC_USER, randomFrom("PUT", "POST"),
102+
"/" + INDEX_NAME + "/_bulk", "{ \"index\" : { \"_id\" : \"5\" } }\n{ \"foo\" : \"bar\" }\n");
103+
}
104+
105+
public void testCreateDocUserIsDeniedToIndexUpdatesToExistingDocumentUsingBulkApi() throws IOException {
106+
assertBodyHasAccessIsDenied(CREATE_DOC_USER, randomFrom("PUT", "POST"),
107+
"/" + INDEX_NAME + "/_bulk", "{ \"index\" : { \"_id\" : \"1\" } }\n{ \"doc\" : {\"foo\" : \"bazbaz\"} }\n");
108+
assertBodyHasAccessIsDenied(CREATE_DOC_USER, randomFrom("PUT", "POST"),
109+
"/" + INDEX_NAME + "/_bulk", "{ \"update\" : { \"_id\" : \"1\" } }\n{ \"doc\" : {\"foo\" : \"bazbaz\"} }\n");
110+
}
111+
112+
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,16 +1193,16 @@ public void testAuthorizationOfIndividualBulkItems() throws IOException {
11931193
eq(DeleteAction.NAME), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()),
11941194
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
11951195
verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication),
1196-
eq(IndexAction.NAME), eq("concrete-index"), eq(BulkItemRequest.class.getSimpleName()),
1196+
eq(IndexAction.NAME + ":op_type/index"), eq("concrete-index"), eq(BulkItemRequest.class.getSimpleName()),
11971197
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
11981198
verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication),
1199-
eq(IndexAction.NAME), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()),
1199+
eq(IndexAction.NAME + ":op_type/index"), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()),
12001200
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12011201
verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_DENIED), eq(authentication),
12021202
eq(DeleteAction.NAME), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()),
12031203
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12041204
verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_DENIED), eq(authentication),
1205-
eq(IndexAction.NAME), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()),
1205+
eq(IndexAction.NAME + ":op_type/index"), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()),
12061206
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12071207
verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request),
12081208
authzInfoRoles(new String[] { role.getName() })); // bulk request is allowed
@@ -1236,7 +1236,7 @@ public void testAuthorizationOfIndividualBulkItemsWithDateMath() throws IOExcept
12361236
eq(DeleteAction.NAME), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()),
12371237
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12381238
verify(auditTrail, times(2)).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication),
1239-
eq(IndexAction.NAME), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()),
1239+
eq(IndexAction.NAME + ":op_type/index"), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()),
12401240
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12411241
// bulk request is allowed
12421242
verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request),

x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ setup:
1616
# I would much prefer we could just check that specific entries are in the array, but we don't have
1717
# an assertion for that
1818
- length: { "cluster" : 30 }
19-
- length: { "index" : 16 }
19+
- length: { "index" : 17 }

0 commit comments

Comments
 (0)