Skip to content

Commit

Permalink
Merge all file entitlements into a single files entitlement (elastic#…
Browse files Browse the repository at this point in the history
…121864)

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.
  • Loading branch information
jdconrad committed Feb 6, 2025
1 parent ecfa62e commit e95ec45
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,17 @@ public abstract class AbstractEntitlementsIT extends ESRestTestCase {
Map.of("properties", List.of("es.entitlements.checkSetSystemProperty", "es.entitlements.checkClearSystemProperty"))
)
);

builder.value(Map.of("file", Map.of("path", tempDir.resolve("read_dir"), "mode", "read")));
builder.value(Map.of("file", Map.of("path", tempDir.resolve("read_write_dir"), "mode", "read_write")));
builder.value(Map.of("file", Map.of("path", tempDir.resolve("read_file"), "mode", "read")));
builder.value(Map.of("file", Map.of("path", tempDir.resolve("read_write_file"), "mode", "read_write")));
builder.value(
Map.of(
"files",
List.of(
Map.of("path", tempDir.resolve("read_dir"), "mode", "read"),
Map.of("path", tempDir.resolve("read_write_dir"), "mode", "read_write"),
Map.of("path", tempDir.resolve("read_file"), "mode", "read"),
Map.of("path", tempDir.resolve("read_write_file"), "mode", "read_write")
)
)
);
};

private final String actionName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

package org.elasticsearch.entitlement.runtime.policy;

import org.elasticsearch.entitlement.runtime.policy.entitlements.FileEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;

import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -20,18 +20,19 @@
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;

