Skip to content

Commit 924b298

Browse files
authored
Add 'create_doc' index privilege (#45806)
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 8c4de64 commit 924b298

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
@@ -95,6 +95,7 @@ A successful call returns an object with "cluster" and "index" fields.
9595
"index" : [
9696
"all",
9797
"create",
98+
"create_doc",
9899
"create_index",
99100
"delete",
100101
"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
@@ -48,6 +48,8 @@ public final class IndexPrivilege extends Privilege {
4848
ClusterSearchShardsAction.NAME);
4949
private static final Automaton CREATE_AUTOMATON = patterns("indices:data/write/index*", "indices:data/write/bulk*",
5050
PutMappingAction.NAME);
51+
private static final Automaton CREATE_DOC_AUTOMATON = patterns("indices:data/write/index", "indices:data/write/index[*",
52+
"indices:data/write/index:op_type/create", "indices:data/write/bulk*", PutMappingAction.NAME);
5153
private static final Automaton INDEX_AUTOMATON =
5254
patterns("indices:data/write/index*", "indices:data/write/bulk*", "indices:data/write/update*", PutMappingAction.NAME);
5355
private static final Automaton DELETE_AUTOMATON = patterns("indices:data/write/delete*", "indices:data/write/bulk*");
@@ -73,6 +75,7 @@ public final class IndexPrivilege extends Privilege {
7375
public static final IndexPrivilege INDEX = new IndexPrivilege("index", INDEX_AUTOMATON);
7476
public static final IndexPrivilege DELETE = new IndexPrivilege("delete", DELETE_AUTOMATON);
7577
public static final IndexPrivilege WRITE = new IndexPrivilege("write", WRITE_AUTOMATON);
78+
public static final IndexPrivilege CREATE_DOC = new IndexPrivilege("create_doc", CREATE_DOC_AUTOMATON);
7679
public static final IndexPrivilege MONITOR = new IndexPrivilege("monitor", MONITOR_AUTOMATON);
7780
public static final IndexPrivilege MANAGE = new IndexPrivilege("manage", MANAGE_AUTOMATON);
7881
public static final IndexPrivilege DELETE_INDEX = new IndexPrivilege("delete_index", DELETE_INDEX_AUTOMATON);
@@ -93,6 +96,7 @@ public final class IndexPrivilege extends Privilege {
9396
entry("delete", DELETE),
9497
entry("write", WRITE),
9598
entry("create", CREATE),
99+
entry("create_doc", CREATE_DOC),
96100
entry("delete_index", DELETE_INDEX),
97101
entry("view_index_metadata", VIEW_METADATA),
98102
entry("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
@@ -1189,16 +1189,16 @@ public void testAuthorizationOfIndividualBulkItems() throws IOException {
11891189
eq(DeleteAction.NAME), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()),
11901190
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
11911191
verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication),
1192-
eq(IndexAction.NAME), eq("concrete-index"), eq(BulkItemRequest.class.getSimpleName()),
1192+
eq(IndexAction.NAME + ":op_type/index"), eq("concrete-index"), eq(BulkItemRequest.class.getSimpleName()),
11931193
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
11941194
verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication),
1195-
eq(IndexAction.NAME), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()),
1195+
eq(IndexAction.NAME + ":op_type/index"), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()),
11961196
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
11971197
verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_DENIED), eq(authentication),
11981198
eq(DeleteAction.NAME), eq("alias-1"), eq(BulkItemRequest.class.getSimpleName()),
11991199
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12001200
verify(auditTrail).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_DENIED), eq(authentication),
1201-
eq(IndexAction.NAME), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()),
1201+
eq(IndexAction.NAME + ":op_type/index"), eq("alias-2"), eq(BulkItemRequest.class.getSimpleName()),
12021202
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12031203
verify(auditTrail).accessGranted(eq(requestId), eq(authentication), eq(action), eq(request),
12041204
authzInfoRoles(new String[] { role.getName() })); // bulk request is allowed
@@ -1232,7 +1232,7 @@ public void testAuthorizationOfIndividualBulkItemsWithDateMath() throws IOExcept
12321232
eq(DeleteAction.NAME), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()),
12331233
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12341234
verify(auditTrail, times(2)).explicitIndexAccessEvent(eq(requestId), eq(AuditLevel.ACCESS_GRANTED), eq(authentication),
1235-
eq(IndexAction.NAME), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()),
1235+
eq(IndexAction.NAME + ":op_type/index"), Matchers.startsWith("datemath-"), eq(BulkItemRequest.class.getSimpleName()),
12361236
eq(request.remoteAddress()), authzInfoRoles(new String[] { role.getName() }));
12371237
// bulk request is allowed
12381238
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)