Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;

import static java.util.Objects.requireNonNull;

Expand All @@ -36,19 +37,24 @@ public class EntitlementBootstrap {
public record BootstrapArgs(
Map<String, Policy> pluginPolicies,
Function<Class<?>, String> pluginResolver,
Function<String, String> settingResolver,
Function<String, Stream<String>> settingGlobResolver,
Path[] dataDirs,
Path configDir,
Path tempDir,
Path logsDir
Path logsDir,
Path tempDir
) {
public BootstrapArgs {
requireNonNull(pluginPolicies);
requireNonNull(pluginResolver);
requireNonNull(settingResolver);
requireNonNull(settingGlobResolver);
requireNonNull(dataDirs);
if (dataDirs.length == 0) {
throw new IllegalArgumentException("must provide at least one data directory");
}
requireNonNull(configDir);
requireNonNull(logsDir);
requireNonNull(tempDir);
}
}
Expand All @@ -73,16 +79,27 @@ public static BootstrapArgs bootstrapArgs() {
public static void bootstrap(
Map<String, Policy> pluginPolicies,
Function<Class<?>, String> pluginResolver,
Function<String, String> settingResolver,
Function<String, Stream<String>> settingGlobResolver,
Path[] dataDirs,
Path configDir,
Path tempDir,
Path logsDir
Path logsDir,
Path tempDir
) {
logger.debug("Loading entitlement agent");
if (EntitlementBootstrap.bootstrapArgs != null) {
throw new IllegalStateException("plugin data is already set");
}
EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(pluginPolicies, pluginResolver, dataDirs, configDir, tempDir, logsDir);
EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(
pluginPolicies,
pluginResolver,
settingResolver,
settingGlobResolver,
dataDirs,
configDir,
logsDir,
tempDir
);
exportInitializationToAgent();
loadAgent(findAgentJar());
selfTest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,14 @@ private static Class<?>[] findClassesToRetransform(Class<?>[] loadedClasses, Set
private static PolicyManager createPolicyManager() {
EntitlementBootstrap.BootstrapArgs bootstrapArgs = EntitlementBootstrap.bootstrapArgs();
Map<String, Policy> pluginPolicies = bootstrapArgs.pluginPolicies();
var pathLookup = new PathLookup(getUserHome(), bootstrapArgs.configDir(), bootstrapArgs.dataDirs(), bootstrapArgs.tempDir());
Path logsDir = EntitlementBootstrap.bootstrapArgs().logsDir();
var pathLookup = new PathLookup(
getUserHome(),
bootstrapArgs.configDir(),
bootstrapArgs.dataDirs(),
bootstrapArgs.tempDir(),
bootstrapArgs.settingResolver(),
bootstrapArgs.settingGlobResolver()
);

List<Scope> serverScopes = new ArrayList<>();
Collections.addAll(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup)
}

// everything has access to the temp dir
readPaths.add(pathLookup.tempDir().toString());
writePaths.add(pathLookup.tempDir().toString());
String tempDir = normalizePath(pathLookup.tempDir());
readPaths.add(tempDir);
writePaths.add(tempDir);

readPaths.sort(String::compareTo);
writePaths.sort(String::compareTo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,14 @@
package org.elasticsearch.entitlement.runtime.policy;

import java.nio.file.Path;
import java.util.function.Function;
import java.util.stream.Stream;

public record PathLookup(Path homeDir, Path configDir, Path[] dataDirs, Path tempDir) {}
public record PathLookup(
Path homeDir,
Path configDir,
Path[] dataDirs,
Path tempDir,
Function<String, String> settingResolver,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I think and interface is better than a record here; a record with 2 Function is just and interface in disguise. OK to keep *Dir() as simple getters though.
Not blocking.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still in favor of the record here as there is only a single implementation. The resolvers are just necessary due to Settings not being available in the entitlements lib

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Moritz, I prefer a record because it is "things" we are passing in. Creating an interface would require creating an implementation, but the record provides that.

Function<String, Stream<String>> settingGlobResolver
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -53,33 +52,81 @@ static FileData ofPath(Path path, Mode mode) {
static FileData ofRelativePath(Path relativePath, BaseDir baseDir, Mode mode) {
return new RelativePathFileData(relativePath, baseDir, mode);
}
}

private record AbsolutePathFileData(Path path, Mode mode) implements FileData {
@Override
public Stream<Path> resolvePaths(PathLookup pathLookup) {
return Stream.of(path);
static FileData ofPathSetting(String setting, Mode mode) {
return new PathSettingFileData(setting, mode);
}

static FileData ofRelativePathSetting(String setting, BaseDir baseDir, Mode mode) {
return new RelativePathSettingFileData(setting, baseDir, mode);
}
}

private record RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode) implements FileData {
private sealed interface RelativeFileData extends FileData {
BaseDir baseDir();

Stream<Path> resolveRelativePaths(PathLookup pathLookup);

@Override
public Stream<Path> resolvePaths(PathLookup pathLookup) {
default Stream<Path> resolvePaths(PathLookup pathLookup) {
Objects.requireNonNull(pathLookup);
switch (baseDir) {
var relativePaths = resolveRelativePaths(pathLookup);
switch (baseDir()) {
case CONFIG:
return Stream.of(pathLookup.configDir().resolve(relativePath));
return relativePaths.map(relativePath -> pathLookup.configDir().resolve(relativePath));
case DATA:
return Arrays.stream(pathLookup.dataDirs()).map(d -> d.resolve(relativePath));
// multiple data dirs are a pain...we need the combination of relative paths and data dirs
List<Path> paths = new ArrayList<>();
for (var relativePath : relativePaths.toList()) {
for (var dataDir : pathLookup.dataDirs()) {
paths.add(dataDir.resolve(relativePath));
}
}
return paths.stream();
case HOME:
return Stream.of(pathLookup.homeDir().resolve(relativePath));
return relativePaths.map(relativePath -> pathLookup.homeDir().resolve(relativePath));
default:
throw new IllegalArgumentException();
}
}
}

private record AbsolutePathFileData(Path path, Mode mode) implements FileData {
@Override
public Stream<Path> resolvePaths(PathLookup pathLookup) {
return Stream.of(path);
}
}

private record RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode) implements FileData, RelativeFileData {
@Override
public Stream<Path> resolveRelativePaths(PathLookup pathLookup) {
return Stream.of(relativePath);
}
}

private record PathSettingFileData(String setting, Mode mode) implements FileData {
@Override
public Stream<Path> resolvePaths(PathLookup pathLookup) {
return resolvePathSettings(pathLookup, setting);
}
}

private record RelativePathSettingFileData(String setting, BaseDir baseDir, Mode mode) implements FileData, RelativeFileData {
@Override
public Stream<Path> resolveRelativePaths(PathLookup pathLookup) {
return resolvePathSettings(pathLookup, setting);
}
}

private static Stream<Path> resolvePathSettings(PathLookup pathLookup, String setting) {
if (setting.contains("*")) {
return pathLookup.settingGlobResolver().apply(setting).map(Path::of);
}
String path = pathLookup.settingResolver().apply(setting);
return path == null ? Stream.of() : Stream.of(Path.of(path));
}

private static Mode parseMode(String mode) {
if (mode.equals("read")) {
return Mode.READ;
Expand Down Expand Up @@ -113,37 +160,56 @@ public static FilesEntitlement build(List<Object> paths) {
String pathAsString = file.remove("path");
String relativePathAsString = file.remove("relative_path");
String relativeTo = file.remove("relative_to");
String mode = file.remove("mode");
String pathSetting = file.remove("path_setting");
String relativePathSetting = file.remove("relative_path_setting");
String modeAsString = file.remove("mode");

if (file.isEmpty() == false) {
throw new PolicyValidationException("unknown key(s) [" + file + "] in a listed file for files entitlement");
}
if (mode == null) {
int foundKeys = (pathAsString != null ? 1 : 0) + (relativePathAsString != null ? 1 : 0) + (pathSetting != null ? 1 : 0)
+ (relativePathSetting != null ? 1 : 0);
if (foundKeys != 1) {
throw new PolicyValidationException(
"a files entitlement entry must contain one of " + "[path, relative_path, path_setting, relative_path_setting]"
);
}

if (modeAsString == null) {
throw new PolicyValidationException("files entitlement must contain 'mode' for every listed file");
}
if (pathAsString != null && relativePathAsString != null) {
throw new PolicyValidationException("a files entitlement entry cannot contain both 'path' and 'relative_path'");
Mode mode = parseMode(modeAsString);

BaseDir baseDir = null;
if (relativeTo != null) {
baseDir = parseBaseDir(relativeTo);
}

if (relativePathAsString != null) {
if (relativeTo == null) {
if (baseDir == null) {
throw new PolicyValidationException("files entitlement with a 'relative_path' must specify 'relative_to'");
}
final BaseDir baseDir = parseBaseDir(relativeTo);

Path relativePath = Path.of(relativePathAsString);
if (relativePath.isAbsolute()) {
throw new PolicyValidationException("'relative_path' [" + relativePathAsString + "] must be relative");
}
filesData.add(FileData.ofRelativePath(relativePath, baseDir, parseMode(mode)));
filesData.add(FileData.ofRelativePath(relativePath, baseDir, mode));
} else if (pathAsString != null) {
Path path = Path.of(pathAsString);
if (path.isAbsolute() == false) {
throw new PolicyValidationException("'path' [" + pathAsString + "] must be absolute");
}
filesData.add(FileData.ofPath(path, parseMode(mode)));
filesData.add(FileData.ofPath(path, mode));
} else if (pathSetting != null) {
filesData.add(FileData.ofPathSetting(pathSetting, mode));
} else if (relativePathSetting != null) {
if (baseDir == null) {
throw new PolicyValidationException("files entitlement with a 'relative_path_setting' must specify 'relative_to'");
}
filesData.add(FileData.ofRelativePathSetting(relativePathSetting, baseDir, mode));
} else {
throw new PolicyValidationException("files entitlement must contain either 'path' or 'relative_path' for every entry");
throw new AssertionError("File entry validation error");
}
}
return new FilesEntitlement(filesData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

package org.elasticsearch.entitlement.runtime.policy;

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

static Path root;
static Settings settings;

@BeforeClass
public static void setupRoot() {
root = createTempDir();
settings = Settings.EMPTY;
}

private static Path path(String s) {
Expand All @@ -39,7 +42,9 @@ private static Path path(String s) {
Path.of("/home"),
Path.of("/config"),
new Path[] { Path.of("/data1"), Path.of("/data2") },
Path.of("/tmp")
Path.of("/tmp"),
setting -> settings.get(setting),
glob -> settings.getGlobValues(glob)
);

public void testEmpty() {
Expand Down Expand Up @@ -163,13 +168,9 @@ public void testForwardSlashes() {
}

public void testTempDirAccess() {
Path tempDir = createTempDir();
var tree = FileAccessTree.of(
FilesEntitlement.EMPTY,
new PathLookup(Path.of("/home"), Path.of("/config"), new Path[] { Path.of("/data1"), Path.of("/data2") }, tempDir)
);
assertThat(tree.canRead(tempDir), is(true));
assertThat(tree.canWrite(tempDir), is(true));
var tree = FileAccessTree.of(FilesEntitlement.EMPTY, TEST_PATH_LOOKUP);
assertThat(tree.canRead(TEST_PATH_LOOKUP.tempDir()), is(true));
assertThat(tree.canWrite(TEST_PATH_LOOKUP.tempDir()), is(true));
}

FileAccessTree accessTree(FilesEntitlement entitlement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

package org.elasticsearch.entitlement.runtime.policy;

import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager.ModuleEntitlements;
import org.elasticsearch.entitlement.runtime.policy.agent.TestAgent;
import org.elasticsearch.entitlement.runtime.policy.agent.inner.TestInnerAgent;
Expand Down Expand Up @@ -68,7 +69,9 @@ public static void beforeClass() {
TEST_BASE_DIR.resolve("/user/home"),
TEST_BASE_DIR.resolve("/config"),
new Path[] { TEST_BASE_DIR.resolve("/data1/"), TEST_BASE_DIR.resolve("/data2") },
TEST_BASE_DIR.resolve("/temp")
TEST_BASE_DIR.resolve("/temp"),
Settings.EMPTY::get,
Settings.EMPTY::getGlobValues
);
} catch (Exception e) {
throw new IllegalStateException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public void testEntitlementMutuallyExclusiveParameters() {
""".getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false).parsePolicy());
assertEquals(
"[2:5] policy parsing error for [test-failure-policy.yaml] in scope [entitlement-module-name] "
+ "for entitlement type [files]: a files entitlement entry cannot contain both 'path' and 'relative_path'",
+ "for entitlement type [files]: a files entitlement entry must contain one of "
+ "[path, relative_path, path_setting, relative_path_setting]",
ppe.getMessage()
);
}
Expand All @@ -87,7 +88,8 @@ public void testEntitlementAtLeastOneParameter() {
""".getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false).parsePolicy());
assertEquals(
"[2:5] policy parsing error for [test-failure-policy.yaml] in scope [entitlement-module-name] "
+ "for entitlement type [files]: files entitlement must contain either 'path' or 'relative_path' for every entry",
+ "for entitlement type [files]: a files entitlement entry must contain one of "
+ "[path, relative_path, path_setting, relative_path_setting]",
ppe.getMessage()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ public void testParseFiles() throws IOException {
mode: "read"
- path: '%s'
mode: "read_write"
- path_setting: foo.bar
mode: read
- relative_path_setting: foo.bar
relative_to: config
mode: read
""", relativePathToFile, relativePathToDir, TEST_ABSOLUTE_PATH_TO_FILE).getBytes(StandardCharsets.UTF_8)),
"test-policy.yaml",
false
Expand All @@ -196,7 +201,9 @@ public void testParseFiles() throws IOException {
List.of(
Map.of("relative_path", relativePathToFile, "mode", "read_write", "relative_to", "data"),
Map.of("relative_path", relativePathToDir, "mode", "read", "relative_to", "config"),
Map.of("path", TEST_ABSOLUTE_PATH_TO_FILE, "mode", "read_write")
Map.of("path", TEST_ABSOLUTE_PATH_TO_FILE, "mode", "read_write"),
Map.of("path_setting", "foo.bar", "mode", "read"),
Map.of("relative_path_setting", "foo.bar", "relative_to", "config", "mode", "read")
)
)
)
Expand Down
Loading