Skip to content

Commit 2656f10

Browse files
authored
Merge all file entitlements into a single files entitlement (elastic#121864) (elastic#121938)
This change replaces FileEntitlement with FilesEntitlement so that we can have exactly one entitlement class per module (or possibly future scope). This cleans up our policy files so that all files are located together to allow access, and this opens up the design for future optimizations.
1 parent 967bcf3 commit 2656f10

File tree

11 files changed

+217
-107
lines changed

11 files changed

+217
-107
lines changed

libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/AbstractEntitlementsIT.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,17 @@ public abstract class AbstractEntitlementsIT extends ESRestTestCase {
3434
Map.of("properties", List.of("es.entitlements.checkSetSystemProperty", "es.entitlements.checkClearSystemProperty"))
3535
)
3636
);
37-
38-
builder.value(Map.of("file", Map.of("path", tempDir.resolve("read_dir"), "mode", "read")));
39-
builder.value(Map.of("file", Map.of("path", tempDir.resolve("read_write_dir"), "mode", "read_write")));
40-
builder.value(Map.of("file", Map.of("path", tempDir.resolve("read_file"), "mode", "read")));
41-
builder.value(Map.of("file", Map.of("path", tempDir.resolve("read_write_file"), "mode", "read_write")));
37+
builder.value(
38+
Map.of(
39+
"files",
40+
List.of(
41+
Map.of("path", tempDir.resolve("read_dir"), "mode", "read"),
42+
Map.of("path", tempDir.resolve("read_write_dir"), "mode", "read_write"),
43+
Map.of("path", tempDir.resolve("read_file"), "mode", "read"),
44+
Map.of("path", tempDir.resolve("read_write_file"), "mode", "read_write")
45+
)
46+
)
47+
);
4248
};
4349

4450
private final String actionName;

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
package org.elasticsearch.entitlement.runtime.policy;
1111

12-
import org.elasticsearch.entitlement.runtime.policy.entitlements.FileEntitlement;
12+
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
1313

1414
import java.nio.file.Path;
1515
import java.util.ArrayList;
@@ -20,18 +20,19 @@
2020
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
2121

