Skip to content

Commit 0c22755

Browse files
authored
Add support for setting based file entitlements (elastic#122656) (elastic#122902)
With Security Manager we have SecuredConfigFileSettingAccessPermission. This commit adds an entitlement equivalent. With each entry in files entitlement, a `path_setting` can now be used. The value may be an explicit setting, or a setting glob with a single `*`. relates ES-10844
1 parent 70cc270 commit 0c22755

File tree

13 files changed

+229
-49
lines changed

13 files changed

+229
-49
lines changed

libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.nio.file.Path;
2929
import java.util.Map;
3030
import java.util.function.Function;
31+
import java.util.stream.Stream;
3132

3233
import static java.util.Objects.requireNonNull;
3334

@@ -36,19 +37,24 @@ public class EntitlementBootstrap {
3637
public record BootstrapArgs(
3738
Map<String, Policy> pluginPolicies,
3839
Function<Class<?>, String> pluginResolver,
40+
Function<String, String> settingResolver,
41+
Function<String, Stream<String>> settingGlobResolver,
3942
Path[] dataDirs,
4043
Path configDir,
41-
Path tempDir,
42-
Path logsDir
44+
Path logsDir,
45+
Path tempDir
4346
) {
4447
public BootstrapArgs {
4548
requireNonNull(pluginPolicies);
4649
requireNonNull(pluginResolver);
50+
requireNonNull(settingResolver);
51+
requireNonNull(settingGlobResolver);
4752
requireNonNull(dataDirs);
4853
if (dataDirs.length == 0) {
4954
throw new IllegalArgumentException("must provide at least one data directory");
5055
}
5156
requireNonNull(configDir);
57+
requireNonNull(logsDir);
5258
requireNonNull(tempDir);
5359
}
5460
}
@@ -73,16 +79,27 @@ public static BootstrapArgs bootstrapArgs() {
7379
public static void bootstrap(
7480
Map<String, Policy> pluginPolicies,
7581
Function<Class<?>, String> pluginResolver,
82+
Function<String, String> settingResolver,
83+
Function<String, Stream<String>> settingGlobResolver,
7684
Path[] dataDirs,
7785
Path configDir,
78-
Path tempDir,
79-
Path logsDir
86+
Path logsDir,
87+
Path tempDir
8088
) {
8189
logger.debug("Loading entitlement agent");
8290
if (EntitlementBootstrap.bootstrapArgs != null) {
8391
throw new IllegalStateException("plugin data is already set");
8492
}
85-
EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(pluginPolicies, pluginResolver, dataDirs, configDir, tempDir, logsDir);
93+
EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(
94+
pluginPolicies,
95+
pluginResolver,
96+
settingResolver,
97+
settingGlobResolver,
98+
dataDirs,
99+
configDir,
100+
logsDir,
101+
tempDir
102+
);
86103
exportInitializationToAgent();
87104
loadAgent(findAgentJar());
88105
selfTest();

libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,14 @@ private static Class<?>[] findClassesToRetransform(Class<?>[] loadedClasses, Set
135135
private static PolicyManager createPolicyManager() {
136136
EntitlementBootstrap.BootstrapArgs bootstrapArgs = EntitlementBootstrap.bootstrapArgs();
137137
Map<String, Policy> pluginPolicies = bootstrapArgs.pluginPolicies();
138-
var pathLookup = new PathLookup(getUserHome(), bootstrapArgs.configDir(), bootstrapArgs.dataDirs(), bootstrapArgs.tempDir());
139-
Path logsDir = EntitlementBootstrap.bootstrapArgs().logsDir();
138+
var pathLookup = new PathLookup(
139+
getUserHome(),
140+
bootstrapArgs.configDir(),
141+
bootstrapArgs.dataDirs(),
142+
bootstrapArgs.tempDir(),
143+
bootstrapArgs.settingResolver(),
144+
bootstrapArgs.settingGlobResolver()
145+
);
140146

141147
List<Scope> serverScopes = new ArrayList<>();
142148
Collections.addAll(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup)
4242
}
4343

4444
// everything has access to the temp dir
45-
readPaths.add(pathLookup.tempDir().toString());
46-
writePaths.add(pathLookup.tempDir().toString());
45+
String tempDir = normalizePath(pathLookup.tempDir());
46+
readPaths.add(tempDir);
47+
writePaths.add(tempDir);
4748

4849
readPaths.sort(String::compareTo);
4950
writePaths.sort(String::compareTo);

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,14 @@
1010
package org.elasticsearch.entitlement.runtime.policy;
1111

1212
import java.nio.file.Path;
13+
import java.util.function.Function;
14+
import java.util.stream.Stream;
1315

14-
public record PathLookup(Path homeDir, Path configDir, Path[] dataDirs, Path tempDir) {}
16+
public record PathLookup(
17+
Path homeDir,
18+
Path configDir,
19+
Path[] dataDirs,
20+
Path tempDir,
21+
Function<String, String> settingResolver,
22+
Function<String, Stream<String>> settingGlobResolver
23+
) {}

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import java.nio.file.Path;
1717
import java.util.ArrayList;
18-
import java.util.Arrays;
1918
import java.util.HashMap;
2019
import java.util.List;
2120
import java.util.Map;
@@ -53,33 +52,81 @@ static FileData ofPath(Path path, Mode mode) {
5352
static FileData ofRelativePath(Path relativePath, BaseDir baseDir, Mode mode) {
5453
return new RelativePathFileData(relativePath, baseDir, mode);
5554
}
56-
}
5755

58-
private record AbsolutePathFileData(Path path, Mode mode) implements FileData {
59-
@Override
60-
public Stream<Path> resolvePaths(PathLookup pathLookup) {
61-
return Stream.of(path);
56+
static FileData ofPathSetting(String setting, Mode mode) {
57+
return new PathSettingFileData(setting, mode);
58+
}
59+
60+
static FileData ofRelativePathSetting(String setting, BaseDir baseDir, Mode mode) {
61+
return new RelativePathSettingFileData(setting, baseDir, mode);
6262
}
6363
}
6464

65-
private record RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode) implements FileData {
65+
private sealed interface RelativeFileData extends FileData {
66+
BaseDir baseDir();
67+
68+
Stream<Path> resolveRelativePaths(PathLookup pathLookup);
6669

6770
@Override
68-
public Stream<Path> resolvePaths(PathLookup pathLookup) {
71+
default Stream<Path> resolvePaths(PathLookup pathLookup) {
6972
Objects.requireNonNull(pathLookup);
70-
switch (baseDir) {
73+
var relativePaths = resolveRelativePaths(pathLookup);
74+
switch (baseDir()) {
7175
case CONFIG:
72-
return Stream.of(pathLookup.configDir().resolve(relativePath));
76+
return relativePaths.map(relativePath -> pathLookup.configDir().resolve(relativePath));
7377
case DATA:
74-
return Arrays.stream(pathLookup.dataDirs()).map(d -> d.resolve(relativePath));
78+
// multiple data dirs are a pain...we need the combination of relative paths and data dirs
79+
List<Path> paths = new ArrayList<>();
80+
for (var relativePath : relativePaths.toList()) {
81+
for (var dataDir : pathLookup.dataDirs()) {
82+
paths.add(dataDir.resolve(relativePath));
83+
}
84+
}
85+
return paths.stream();
7586
case HOME:
76-
return Stream.of(pathLookup.homeDir().resolve(relativePath));
87+
return relativePaths.map(relativePath -> pathLookup.homeDir().resolve(relativePath));
7788
default:
7889
throw new IllegalArgumentException();
7990
}
8091
}
8192
}
8293

94+
private record AbsolutePathFileData(Path path, Mode mode) implements FileData {
95+
@Override
96+
public Stream<Path> resolvePaths(PathLookup pathLookup) {
97+
return Stream.of(path);
98+
}
99+
}
100+
101+
private record RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode) implements FileData, RelativeFileData {
102+
@Override
103+
public Stream<Path> resolveRelativePaths(PathLookup pathLookup) {
104+
return Stream.of(relativePath);
105+
}
106+
}
107+
108+
private record PathSettingFileData(String setting, Mode mode) implements FileData {
109+
@Override
110+
public Stream<Path> resolvePaths(PathLookup pathLookup) {
111+
return resolvePathSettings(pathLookup, setting);
112+
}
113+
}
114+
115+
private record RelativePathSettingFileData(String setting, BaseDir baseDir, Mode mode) implements FileData, RelativeFileData {
116+
@Override
117+
public Stream<Path> resolveRelativePaths(PathLookup pathLookup) {
118+
return resolvePathSettings(pathLookup, setting);
119+
}
120+
}
121+
122+
private static Stream<Path> resolvePathSettings(PathLookup pathLookup, String setting) {
123+
if (setting.contains("*")) {
124+
return pathLookup.settingGlobResolver().apply(setting).map(Path::of);
125+
}
126+
String path = pathLookup.settingResolver().apply(setting);
127+
return path == null ? Stream.of() : Stream.of(Path.of(path));
128+
}
129+
83130
private static Mode parseMode(String mode) {
84131
if (mode.equals("read")) {
85132
return Mode.READ;
@@ -113,37 +160,56 @@ public static FilesEntitlement build(List<Object> paths) {
113160
String pathAsString = file.remove("path");
114161
String relativePathAsString = file.remove("relative_path");
115162
String relativeTo = file.remove("relative_to");
116-
String mode = file.remove("mode");
163+
String pathSetting = file.remove("path_setting");
164+
String relativePathSetting = file.remove("relative_path_setting");
165+
String modeAsString = file.remove("mode");
117166

118167
if (file.isEmpty() == false) {
119168
throw new PolicyValidationException("unknown key(s) [" + file + "] in a listed file for files entitlement");
120169
}
121-
if (mode == null) {
170+
int foundKeys = (pathAsString != null ? 1 : 0) + (relativePathAsString != null ? 1 : 0) + (pathSetting != null ? 1 : 0)
171+
+ (relativePathSetting != null ? 1 : 0);
172+
if (foundKeys != 1) {
173+
throw new PolicyValidationException(
174+
"a files entitlement entry must contain one of " + "[path, relative_path, path_setting, relative_path_setting]"
175+
);
176+
}
177+
178+
if (modeAsString == null) {
122179
throw new PolicyValidationException("files entitlement must contain 'mode' for every listed file");
123180
}
124-
if (pathAsString != null && relativePathAsString != null) {
125-
throw new PolicyValidationException("a files entitlement entry cannot contain both 'path' and 'relative_path'");
181+
Mode mode = parseMode(modeAsString);
182+
183+
BaseDir baseDir = null;
184+
if (relativeTo != null) {
185+
baseDir = parseBaseDir(relativeTo);
126186
}
127187

128188
if (relativePathAsString != null) {
129-
if (relativeTo == null) {
189+
if (baseDir == null) {
130190
throw new PolicyValidationException("files entitlement with a 'relative_path' must specify 'relative_to'");
131191
}
132-
final BaseDir baseDir = parseBaseDir(relativeTo);
133192

134193
Path relativePath = Path.of(relativePathAsString);
135194
if (relativePath.isAbsolute()) {
136195
throw new PolicyValidationException("'relative_path' [" + relativePathAsString + "] must be relative");
137196
}
138-
filesData.add(FileData.ofRelativePath(relativePath, baseDir, parseMode(mode)));
197+
filesData.add(FileData.ofRelativePath(relativePath, baseDir, mode));
139198
} else if (pathAsString != null) {
140199
Path path = Path.of(pathAsString);
141200
if (path.isAbsolute() == false) {
142201
throw new PolicyValidationException("'path' [" + pathAsString + "] must be absolute");
143202
}
144-
filesData.add(FileData.ofPath(path, parseMode(mode)));
203+
filesData.add(FileData.ofPath(path, mode));
204+
} else if (pathSetting != null) {
205+
filesData.add(FileData.ofPathSetting(pathSetting, mode));
206+
} else if (relativePathSetting != null) {
207+
if (baseDir == null) {
208+
throw new PolicyValidationException("files entitlement with a 'relative_path_setting' must specify 'relative_to'");
209+
}
210+
filesData.add(FileData.ofRelativePathSetting(relativePathSetting, baseDir, mode));
145211
} else {
146-
throw new PolicyValidationException("files entitlement must contain either 'path' or 'relative_path' for every entry");
212+
throw new AssertionError("File entry validation error");
147213
}
148214
}
149215
return new FilesEntitlement(filesData);

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

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

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

12+
import org.elasticsearch.common.settings.Settings;
1213
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
1314
import org.elasticsearch.test.ESTestCase;
1415
import org.junit.BeforeClass;
@@ -25,10 +26,12 @@
2526
public class FileAccessTreeTests extends ESTestCase {
2627

2728
static Path root;
29+
static Settings settings;
2830

2931
@BeforeClass
3032
public static void setupRoot() {
3133
root = createTempDir();
34+
settings = Settings.EMPTY;
3235
}
3336

3437
private static Path path(String s) {
@@ -39,7 +42,9 @@ private static Path path(String s) {
3942
Path.of("/home"),
4043
Path.of("/config"),
4144
new Path[] { Path.of("/data1"), Path.of("/data2") },
42-
Path.of("/tmp")
45+
Path.of("/tmp"),
46+
setting -> settings.get(setting),
47+
glob -> settings.getGlobValues(glob)
4348
);
4449

4550
public void testEmpty() {
@@ -163,13 +168,9 @@ public void testForwardSlashes() {
163168
}
164169

165170
public void testTempDirAccess() {
166-
Path tempDir = createTempDir();
167-
var tree = FileAccessTree.of(
168-
FilesEntitlement.EMPTY,
169-
new PathLookup(Path.of("/home"), Path.of("/config"), new Path[] { Path.of("/data1"), Path.of("/data2") }, tempDir)
170-
);
171-
assertThat(tree.canRead(tempDir), is(true));
172-
assertThat(tree.canWrite(tempDir), is(true));
171+
var tree = FileAccessTree.of(FilesEntitlement.EMPTY, TEST_PATH_LOOKUP);
172+
assertThat(tree.canRead(TEST_PATH_LOOKUP.tempDir()), is(true));
173+
assertThat(tree.canWrite(TEST_PATH_LOOKUP.tempDir()), is(true));
173174
}
174175

175176
FileAccessTree accessTree(FilesEntitlement entitlement) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

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

12+
import org.elasticsearch.common.settings.Settings;
1213
import org.elasticsearch.entitlement.runtime.policy.PolicyManager.ModuleEntitlements;
1314
import org.elasticsearch.entitlement.runtime.policy.agent.TestAgent;
1415
import org.elasticsearch.entitlement.runtime.policy.agent.inner.TestInnerAgent;
@@ -68,7 +69,9 @@ public static void beforeClass() {
6869
TEST_BASE_DIR.resolve("/user/home"),
6970
TEST_BASE_DIR.resolve("/config"),
7071
new Path[] { TEST_BASE_DIR.resolve("/data1/"), TEST_BASE_DIR.resolve("/data2") },
71-
TEST_BASE_DIR.resolve("/temp")
72+
TEST_BASE_DIR.resolve("/temp"),
73+
Settings.EMPTY::get,
74+
Settings.EMPTY::getGlobValues
7275
);
7376
} catch (Exception e) {
7477
throw new IllegalStateException(e);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ public void testEntitlementMutuallyExclusiveParameters() {
7474
""".getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false).parsePolicy());
7575
assertEquals(
7676
"[2:5] policy parsing error for [test-failure-policy.yaml] in scope [entitlement-module-name] "
77-
+ "for entitlement type [files]: a files entitlement entry cannot contain both 'path' and 'relative_path'",
77+
+ "for entitlement type [files]: a files entitlement entry must contain one of "
78+
+ "[path, relative_path, path_setting, relative_path_setting]",
7879
ppe.getMessage()
7980
);
8081
}
@@ -87,7 +88,8 @@ public void testEntitlementAtLeastOneParameter() {
8788
""".getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false).parsePolicy());
8889
assertEquals(
8990
"[2:5] policy parsing error for [test-failure-policy.yaml] in scope [entitlement-module-name] "
90-
+ "for entitlement type [files]: files entitlement must contain either 'path' or 'relative_path' for every entry",
91+
+ "for entitlement type [files]: a files entitlement entry must contain one of "
92+
+ "[path, relative_path, path_setting, relative_path_setting]",
9193
ppe.getMessage()
9294
);
9395
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ public void testParseFiles() throws IOException {
182182
mode: "read"
183183
- path: '%s'
184184
mode: "read_write"
185+
- path_setting: foo.bar
186+
mode: read
187+
- relative_path_setting: foo.bar
188+
relative_to: config
189+
mode: read
185190
""", relativePathToFile, relativePathToDir, TEST_ABSOLUTE_PATH_TO_FILE).getBytes(StandardCharsets.UTF_8)),
186191
"test-policy.yaml",
187192
false
@@ -196,7 +201,9 @@ public void testParseFiles() throws IOException {
196201
List.of(
197202
Map.of("relative_path", relativePathToFile, "mode", "read_write", "relative_to", "data"),
198203
Map.of("relative_path", relativePathToDir, "mode", "read", "relative_to", "config"),
199-
Map.of("path", TEST_ABSOLUTE_PATH_TO_FILE, "mode", "read_write")
204+
Map.of("path", TEST_ABSOLUTE_PATH_TO_FILE, "mode", "read_write"),
205+
Map.of("path_setting", "foo.bar", "mode", "read"),
206+
Map.of("relative_path_setting", "foo.bar", "relative_to", "config", "mode", "read")
200207
)
201208
)
202209
)

0 commit comments

Comments
 (0)