Skip to content

Commit 3aca3c9

Browse files
authored
[Entitlements] Add ability to set path relative to a special directory for Files policies (#122370) (#122628)
1 parent 44d4721 commit 3aca3c9

File tree

11 files changed

+448
-60
lines changed

11 files changed

+448
-60
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.elasticsearch.entitlement.instrumentation.MethodKey;
1919
import org.elasticsearch.entitlement.instrumentation.Transformer;
2020
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
21+
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
2122
import org.elasticsearch.entitlement.runtime.policy.Policy;
2223
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
2324
import org.elasticsearch.entitlement.runtime.policy.Scope;
@@ -48,7 +49,6 @@
4849
import java.nio.file.attribute.FileAttribute;
4950
import java.nio.file.spi.FileSystemProvider;
5051
import java.util.ArrayList;
51-
import java.util.Arrays;
5252
import java.util.HashMap;
5353
import java.util.List;
5454
import java.util.Map;
@@ -126,9 +126,9 @@ private static Class<?>[] findClassesToRetransform(Class<?>[] loadedClasses, Set
126126
}
127127

128128
private static PolicyManager createPolicyManager() {
129-
Map<String, Policy> pluginPolicies = EntitlementBootstrap.bootstrapArgs().pluginPolicies();
130-
Path[] dataDirs = EntitlementBootstrap.bootstrapArgs().dataDirs();
131-
Path tempDir = EntitlementBootstrap.bootstrapArgs().tempDir();
129+
EntitlementBootstrap.BootstrapArgs bootstrapArgs = EntitlementBootstrap.bootstrapArgs();
130+
Map<String, Policy> pluginPolicies = bootstrapArgs.pluginPolicies();
131+
var pathLookup = new PathLookup(bootstrapArgs.configDir(), bootstrapArgs.dataDirs(), bootstrapArgs.tempDir());
132132

133133
// TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it
134134
var serverPolicy = new Policy(
@@ -147,7 +147,7 @@ private static PolicyManager createPolicyManager() {
147147
new LoadNativeLibrariesEntitlement(),
148148
new ManageThreadsEntitlement(),
149149
new FilesEntitlement(
150-
List.of(new FilesEntitlement.FileData(EntitlementBootstrap.bootstrapArgs().tempDir().toString(), READ_WRITE))
150+
List.of(FilesEntitlement.FileData.ofPath(EntitlementBootstrap.bootstrapArgs().tempDir(), READ_WRITE))
151151
)
152152
)
153153
),
@@ -159,7 +159,7 @@ private static PolicyManager createPolicyManager() {
159159
"org.elasticsearch.nativeaccess",
160160
List.of(
161161
new LoadNativeLibrariesEntitlement(),
162-
new FilesEntitlement(Arrays.stream(dataDirs).map(d -> new FileData(d.toString(), READ_WRITE)).toList())
162+
new FilesEntitlement(List.of(FileData.ofRelativePath(Path.of(""), FilesEntitlement.BaseDir.DATA, READ_WRITE)))
163163
)
164164
)
165165
)
@@ -175,7 +175,7 @@ private static PolicyManager createPolicyManager() {
175175
resolver,
176176
AGENTS_PACKAGE_NAME,
177177
ENTITLEMENTS_MODULE,
178-
tempDir
178+
pathLookup
179179
);
180180
}
181181

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,30 @@
2020
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
2121