2222
public final class FileAccessTree {
23-
public static final FileAccessTree EMPTY = new FileAccessTree(List.of());
23+
public static final FileAccessTree EMPTY = new FileAccessTree(FilesEntitlement.EMPTY);
2424
private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator();
2525

2626
private final String[] readPaths;
2727
private final String[] writePaths;
2828

29-
private FileAccessTree(List<FileEntitlement> fileEntitlements) {
29+
private FileAccessTree(FilesEntitlement filesEntitlement) {
3030
List<String> readPaths = new ArrayList<>();
3131
List<String> writePaths = new ArrayList<>();
32-
for (FileEntitlement fileEntitlement : fileEntitlements) {
33-
String path = normalizePath(Path.of(fileEntitlement.path()));
34-
if (fileEntitlement.mode() == FileEntitlement.Mode.READ_WRITE) {
32+
for (FilesEntitlement.FileData fileData : filesEntitlement.filesData()) {
33+
var path = normalizePath(Path.of(fileData.path()));
34+
var mode = fileData.mode();
35+
if (mode == FilesEntitlement.Mode.READ_WRITE) {
3536
writePaths.add(path);
3637
}
3738
readPaths.add(path);
@@ -44,8 +45,8 @@ private FileAccessTree(List<FileEntitlement> fileEntitlements) {
4445
this.writePaths = writePaths.toArray(new String[0]);
4546
}
4647

47-
public static FileAccessTree of(List<FileEntitlement> fileEntitlements) {
48-
return new FileAccessTree(fileEntitlements);
48+
public static FileAccessTree of(FilesEntitlement filesEntitlement) {
49+
return new FileAccessTree(filesEntitlement);
4950
}
5051

5152
boolean canRead(Path path) {

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
1717
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
1818
import org.elasticsearch.entitlement.runtime.policy.entitlements.ExitVMEntitlement;
19-
import org.elasticsearch.entitlement.runtime.policy.entitlements.FileEntitlement;
19+
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
2020
import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
2121
import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
2222
import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
@@ -73,14 +73,16 @@ public static ModuleEntitlements none(String componentName) {
7373
}
7474

7575
public static ModuleEntitlements from(String componentName, List<Entitlement> entitlements) {
76-
var fileEntitlements = entitlements.stream()
77-
.filter(e -> e.getClass().equals(FileEntitlement.class))
78-
.map(e -> (FileEntitlement) e)
79-
.toList();
76+
FilesEntitlement filesEntitlement = FilesEntitlement.EMPTY;
77+
for (Entitlement entitlement : entitlements) {
78+
if (entitlement instanceof FilesEntitlement) {
79+
filesEntitlement = (FilesEntitlement) entitlement;
80+
}
81+
}
8082
return new ModuleEntitlements(
8183
componentName,
8284
entitlements.stream().collect(groupingBy(Entitlement::getClass)),
83-
FileAccessTree.of(fileEntitlements)
85+
FileAccessTree.of(filesEntitlement)
8486
);
8587
}
8688

@@ -164,23 +166,14 @@ private static Map<String, List<Entitlement>> buildScopeEntitlementsMap(Policy p
164166
}
165167

166168
private static void validateEntitlementsPerModule(String componentName, String moduleName, List<Entitlement> entitlements) {
167-
Set<Class<? extends Entitlement>> flagEntitlements = new HashSet<>();
169+
Set<Class<? extends Entitlement>> found = new HashSet<>();
168170
for (var e : entitlements) {
169-
if (e instanceof FileEntitlement) {
170-
continue;
171-
}
172-
if (flagEntitlements.contains(e.getClass())) {
171+
if (found.contains(e.getClass())) {
173172
throw new IllegalArgumentException(
174-
"["
175-
+ componentName
176-
+ "] using module ["
177-
+ moduleName
178-
+ "] found duplicate flag entitlements ["
179-
+ e.getClass().getName()
180-
+ "]"
173+
"[" + componentName + "] using module [" + moduleName + "] found duplicate entitlement [" + e.getClass().getName() + "]"
181174
);
182175
}
183-
flagEntitlements.add(e.getClass());
176+
found.add(e.getClass());
184177
}
185178
}
186179

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
1313
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
14-
import org.elasticsearch.entitlement.runtime.policy.entitlements.FileEntitlement;
14+
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
1515
import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
1616
import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
1717
import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
@@ -46,7 +46,7 @@
4646
public class PolicyParser {
4747

4848
private static final Map<String, Class<?>> EXTERNAL_ENTITLEMENTS = Stream.of(
49-
FileEntitlement.class,
49+
FilesEntitlement.class,
5050
CreateClassLoaderEntitlement.class,
5151
SetHttpsConnectionPropertiesEntitlement.class,
5252
OutboundNetworkEntitlement.class,
@@ -197,34 +197,41 @@ protected Entitlement parseEntitlement(String scopeName, String entitlementType)
197197
? entitlementConstructor.getParameterTypes()
198198
: entitlementMethod.getParameterTypes();
199199
String[] parametersNames = entitlementMetadata.parameterNames();
200+
Object[] parameterValues = new Object[parameterTypes.length];
200201

201202
if (parameterTypes.length != 0 || parametersNames.length != 0) {
202-
if (policyParser.nextToken() != XContentParser.Token.START_OBJECT) {
203-
throw newPolicyParserException(scopeName, entitlementType, "expected entitlement parameters");
204-
}
205-
}
206-
207-
Map<String, Object> parsedValues = policyParser.map();
203+
if (policyParser.nextToken() == XContentParser.Token.START_OBJECT) {
204+
Map<String, Object> parsedValues = policyParser.map();
208205

209-
Object[] parameterValues = new Object[parameterTypes.length];
210-
for (int parameterIndex = 0; parameterIndex < parameterTypes.length; ++parameterIndex) {
211-
String parameterName = parametersNames[parameterIndex];
212-
Object parameterValue = parsedValues.remove(parameterName);
213-
if (parameterValue == null) {
214-
throw newPolicyParserException(scopeName, entitlementType, "missing entitlement parameter [" + parameterName + "]");
215-
}
216-
Class<?> parameterType = parameterTypes[parameterIndex];
217-
if (parameterType.isAssignableFrom(parameterValue.getClass()) == false) {
218-
throw newPolicyParserException(
219-
scopeName,
220-
entitlementType,
221-
"unexpected parameter type [" + parameterType.getSimpleName() + "] for entitlement parameter [" + parameterName + "]"
222-
);
206+
for (int parameterIndex = 0; parameterIndex < parameterTypes.length; ++parameterIndex) {
207+
String parameterName = parametersNames[parameterIndex];
208+
Object parameterValue = parsedValues.remove(parameterName);
209+
if (parameterValue == null) {
210+
throw newPolicyParserException(scopeName, entitlementType, "missing entitlement parameter [" + parameterName + "]");
211+
}
212+
Class<?> parameterType = parameterTypes[parameterIndex];
213+
if (parameterType.isAssignableFrom(parameterValue.getClass()) == false) {
214+
throw newPolicyParserException(
215+
scopeName,
216+
entitlementType,
217+
"unexpected parameter type ["
218+
+ parameterType.getSimpleName()
219+
+ "] for entitlement parameter ["
220+
+ parameterName
221+
+ "]"
222+
);
223+
}
224+
parameterValues[parameterIndex] = parameterValue;
225+
}
226+
if (parsedValues.isEmpty() == false) {
227+
throw newPolicyParserException(scopeName, entitlementType, "extraneous entitlement parameter(s) " + parsedValues);
228+
}
229+
} else if (policyParser.currentToken() == XContentParser.Token.START_ARRAY) {
230+
List<Object> parsedValues = policyParser.list();
231+
parameterValues[0] = parsedValues;
232+
} else {
233+
throw newPolicyParserException(scopeName, entitlementType, "expected entitlement parameters");
223234
}
224-
parameterValues[parameterIndex] = parameterValue;
225-
}
226-
if (parsedValues.isEmpty() == false) {
227-
throw newPolicyParserException(scopeName, entitlementType, "extraneous entitlement parameter(s) " + parsedValues);
228235
}
229236

230237
try {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.runtime.policy.entitlements;
11+
12+
import org.elasticsearch.entitlement.runtime.policy.ExternalEntitlement;
13+
import org.elasticsearch.entitlement.runtime.policy.PolicyValidationException;
14+
15+
import java.util.ArrayList;
16+
import java.util.HashMap;
17+
import java.util.List;
18+
import java.util.Map;
19+
20+
/**
21+
* Describes a file entitlement with a path and mode.
22+
*/
23+
public record FilesEntitlement(List<FileData> filesData) implements Entitlement {
24+
25+
public static final FilesEntitlement EMPTY = new FilesEntitlement(List.of());
26+
27+
public enum Mode {
28+
READ,
29+
READ_WRITE
30+
}
31+
32+
public record FileData(String path, Mode mode) {
33+
34+
}
35+
36+
private static Mode parseMode(String mode) {
37+
if (mode.equals("read")) {
38+
return Mode.READ;
39+
} else if (mode.equals("read_write")) {
40+
return Mode.READ_WRITE;
41+
} else {
42+
throw new PolicyValidationException("invalid mode: " + mode + ", valid values: [read, read_write]");
43+
}
44+
}
45+
46+
@ExternalEntitlement(parameterNames = { "paths" }, esModulesOnly = false)
47+
@SuppressWarnings("unchecked")
48+
public static FilesEntitlement build(List<Object> paths) {
49+
if (paths == null || paths.isEmpty()) {
50+
throw new PolicyValidationException("must specify at least one path");
51+
}
52+
List<FileData> filesData = new ArrayList<>();
53+
for (Object object : paths) {
54+
Map<String, String> file = new HashMap<>((Map<String, String>) object);
55+
String path = file.remove("path");
56+
if (path == null) {
57+
throw new PolicyValidationException("files entitlement must contain path for every listed file");
58+
}
59+
String mode = file.remove("mode");
60+
if (mode == null) {
61+
throw new PolicyValidationException("files entitlement must contain mode for every listed file");
62+
}
63+
if (file.isEmpty() == false) {
64+
throw new PolicyValidationException("unknown key(s) " + file + " in a listed file for files entitlement");
65+
}
66+
filesData.add(new FileData(path, parseMode(mode)));
67+
}
68+
return new FilesEntitlement(filesData);
69+
}
70+
}

libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99

1010
package org.elasticsearch.entitlement.runtime.policy;
1111

12-
import org.elasticsearch.entitlement.runtime.policy.entitlements.FileEntitlement;
12+
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
1313
import org.elasticsearch.test.ESTestCase;
1414
import org.junit.BeforeClass;
1515

1616
import java.nio.file.Path;
17+
import java.util.ArrayList;
18+
import java.util.HashMap;
1719
import java.util.List;
20+
import java.util.Map;
1821

1922
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
2023
import static org.hamcrest.Matchers.is;
@@ -33,13 +36,13 @@ private static Path path(String s) {
3336
}
3437

3538
public void testEmpty() {
36-
var tree = FileAccessTree.of(List.of());
39+
var tree = FileAccessTree.of(FilesEntitlement.EMPTY);
3740
assertThat(tree.canRead(path("path")), is(false));
3841
assertThat(tree.canWrite(path("path")), is(false));
3942
}
4043

4144
public void testRead() {
42-
var tree = FileAccessTree.of(List.of(entitlement("foo", "read")));
45+
var tree = FileAccessTree.of(entitlement("foo", "read"));
4346
assertThat(tree.canRead(path("foo")), is(true));
4447
assertThat(tree.canRead(path("foo/subdir")), is(true));
4548
assertThat(tree.canRead(path("food")), is(false));
@@ -51,7 +54,7 @@ public void testRead() {
5154
}
5255

5356
public void testWrite() {
54-
var tree = FileAccessTree.of(List.of(entitlement("foo", "read_write")));
57+
var tree = FileAccessTree.of(entitlement("foo", "read_write"));
5558
assertThat(tree.canWrite(path("foo")), is(true));
5659
assertThat(tree.canWrite(path("foo/subdir")), is(true));
5760
assertThat(tree.canWrite(path("food")), is(false));
@@ -63,7 +66,7 @@ public void testWrite() {
6366
}
6467

6568
public void testTwoPaths() {
66-
var tree = FileAccessTree.of(List.of(entitlement("foo", "read"), entitlement("bar", "read")));
69+
var tree = FileAccessTree.of(entitlement("foo", "read", "bar", "read"));
6770
assertThat(tree.canRead(path("a")), is(false));
6871
assertThat(tree.canRead(path("bar")), is(true));
6972
assertThat(tree.canRead(path("bar/subdir")), is(true));
@@ -74,23 +77,23 @@ public void testTwoPaths() {
7477
}
7578

7679
public void testReadWriteUnderRead() {
77-
var tree = FileAccessTree.of(List.of(entitlement("foo", "read"), entitlement("foo/bar", "read_write")));
80+
var tree = FileAccessTree.of(entitlement("foo", "read", "foo/bar", "read_write"));
7881
assertThat(tree.canRead(path("foo")), is(true));
7982
assertThat(tree.canWrite(path("foo")), is(false));
8083
assertThat(tree.canRead(path("foo/bar")), is(true));
8184
assertThat(tree.canWrite(path("foo/bar")), is(true));
8285
}
8386

8487
public void testNormalizePath() {
85-
var tree = FileAccessTree.of(List.of(entitlement("foo/../bar", "read")));
88+
var tree = FileAccessTree.of(entitlement("foo/../bar", "read"));
8689
assertThat(tree.canRead(path("foo/../bar")), is(true));
8790
assertThat(tree.canRead(path("foo")), is(false));
8891
assertThat(tree.canRead(path("")), is(false));
8992
}
9093

9194
public void testForwardSlashes() {
9295
String sep = getDefaultFileSystem().getSeparator();
93-
var tree = FileAccessTree.of(List.of(entitlement("a/b", "read"), entitlement("m" + sep + "n", "read")));
96+
var tree = FileAccessTree.of(entitlement("a/b", "read", "m" + sep + "n", "read"));
9497

9598
// Native separators work
9699
assertThat(tree.canRead(path("a" + sep + "b")), is(true));
@@ -104,8 +107,14 @@ public void testForwardSlashes() {
104107
assertThat(tree.canRead(path("m\n")), is(false));
105108
}
106109

107-
FileEntitlement entitlement(String path, String mode) {
108-
Path p = path(path);
109-
return FileEntitlement.create(p.toString(), mode);
110+
FilesEntitlement entitlement(String... values) {
111+
List<Object> filesData = new ArrayList<>();
112+
for (int i = 0; i < values.length; i += 2) {
113+
Map<String, String> fileData = new HashMap<>();
114+
fileData.put("path", path(values[i]).toString());
115+
fileData.put("mode", values[i + 1]);
116+
filesData.add(fileData);
117+
}
118+
return FilesEntitlement.build(filesData);
110119
}
111120
}

0 commit comments

Comments
 (0)