Skip to content

Commit 66ddd8d

Browse files
Create snapshot role (#35820)
This commit introduces the `create_snapshot` cluster privilege and the `snapshot_user` role. This role is to be used by "cronable" tools that call the snapshot API periodically without recurring to the `manage` cluster privilege. The `create_snapshot` cluster privilege is much more limited compared to the `manage` privilege. The `snapshot_user` role grants the privileges to view the metadata of all indices (including restricted ones, i.e. .security). It obviously grants the create snapshot privilege but the repository has to be created using another role. In addition, it grants the privileges to (only) GET repositories and snapshots, but not create and delete them. The role does not allow to create repositories. This distinction is important because snapshotting equates to the `read` index privilege if the user has control of the snapshot destination, but this is not the case in this instance, because the role does not grant control over repository configuration.
1 parent f24dce1 commit 66ddd8d

File tree

7 files changed

+229
-8
lines changed

7 files changed

+229
-8
lines changed

client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,8 @@ public void testGetRoles() throws Exception {
638638

639639
List<Role> roles = response.getRoles();
640640
assertNotNull(response);
641-
// 23 system roles plus the three we created
642-
assertThat(roles.size(), equalTo(26));
641+
// 24 system roles plus the three we created
642+
assertThat(roles.size(), equalTo(27));
643643
}
644644

645645
{

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
package org.elasticsearch.xpack.core.security.authz.privilege;
77

88
import org.apache.lucene.util.automaton.Automaton;
9+
import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesAction;
10+
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotAction;
11+
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsAction;
12+
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusAction;
913
import org.elasticsearch.action.admin.cluster.state.ClusterStateAction;
1014
import org.elasticsearch.common.Strings;
1115
import org.elasticsearch.common.collect.MapBuilder;
@@ -48,6 +52,8 @@ public final class ClusterPrivilege extends Privilege {
4852
private static final Automaton MANAGE_ROLLUP_AUTOMATON = patterns("cluster:admin/xpack/rollup/*", "cluster:monitor/xpack/rollup/*");
4953
private static final Automaton MANAGE_CCR_AUTOMATON =
5054
patterns("cluster:admin/xpack/ccr/*", ClusterStateAction.NAME, HasPrivilegesAction.NAME);
55+
private static final Automaton CREATE_SNAPSHOT_AUTOMATON = patterns(CreateSnapshotAction.NAME, SnapshotsStatusAction.NAME + "*",
56+
GetSnapshotsAction.NAME, SnapshotsStatusAction.NAME, GetRepositoriesAction.NAME);
5157
private static final Automaton READ_CCR_AUTOMATON = patterns(ClusterStateAction.NAME, HasPrivilegesAction.NAME);
5258
private static final Automaton MANAGE_ILM_AUTOMATON = patterns("cluster:admin/ilm/*");
5359
private static final Automaton READ_ILM_AUTOMATON = patterns(GetLifecycleAction.NAME, GetStatusAction.NAME);
@@ -73,6 +79,7 @@ public final class ClusterPrivilege extends Privilege {
7379
public static final ClusterPrivilege MANAGE_PIPELINE = new ClusterPrivilege("manage_pipeline", "cluster:admin/ingest/pipeline/*");
7480
public static final ClusterPrivilege MANAGE_CCR = new ClusterPrivilege("manage_ccr", MANAGE_CCR_AUTOMATON);
7581
public static final ClusterPrivilege READ_CCR = new ClusterPrivilege("read_ccr", READ_CCR_AUTOMATON);
82+
public static final ClusterPrivilege CREATE_SNAPSHOT = new ClusterPrivilege("create_snapshot", CREATE_SNAPSHOT_AUTOMATON);
7683
public static final ClusterPrivilege MANAGE_ILM = new ClusterPrivilege("manage_ilm", MANAGE_ILM_AUTOMATON);
7784
public static final ClusterPrivilege READ_ILM = new ClusterPrivilege("read_ilm", READ_ILM_AUTOMATON);
7885

@@ -98,6 +105,7 @@ public final class ClusterPrivilege extends Privilege {
98105
.put("manage_rollup", MANAGE_ROLLUP)
99106
.put("manage_ccr", MANAGE_CCR)
100107
.put("read_ccr", READ_CCR)
108+
.put("create_snapshot", CREATE_SNAPSHOT)
101109
.put("manage_ilm", MANAGE_ILM)
102110
.put("read_ilm", READ_ILM)
103111
.immutableMap();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ public final class IndexPrivilege extends Privilege {
6464
CloseIndexAction.NAME + "*");
6565
private static final Automaton MANAGE_ILM_AUTOMATON = patterns("indices:admin/ilm/*");
6666

67-
public static final IndexPrivilege NONE = new IndexPrivilege("none", Automatons.EMPTY);
68-
public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON);
67+
public static final IndexPrivilege NONE = new IndexPrivilege("none", Automatons.EMPTY);
68+
public static final IndexPrivilege ALL = new IndexPrivilege("all", ALL_AUTOMATON);
6969
public static final IndexPrivilege READ = new IndexPrivilege("read", READ_AUTOMATON);
7070
public static final IndexPrivilege READ_CROSS_CLUSTER = new IndexPrivilege("read_cross_cluster", READ_CROSS_CLUSTER_AUTOMATON);
7171
public static final IndexPrivilege CREATE = new IndexPrivilege("create", CREATE_AUTOMATON);

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package org.elasticsearch.xpack.core.security.authz.store;
77

88
import org.elasticsearch.action.ActionListener;
9+
import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesAction;
910
import org.elasticsearch.common.collect.MapBuilder;
1011
import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction;
1112
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
@@ -179,6 +180,12 @@ private static Map<String, RoleDescriptor> initializeReservedRoles() {
179180
RoleDescriptor.IndicesPrivileges.builder()
180181
.indices(".code-*").privileges("read").build()
181182
}, null, MetadataUtils.DEFAULT_RESERVED_METADATA))
183+
.put("snapshot_user", new RoleDescriptor("snapshot_user", new String[] { "create_snapshot", GetRepositoriesAction.NAME },
184+
new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder()
185+
.indices("*")
186+
.privileges("view_index_metadata")
187+
.allowRestrictedIndices(true)
188+
.build() }, null, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA, null))
182189
.immutableMap();
183190
}
184191

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77