2222
public final class FileAccessTree {
23+
2324
private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator();
2425

2526
private final String[] readPaths;
2627
private final String[] writePaths;
2728

28-
private FileAccessTree(FilesEntitlement filesEntitlement, Path tempDir) {
29+
private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup) {
2930
List<String> readPaths = new ArrayList<>();
3031
List<String> writePaths = new ArrayList<>();
3132
for (FilesEntitlement.FileData fileData : filesEntitlement.filesData()) {
32-
var path = normalizePath(Path.of(fileData.path()));
3333
var mode = fileData.mode();
34-
if (mode == FilesEntitlement.Mode.READ_WRITE) {
35-
writePaths.add(path);
36-
}
37-
readPaths.add(path);
34+
var paths = fileData.resolvePaths(pathLookup);
35+
paths.forEach(path -> {
36+
var normalized = normalizePath(path);
37+
if (mode == FilesEntitlement.Mode.READ_WRITE) {
38+
writePaths.add(normalized);
39+
}
40+
readPaths.add(normalized);
41+
});
3842
}
3943

4044
// everything has access to the temp dir
41-
readPaths.add(tempDir.toString());
42-
writePaths.add(tempDir.toString());
45+
readPaths.add(pathLookup.tempDir().toString());
46+
writePaths.add(pathLookup.tempDir().toString());
4347

4448
readPaths.sort(String::compareTo);
4549
writePaths.sort(String::compareTo);
@@ -48,8 +52,8 @@ private FileAccessTree(FilesEntitlement filesEntitlement, Path tempDir) {
4852
this.writePaths = writePaths.toArray(new String[0]);
4953
}
5054

51-
public static FileAccessTree of(FilesEntitlement filesEntitlement, Path tempDir) {
52-
return new FileAccessTree(filesEntitlement, tempDir);
55+
public static FileAccessTree of(FilesEntitlement filesEntitlement, PathLookup pathLookup) {
56+
return new FileAccessTree(filesEntitlement, pathLookup);
5357
}
5458

5559
boolean canRead(Path path) {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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;
11+
12+
import java.nio.file.Path;
13+
14+
public record PathLookup(Path configDir, Path[] dataDirs, Path tempDir) {}

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ ModuleEntitlements policyEntitlements(String componentName, List<Entitlement> en
9999
return new ModuleEntitlements(
100100
componentName,
101101
entitlements.stream().collect(groupingBy(Entitlement::getClass)),
102-
FileAccessTree.of(filesEntitlement, tempDir)
102+
FileAccessTree.of(filesEntitlement, pathLookup)
103103
);
104104
}
105105

@@ -109,7 +109,7 @@ ModuleEntitlements policyEntitlements(String componentName, List<Entitlement> en
109109
private final List<Entitlement> apmAgentEntitlements;
110110
private final Map<String, Map<String, List<Entitlement>>> pluginsEntitlements;
111111
private final Function<Class<?>, String> pluginResolver;
112-
private final Path tempDir;
112+
private final PathLookup pathLookup;
113113
private final FileAccessTree defaultFileAccess;
114114

115115
public static final String ALL_UNNAMED = "ALL-UNNAMED";
@@ -146,7 +146,7 @@ public PolicyManager(
146146
Function<Class<?>, String> pluginResolver,
147147
String apmAgentPackageName,
148148
Module entitlementsModule,
149-
Path tempDir
149+
PathLookup pathLookup
150150
) {
151151
this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy));
152152
this.apmAgentEntitlements = apmAgentEntitlements;
@@ -156,9 +156,8 @@ public PolicyManager(
156156
this.pluginResolver = pluginResolver;
157157
this.apmAgentPackageName = apmAgentPackageName;
158158
this.entitlementsModule = entitlementsModule;
159-
this.defaultFileAccess = FileAccessTree.of(FilesEntitlement.EMPTY, tempDir);
160-
161-
this.tempDir = tempDir;
159+
this.pathLookup = requireNonNull(pathLookup);
160+
this.defaultFileAccess = FileAccessTree.of(FilesEntitlement.EMPTY, pathLookup);
162161

163162
for (var e : serverEntitlements.entrySet()) {
164163
validateEntitlementsPerModule(SERVER_COMPONENT_NAME, e.getKey(), e.getValue());

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

Lines changed: 142 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010
package org.elasticsearch.entitlement.runtime.policy.entitlements;
1111

1212
import org.elasticsearch.entitlement.runtime.policy.ExternalEntitlement;
13+
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
1314
import org.elasticsearch.entitlement.runtime.policy.PolicyValidationException;
1415

16+
import java.nio.file.Path;
1517
import java.util.ArrayList;
18+
import java.util.Arrays;
1619
import java.util.HashMap;
1720
import java.util.List;
1821
import java.util.Map;
22+
import java.util.Objects;
23+
import java.util.stream.Stream;
1924

2025
/**
2126
* Describes a file entitlement with a path and mode.
@@ -29,8 +34,104 @@ public enum Mode {
2934
READ_WRITE
3035
}
3136

32-
public record FileData(String path, Mode mode) {
37+
public enum BaseDir {
38+
CONFIG,
39+
DATA
40+
}
41+
42+
public sealed interface FileData {
43+
44+
final class AbsolutePathFileData implements FileData {
45+
private final Path path;
46+
private final Mode mode;
47+
48+
private AbsolutePathFileData(Path path, Mode mode) {
49+
this.path = path;
50+
this.mode = mode;
51+
}
52+
53+
@Override
54+
public Stream<Path> resolvePaths(PathLookup pathLookup) {
55+
return Stream.of(path);
56+
}
57+
58+
@Override
59+
public Mode mode() {
60+
return mode;
61+
}
62+
63+
@Override
64+
public boolean equals(Object obj) {
65+
if (obj == this) return true;
66+
if (obj == null || obj.getClass() != this.getClass()) return false;
67+
var that = (AbsolutePathFileData) obj;
68+
return Objects.equals(this.path, that.path) && Objects.equals(this.mode, that.mode);
69+
}
70+
71+
@Override
72+
public int hashCode() {
73+
return Objects.hash(path, mode);
74+
}
75+
}
76+
77+
final class RelativePathFileData implements FileData {
78+
private final Path relativePath;
79+
private final BaseDir baseDir;
80+
private final Mode mode;
81+
82+
private RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode) {
83+
this.relativePath = relativePath;
84+
this.baseDir = baseDir;
85+
this.mode = mode;
86+
}
87+
88+
@Override
89+
public Stream<Path> resolvePaths(PathLookup pathLookup) {
90+
Objects.requireNonNull(pathLookup);
91+
switch (baseDir) {
92+
case CONFIG:
93+
return Stream.of(pathLookup.configDir().resolve(relativePath));
94+
case DATA:
95+
return Arrays.stream(pathLookup.dataDirs()).map(d -> d.resolve(relativePath));
96+
default:
97+
throw new IllegalArgumentException();
98+
}
99+
}
100+
101+
@Override
102+
public Mode mode() {
103+
return mode;
104+
}
33105

106+
@Override
107+
public boolean equals(Object obj) {
108+
if (obj == this) return true;
109+
if (obj == null || obj.getClass() != this.getClass()) return false;
110+
var that = (RelativePathFileData) obj;
111+
return Objects.equals(this.mode, that.mode)
112+
&& Objects.equals(this.relativePath, that.relativePath)
113+
&& Objects.equals(this.baseDir, that.baseDir);
114+
}
115+
116+
@Override
117+
public int hashCode() {
118+
return Objects.hash(relativePath, baseDir, mode);
119+
}
120+
}
121+
122+
static FileData ofPath(Path path, Mode mode) {
123+
assert path.isAbsolute();
124+
return new AbsolutePathFileData(path, mode);
125+
}
126+
127+
static FileData ofRelativePath(Path relativePath, BaseDir baseDir, Mode mode) {
128+
assert relativePath.isAbsolute() == false;
129+
return new RelativePathFileData(relativePath, baseDir, mode);
130+
}
131+
132+
Stream<Path> resolvePaths(PathLookup pathLookup);
133+
134+
Mode mode();
34135
}
35136

36137
private static Mode parseMode(String mode) {
@@ -43,6 +144,15 @@ private static Mode parseMode(String mode) {
43144
}
44145
}
45146

147+
private static BaseDir parseBaseDir(String baseDir) {
148+
if (baseDir.equals("config")) {
149+
return BaseDir.CONFIG;
150+
} else if (baseDir.equals("data")) {
151+
return BaseDir.DATA;
152+
}
153+
throw new PolicyValidationException("invalid relative directory: " + baseDir + ", valid values: [config, data]");
154+
}
155+
46156
@ExternalEntitlement(parameterNames = { "paths" }, esModulesOnly = false)
47157
@SuppressWarnings("unchecked")
48158
public static FilesEntitlement build(List<Object> paths) {
@@ -52,18 +162,41 @@ public static FilesEntitlement build(List<Object> paths) {
52162
List<FileData> filesData = new ArrayList<>();
53163
for (Object object : paths) {
54164
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-
}
165+
String pathAsString = file.remove("path");
166+
String relativePathAsString = file.remove("relative_path");
167+
String relativeTo = file.remove("relative_to");
59168
String mode = file.remove("mode");
169+
170+
if (file.isEmpty() == false) {
171+
throw new PolicyValidationException("unknown key(s) [" + file + "] in a listed file for files entitlement");
172+
}
60173
if (mode == null) {
61-
throw new PolicyValidationException("files entitlement must contain mode for every listed file");
174+
throw new PolicyValidationException("files entitlement must contain 'mode' for every listed file");
62175
}
63-
if (file.isEmpty() == false) {
64-
throw new PolicyValidationException("unknown key(s) " + file + " in a listed file for files entitlement");
176+
if (pathAsString != null && relativePathAsString != null) {
177+
throw new PolicyValidationException("a files entitlement entry cannot contain both 'path' and 'relative_path'");
178+
}
179+
180+
if (relativePathAsString != null) {
181+
if (relativeTo == null) {
182+
throw new PolicyValidationException("files entitlement with a 'relative_path' must specify 'relative_to'");
183+
}
184+
final BaseDir baseDir = parseBaseDir(relativeTo);
185+
186+
Path relativePath = Path.of(relativePathAsString);
187+
if (relativePath.isAbsolute()) {
188+
throw new PolicyValidationException("'relative_path' [" + relativePathAsString + "] must be relative");
189+
}
190+
filesData.add(FileData.ofRelativePath(relativePath, baseDir, parseMode(mode)));
191+
} else if (pathAsString != null) {
192+
Path path = Path.of(pathAsString);
193+
if (path.isAbsolute() == false) {
194+
throw new PolicyValidationException("'path' [" + pathAsString + "] must be absolute");
195+
}
196+
filesData.add(FileData.ofPath(path, parseMode(mode)));
197+
} else {
198+
throw new PolicyValidationException("files entitlement must contain either 'path' or 'relative_path' for every entry");
65199
}
66-
filesData.add(new FileData(path, parseMode(mode)));
67200
}
68201
return new FilesEntitlement(filesData);
69202
}

0 commit comments

Comments
 (0)