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,13 +37,17 @@ 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
) {
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");
Expand Down Expand Up @@ -71,6 +76,8 @@ 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
Expand All @@ -79,7 +86,15 @@ public static void bootstrap(
if (EntitlementBootstrap.bootstrapArgs != null) {
throw new IllegalStateException("plugin data is already set");
}
EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(pluginPolicies, pluginResolver, dataDirs, configDir, tempDir);
EntitlementBootstrap.bootstrapArgs = new BootstrapArgs(
pluginPolicies,
pluginResolver,
settingResolver,
settingGlobResolver,
dataDirs,
configDir,
tempDir
);
exportInitializationToAgent();
loadAgent(findAgentJar());
selfTest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,13 @@ 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(bootstrapArgs.configDir(), bootstrapArgs.dataDirs(), bootstrapArgs.tempDir());
var pathLookup = new PathLookup(
bootstrapArgs.configDir(),
bootstrapArgs.dataDirs(),
bootstrapArgs.tempDir(),
bootstrapArgs.settingResolver(),
bootstrapArgs.settingGlobResolver()
);

// TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it
var serverPolicy = new Policy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,13 @@
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 configDir, Path[] dataDirs, Path tempDir) {}
public record PathLookup(
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 @@ -41,6 +41,33 @@ public enum BaseDir {

public sealed interface FileData {

sealed interface RelativeFileData extends FileData {
BaseDir baseDir();

Stream<Path> resolveRelativePaths(PathLookup pathLookup);

@Override
default Stream<Path> resolvePaths(PathLookup pathLookup) {
Objects.requireNonNull(pathLookup);
var relativePaths = resolveRelativePaths(pathLookup);
switch (baseDir()) {
case CONFIG:
return relativePaths.map(relativePath -> pathLookup.configDir().resolve(relativePath));
case DATA:
// 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();
default:
throw new IllegalArgumentException();
}
}
}

final class AbsolutePathFileData implements FileData {
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: I'll align these implementations completely once #122658 is merged.

private final Path path;
private final Mode mode;
Expand Down Expand Up @@ -119,6 +146,28 @@ public int hashCode() {
}
}

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

record RelativePathSettingFileData(String setting, BaseDir baseDir, Mode mode) implements FileData, RelativeFileData {
@Override
public Stream<Path> resolveRelativePaths(PathLookup pathLookup) {
return FileData.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));
}

static FileData ofPath(Path path, Mode mode) {
assert path.isAbsolute();
return new AbsolutePathFileData(path, mode);
Expand All @@ -129,6 +178,14 @@ static FileData ofRelativePath(Path relativePath, BaseDir baseDir, Mode mode) {
return new RelativePathFileData(relativePath, baseDir, mode);
}

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);
}

Stream<Path> resolvePaths(PathLookup pathLookup);

Mode mode();
Expand Down Expand Up @@ -165,37 +222,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 @@ -38,7 +41,9 @@ private static Path path(String s) {
private static final PathLookup TEST_PATH_LOOKUP = new PathLookup(
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 @@ -158,13 +163,9 @@ public void testForwardSlashes() {
}

public void testTempDirAccess() {
Path tempDir = createTempDir();
var tree = FileAccessTree.of(
FilesEntitlement.EMPTY,
new PathLookup(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 @@ -56,7 +57,9 @@ public class PolicyManagerTests extends ESTestCase {
private static final PathLookup TEST_PATH_LOOKUP = new PathLookup(
Path.of("/config"),
new Path[] { Path.of("/data1/"), Path.of("/data2") },
Path.of("/temp")
Path.of("/temp"),
Settings.EMPTY::get,
Settings.EMPTY::getGlobValues
);

@BeforeClass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,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 @@ -116,7 +117,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 @@ -153,7 +153,7 @@ public void testParseFiles() throws IOException {
);
assertEquals(expected, policyWithTwoPaths);

Policy policyWithMultiplePathsAndBaseDir = new PolicyParser(new ByteArrayInputStream("""
Policy policyWithMultiplePathTypes = new PolicyParser(new ByteArrayInputStream("""
entitlement-module-name:
- files:
- relative_path: "test/path/to/file"
Expand All @@ -164,6 +164,11 @@ public void testParseFiles() throws IOException {
mode: "read"
- path: "/path/to/file"
mode: "read_write"
- path_setting: foo.bar
mode: read
- relative_path_setting: foo.bar
relative_to: config
mode: read
""".getBytes(StandardCharsets.UTF_8)), "test-policy.yaml", false).parsePolicy();
expected = new Policy(
"test-policy.yaml",
Expand All @@ -175,14 +180,16 @@ public void testParseFiles() throws IOException {
List.of(
Map.of("relative_path", "test/path/to/file", "mode", "read_write", "relative_to", "data"),
Map.of("relative_path", "test/path/to/read-dir/", "mode", "read", "relative_to", "config"),
Map.of("path", "/path/to/file", "mode", "read_write")
Map.of("path", "/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")
)
)
)
)
)
);
assertEquals(expected, policyWithMultiplePathsAndBaseDir);
assertEquals(expected, policyWithMultiplePathTypes);
}

public void testParseNetwork() throws IOException {
Expand Down
Loading