88
import org.elasticsearch.Version;
99
import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction;
10+
import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesAction;
11+
import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryAction;
1012
import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteAction;
1113
import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction;
14+
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotAction;
15+
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsAction;
16+
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusAction;
1217
import org.elasticsearch.action.admin.cluster.state.ClusterStateAction;
1318
import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction;
1419
import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
@@ -173,9 +178,54 @@ public void testIsReserved() {
173178
assertThat(ReservedRolesStore.isReserved(APMSystemUser.ROLE_NAME), is(true));
174179
assertThat(ReservedRolesStore.isReserved(RemoteMonitoringUser.COLLECTION_ROLE_NAME), is(true));
175180
assertThat(ReservedRolesStore.isReserved(RemoteMonitoringUser.INDEXING_ROLE_NAME), is(true));
181+
assertThat(ReservedRolesStore.isReserved("snapshot_user"), is(true));
176182
assertThat(ReservedRolesStore.isReserved("code_admin"), is(true));
177183
assertThat(ReservedRolesStore.isReserved("code_user"), is(true));
184+
}
185+
186+
public void testSnapshotUserRole() {
187+
final TransportRequest request = mock(TransportRequest.class);
188+
189+
RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("snapshot_user");
190+
assertNotNull(roleDescriptor);
191+
assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true));
192+
193+
Role snapshotUserRole = Role.builder(roleDescriptor, null).build();
194+
assertThat(snapshotUserRole.cluster().check(GetRepositoriesAction.NAME, request), is(true));
195+
assertThat(snapshotUserRole.cluster().check(CreateSnapshotAction.NAME, request), is(true));
196+
assertThat(snapshotUserRole.cluster().check(SnapshotsStatusAction.NAME, request), is(true));
197+
assertThat(snapshotUserRole.cluster().check(GetSnapshotsAction.NAME, request), is(true));
198+
199+
assertThat(snapshotUserRole.cluster().check(PutRepositoryAction.NAME, request), is(false));
200+
assertThat(snapshotUserRole.cluster().check(GetIndexTemplatesAction.NAME, request), is(false));
201+
assertThat(snapshotUserRole.cluster().check(DeleteIndexTemplateAction.NAME, request), is(false));
202+
assertThat(snapshotUserRole.cluster().check(PutPipelineAction.NAME, request), is(false));
203+
assertThat(snapshotUserRole.cluster().check(GetPipelineAction.NAME, request), is(false));
204+
assertThat(snapshotUserRole.cluster().check(DeletePipelineAction.NAME, request), is(false));
205+
assertThat(snapshotUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
206+
assertThat(snapshotUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
207+
assertThat(snapshotUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false));
208+
assertThat(snapshotUserRole.cluster().check(GetWatchAction.NAME, request), is(false));
209+
assertThat(snapshotUserRole.cluster().check(PutWatchAction.NAME, request), is(false));
210+
assertThat(snapshotUserRole.cluster().check(DeleteWatchAction.NAME, request), is(false));
211+
assertThat(snapshotUserRole.cluster().check(ExecuteWatchAction.NAME, request), is(false));
212+
assertThat(snapshotUserRole.cluster().check(AckWatchAction.NAME, request), is(false));
213+
assertThat(snapshotUserRole.cluster().check(ActivateWatchAction.NAME, request), is(false));
214+
assertThat(snapshotUserRole.cluster().check(WatcherServiceAction.NAME, request), is(false));
215+
216+
assertThat(snapshotUserRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(randomAlphaOfLengthBetween(8, 24)), is(false));
217+
assertThat(snapshotUserRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), is(false));
218+
assertThat(snapshotUserRole.indices().allowedIndicesMatcher(GetAction.NAME).test(randomAlphaOfLengthBetween(8, 24)), is(false));
219+
assertThat(snapshotUserRole.indices().allowedIndicesMatcher(GetAction.NAME).test(randomAlphaOfLengthBetween(8, 24)), is(false));
220+
221+
assertThat(snapshotUserRole.indices().allowedIndicesMatcher(GetIndexAction.NAME)
222+
.test(randomAlphaOfLengthBetween(8, 24)), is(true));
223+
assertThat(snapshotUserRole.indices().allowedIndicesMatcher(GetIndexAction.NAME)
224+
.test(RestrictedIndicesNames.INTERNAL_SECURITY_INDEX), is(true));
225+
assertThat(snapshotUserRole.indices().allowedIndicesMatcher(GetIndexAction.NAME)
226+
.test(RestrictedIndicesNames.SECURITY_INDEX_NAME), is(true));
178227