public final class FileAccessTree {
public static final FileAccessTree EMPTY = new FileAccessTree(List.of());
public static final FileAccessTree EMPTY = new FileAccessTree(FilesEntitlement.EMPTY);
private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator();

private final String[] readPaths;
private final String[] writePaths;

private FileAccessTree(List<FileEntitlement> fileEntitlements) {
private FileAccessTree(FilesEntitlement filesEntitlement) {
List<String> readPaths = new ArrayList<>();
List<String> writePaths = new ArrayList<>();
for (FileEntitlement fileEntitlement : fileEntitlements) {
String path = normalizePath(Path.of(fileEntitlement.path()));
if (fileEntitlement.mode() == FileEntitlement.Mode.READ_WRITE) {
for (FilesEntitlement.FileData fileData : filesEntitlement.filesData()) {
var path = normalizePath(Path.of(fileData.path()));
var mode = fileData.mode();
if (mode == FilesEntitlement.Mode.READ_WRITE) {
writePaths.add(path);
}
readPaths.add(path);
Expand All @@ -44,8 +45,8 @@ private FileAccessTree(List<FileEntitlement> fileEntitlements) {
this.writePaths = writePaths.toArray(new String[0]);
}

public static FileAccessTree of(List<FileEntitlement> fileEntitlements) {
return new FileAccessTree(fileEntitlements);
public static FileAccessTree of(FilesEntitlement filesEntitlement) {
return new FileAccessTree(filesEntitlement);
}

boolean canRead(Path path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.ExitVMEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FileEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
Expand Down Expand Up @@ -73,14 +73,16 @@ public static ModuleEntitlements none(String componentName) {
}

public static ModuleEntitlements from(String componentName, List<Entitlement> entitlements) {
var fileEntitlements = entitlements.stream()
.filter(e -> e.getClass().equals(FileEntitlement.class))
.map(e -> (FileEntitlement) e)
.toList();
FilesEntitlement filesEntitlement = FilesEntitlement.EMPTY;
for (Entitlement entitlement : entitlements) {
if (entitlement instanceof FilesEntitlement) {
filesEntitlement = (FilesEntitlement) entitlement;
}
}
return new ModuleEntitlements(
componentName,
entitlements.stream().collect(groupingBy(Entitlement::getClass)),
FileAccessTree.of(fileEntitlements)
FileAccessTree.of(filesEntitlement)
);
}

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

private static void validateEntitlementsPerModule(String componentName, String moduleName, List<Entitlement> entitlements) {
Set<Class<? extends Entitlement>> flagEntitlements = new HashSet<>();
Set<Class<? extends Entitlement>> found = new HashSet<>();
for (var e : entitlements) {
if (e instanceof FileEntitlement) {
continue;
}
if (flagEntitlements.contains(e.getClass())) {
if (found.contains(e.getClass())) {
throw new IllegalArgumentException(
"["
+ componentName
+ "] using module ["
+ moduleName
+ "] found duplicate flag entitlements ["
+ e.getClass().getName()
+ "]"
"[" + componentName + "] using module [" + moduleName + "] found duplicate entitlement [" + e.getClass().getName() + "]"
);
}
flagEntitlements.add(e.getClass());
found.add(e.getClass());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FileEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
Expand Down Expand Up @@ -46,7 +46,7 @@
public class PolicyParser {

private static final Map<String, Class<?>> EXTERNAL_ENTITLEMENTS = Stream.of(
FileEntitlement.class,
FilesEntitlement.class,
CreateClassLoaderEntitlement.class,
SetHttpsConnectionPropertiesEntitlement.class,
OutboundNetworkEntitlement.class,
Expand Down Expand Up @@ -197,34 +197,41 @@ protected Entitlement parseEntitlement(String scopeName, String entitlementType)
? entitlementConstructor.getParameterTypes()
: entitlementMethod.getParameterTypes();
String[] parametersNames = entitlementMetadata.parameterNames();
Object[] parameterValues = new Object[parameterTypes.length];

if (parameterTypes.length != 0 || parametersNames.length != 0) {
if (policyParser.nextToken() != XContentParser.Token.START_OBJECT) {
throw newPolicyParserException(scopeName, entitlementType, "expected entitlement parameters");
}
}

Map<String, Object> parsedValues = policyParser.map();
if (policyParser.nextToken() == XContentParser.Token.START_OBJECT) {
Map<String, Object> parsedValues = policyParser.map();

Object[] parameterValues = new Object[parameterTypes.length];
for (int parameterIndex = 0; parameterIndex < parameterTypes.length; ++parameterIndex) {
String parameterName = parametersNames[parameterIndex];
Object parameterValue = parsedValues.remove(parameterName);
if (parameterValue == null) {
throw newPolicyParserException(scopeName, entitlementType, "missing entitlement parameter [" + parameterName + "]");
}
Class<?> parameterType = parameterTypes[parameterIndex];
if (parameterType.isAssignableFrom(parameterValue.getClass()) == false) {
throw newPolicyParserException(
scopeName,
entitlementType,
"unexpected parameter type [" + parameterType.getSimpleName() + "] for entitlement parameter [" + parameterName + "]"
);
for (int parameterIndex = 0; parameterIndex < parameterTypes.length; ++parameterIndex) {
String parameterName = parametersNames[parameterIndex];
Object parameterValue = parsedValues.remove(parameterName);
if (parameterValue == null) {
throw newPolicyParserException(scopeName, entitlementType, "missing entitlement parameter [" + parameterName + "]");
}
Class<?> parameterType = parameterTypes[parameterIndex];
if (parameterType.isAssignableFrom(parameterValue.getClass()) == false) {
throw newPolicyParserException(
scopeName,
entitlementType,
"unexpected parameter type ["
+ parameterType.getSimpleName()
+ "] for entitlement parameter ["
+ parameterName
+ "]"
);
}
parameterValues[parameterIndex] = parameterValue;
}
if (parsedValues.isEmpty() == false) {
throw newPolicyParserException(scopeName, entitlementType, "extraneous entitlement parameter(s) " + parsedValues);
}
} else if (policyParser.currentToken() == XContentParser.Token.START_ARRAY) {
List<Object> parsedValues = policyParser.list();
parameterValues[0] = parsedValues;
} else {
throw newPolicyParserException(scopeName, entitlementType, "expected entitlement parameters");
}
parameterValues[parameterIndex] = parameterValue;
}
if (parsedValues.isEmpty() == false) {
throw newPolicyParserException(scopeName, entitlementType, "extraneous entitlement parameter(s) " + parsedValues);
}

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.entitlement.runtime.policy.entitlements;

import org.elasticsearch.entitlement.runtime.policy.ExternalEntitlement;
import org.elasticsearch.entitlement.runtime.policy.PolicyValidationException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Describes a file entitlement with a path and mode.
*/
public record FilesEntitlement(List<FileData> filesData) implements Entitlement {

public static final FilesEntitlement EMPTY = new FilesEntitlement(List.of());

public enum Mode {
READ,
READ_WRITE
}

public record FileData(String path, Mode mode) {

}

private static Mode parseMode(String mode) {
if (mode.equals("read")) {
return Mode.READ;
} else if (mode.equals("read_write")) {
return Mode.READ_WRITE;
} else {
throw new PolicyValidationException("invalid mode: " + mode + ", valid values: [read, read_write]");
}
}

@ExternalEntitlement(parameterNames = { "paths" }, esModulesOnly = false)
@SuppressWarnings("unchecked")
public static FilesEntitlement build(List<Object> paths) {
if (paths == null || paths.isEmpty()) {
throw new PolicyValidationException("must specify at least one path");
}
List<FileData> filesData = new ArrayList<>();
for (Object object : paths) {
Map<String, String> file = new HashMap<>((Map<String, String>) object);
String path = file.remove("path");
if (path == null) {
throw new PolicyValidationException("files entitlement must contain path for every listed file");
}
String mode = file.remove("mode");
if (mode == null) {
throw new PolicyValidationException("files entitlement must contain mode for every listed file");
}
if (file.isEmpty() == false) {
throw new PolicyValidationException("unknown key(s) " + file + " in a listed file for files entitlement");
}
filesData.add(new FileData(path, parseMode(mode)));
}
return new FilesEntitlement(filesData);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

package org.elasticsearch.entitlement.runtime.policy;

import org.elasticsearch.entitlement.runtime.policy.entitlements.FileEntitlement;
import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
import org.elasticsearch.test.ESTestCase;
import org.junit.BeforeClass;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

public void testEmpty() {
var tree = FileAccessTree.of(List.of());
var tree = FileAccessTree.of(FilesEntitlement.EMPTY);
assertThat(tree.canRead(path("path")), is(false));
assertThat(tree.canWrite(path("path")), is(false));
}

public void testRead() {
var tree = FileAccessTree.of(List.of(entitlement("foo", "read")));
var tree = FileAccessTree.of(entitlement("foo", "read"));
assertThat(tree.canRead(path("foo")), is(true));
assertThat(tree.canRead(path("foo/subdir")), is(true));
assertThat(tree.canRead(path("food")), is(false));
Expand All @@ -51,7 +54,7 @@ public void testRead() {
}

public void testWrite() {
var tree = FileAccessTree.of(List.of(entitlement("foo", "read_write")));
var tree = FileAccessTree.of(entitlement("foo", "read_write"));
assertThat(tree.canWrite(path("foo")), is(true));
assertThat(tree.canWrite(path("foo/subdir")), is(true));
assertThat(tree.canWrite(path("food")), is(false));
Expand All @@ -63,7 +66,7 @@ public void testWrite() {
}

public void testTwoPaths() {
var tree = FileAccessTree.of(List.of(entitlement("foo", "read"), entitlement("bar", "read")));
var tree = FileAccessTree.of(entitlement("foo", "read", "bar", "read"));
assertThat(tree.canRead(path("a")), is(false));
assertThat(tree.canRead(path("bar")), is(true));
assertThat(tree.canRead(path("bar/subdir")), is(true));
Expand All @@ -74,23 +77,23 @@ public void testTwoPaths() {
}

public void testReadWriteUnderRead() {
var tree = FileAccessTree.of(List.of(entitlement("foo", "read"), entitlement("foo/bar", "read_write")));
var tree = FileAccessTree.of(entitlement("foo", "read", "foo/bar", "read_write"));
assertThat(tree.canRead(path("foo")), is(true));
assertThat(tree.canWrite(path("foo")), is(false));
assertThat(tree.canRead(path("foo/bar")), is(true));
assertThat(tree.canWrite(path("foo/bar")), is(true));
}

public void testNormalizePath() {
var tree = FileAccessTree.of(List.of(entitlement("foo/../bar", "read")));
var tree = FileAccessTree.of(entitlement("foo/../bar", "read"));
assertThat(tree.canRead(path("foo/../bar")), is(true));
assertThat(tree.canRead(path("foo")), is(false));
assertThat(tree.canRead(path("")), is(false));
}

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

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

FileEntitlement entitlement(String path, String mode) {
Path p = path(path);
return FileEntitlement.create(p.toString(), mode);
FilesEntitlement entitlement(String... values) {
List<Object> filesData = new ArrayList<>();
for (int i = 0; i < values.length; i += 2) {
Map<String, String> fileData = new HashMap<>();
fileData.put("path", path(values[i]).toString());
fileData.put("mode", values[i + 1]);
filesData.add(fileData);
}
return FilesEntitlement.build(filesData);
}
}
Loading

0 comments on commit e95ec45

Please sign in to comment.