228+
assertNoAccessAllowed(snapshotUserRole, RestrictedIndicesNames.NAMES_SET);
179229
}
180230

181231
public void testIngestAdminRole() {

x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ public class ClusterPrivilegeTests extends AbstractPrivilegeTestCase {
3232
"role_c:\n" +
3333
" indices:\n" +
3434
" - names: 'someindex'\n" +
35-
" privileges: [ all ]\n";
35+
" privileges: [ all ]\n" +
36+
"role_d:\n" +
37+
" cluster: [ create_snapshot ]\n";
3638

3739
private static final String USERS_ROLES =
3840
"role_a:user_a\n" +
3941
"role_b:user_b\n" +
40-
"role_c:user_c\n";
42+
"role_c:user_c\n" +
43+
"role_d:user_d\n";
4144

4245
private static Path repositoryLocation;
4346

@@ -75,8 +78,8 @@ protected String configUsers() {
7578
return super.configUsers() +
7679
"user_a:" + usersPasswdHashed + "\n" +
7780
"user_b:" + usersPasswdHashed + "\n" +
78-
"user_c:" + usersPasswdHashed + "\n";
79-
81+
"user_c:" + usersPasswdHashed + "\n" +
82+
"user_d:" + usersPasswdHashed + "\n";
8083
}
8184

8285
@Override
@@ -122,20 +125,34 @@ public void testThatClusterPrivilegesWorkAsExpectedViaHttp() throws Exception {
122125
assertAccessIsDenied("user_c", "GET", "/_nodes/infos");
123126
assertAccessIsDenied("user_c", "POST", "/_cluster/reroute");
124127
assertAccessIsDenied("user_c", "PUT", "/_cluster/settings", "{ \"transient\" : { \"search.default_search_timeout\": \"1m\" } }");
128+
129+
// user_d can view repos and create and view snapshots on existings repos, everything else is DENIED
130+
assertAccessIsDenied("user_d", "GET", "/_cluster/state");
131+
assertAccessIsDenied("user_d", "GET", "/_cluster/health");
132+
assertAccessIsDenied("user_d", "GET", "/_cluster/settings");
133+
assertAccessIsDenied("user_d", "GET", "/_cluster/stats");
134+
assertAccessIsDenied("user_d", "GET", "/_cluster/pending_tasks");
135+
assertAccessIsDenied("user_d", "GET", "/_nodes/stats");
136+
assertAccessIsDenied("user_d", "GET", "/_nodes/hot_threads");
137+
assertAccessIsDenied("user_d", "GET", "/_nodes/infos");
138+
assertAccessIsDenied("user_d", "POST", "/_cluster/reroute");
139+
assertAccessIsDenied("user_d", "PUT", "/_cluster/settings", "{ \"transient\" : { \"search.default_search_timeout\": \"1m\" } }");
125140
}
126141

127142
public void testThatSnapshotAndRestore() throws Exception {
128143
String repoJson = Strings.toString(jsonBuilder().startObject().field("type", "fs").startObject("settings").field("location",
129144
repositoryLocation.toString()).endObject().endObject());
130145
assertAccessIsDenied("user_b", "PUT", "/_snapshot/my-repo", repoJson);
131146
assertAccessIsDenied("user_c", "PUT", "/_snapshot/my-repo", repoJson);
147+
assertAccessIsDenied("user_d", "PUT", "/_snapshot/my-repo", repoJson);
132148
assertAccessIsAllowed("user_a", "PUT", "/_snapshot/my-repo", repoJson);
133149

134150
Request createBar = new Request("PUT", "/someindex/bar/1");
135151
createBar.setJsonEntity("{ \"name\" : \"elasticsearch\" }");
136152
createBar.addParameter("refresh", "true");
137153
assertAccessIsDenied("user_a", createBar);
138154
assertAccessIsDenied("user_b", createBar);
155+
assertAccessIsDenied("user_d", createBar);
139156
assertAccessIsAllowed("user_c", createBar);
140157

141158
assertAccessIsDenied("user_b", "PUT", "/_snapshot/my-repo/my-snapshot", "{ \"indices\": \"someindex\" }");
@@ -145,30 +162,38 @@ public void testThatSnapshotAndRestore() throws Exception {
145162
assertAccessIsDenied("user_b", "GET", "/_snapshot/my-repo/my-snapshot/_status");
146163
assertAccessIsDenied("user_c", "GET", "/_snapshot/my-repo/my-snapshot/_status");
147164
assertAccessIsAllowed("user_a", "GET", "/_snapshot/my-repo/my-snapshot/_status");
165+
assertAccessIsAllowed("user_d", "GET", "/_snapshot/my-repo/my-snapshot/_status");
148166

149167
// This snapshot needs to be finished in order to be restored
150168
waitForSnapshotToFinish("my-repo", "my-snapshot");
169+
// user_d can create snapshots, but not concurrently
170+
assertAccessIsAllowed("user_d", "PUT", "/_snapshot/my-repo/my-snapshot-d", "{ \"indices\": \"someindex\" }");
151171

152172
assertAccessIsDenied("user_a", "DELETE", "/someindex");
153173
assertAccessIsDenied("user_b", "DELETE", "/someindex");
174+
assertAccessIsDenied("user_d", "DELETE", "/someindex");
154175
assertAccessIsAllowed("user_c", "DELETE", "/someindex");
155176

156177
Request restoreSnapshotRequest = new Request("POST", "/_snapshot/my-repo/my-snapshot/_restore");
157178
restoreSnapshotRequest.addParameter("wait_for_completion", "true");
158179
assertAccessIsDenied("user_b", restoreSnapshotRequest);
159180
assertAccessIsDenied("user_c", restoreSnapshotRequest);
181+
assertAccessIsDenied("user_d", restoreSnapshotRequest);
160182
assertAccessIsAllowed("user_a", restoreSnapshotRequest);
161183

162184
assertAccessIsDenied("user_a", "GET", "/someindex/bar/1");
163185
assertAccessIsDenied("user_b", "GET", "/someindex/bar/1");
186+
assertAccessIsDenied("user_d", "GET", "/someindex/bar/1");
164187
assertAccessIsAllowed("user_c", "GET", "/someindex/bar/1");
165188

166189
assertAccessIsDenied("user_b", "DELETE", "/_snapshot/my-repo/my-snapshot");
167190
assertAccessIsDenied("user_c", "DELETE", "/_snapshot/my-repo/my-snapshot");
191+
assertAccessIsDenied("user_d", "DELETE", "/_snapshot/my-repo/my-snapshot");
168192
assertAccessIsAllowed("user_a", "DELETE", "/_snapshot/my-repo/my-snapshot");
169193

170194
assertAccessIsDenied("user_b", "DELETE", "/_snapshot/my-repo");
171195
assertAccessIsDenied("user_c", "DELETE", "/_snapshot/my-repo");
196+
assertAccessIsDenied("user_d", "DELETE", "/_snapshot/my-repo");
172197
assertAccessIsAllowed("user_a", "DELETE", "/_snapshot/my-repo");
173198
}
174199

0 commit comments

Comments
 (0)