Skip to content

Commit

Permalink
[Entitlements] Add ability to set path relative to a special director…
Browse files Browse the repository at this point in the history
…y for Files policies (elastic#122370)
  • Loading branch information
ldematte authored Feb 14, 2025
1 parent eff3060 commit 9141335
Show file tree
Hide file tree
Showing 11 changed files with 448 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.elasticsearch.entitlement.instrumentation.MethodKey;
import org.elasticsearch.entitlement.instrumentation.Transformer;
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
import org.elasticsearch.entitlement.runtime.policy.Scope;
Expand Down Expand Up @@ -48,7 +49,6 @@
import java.nio.file.attribute.FileAttribute;
import java.nio.file.spi.FileSystemProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -126,9 +126,9 @@ private static Class<?>[] findClassesToRetransform(Class<?>[] loadedClasses, Set
}

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

// TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it
var serverPolicy = new Policy(
Expand All @@ -147,7 +147,7 @@ private static PolicyManager createPolicyManager() {
new LoadNativeLibrariesEntitlement(),
new ManageThreadsEntitlement(),
new FilesEntitlement(
List.of(new FilesEntitlement.FileData(EntitlementBootstrap.bootstrapArgs().tempDir().toString(), READ_WRITE))
List.of(FilesEntitlement.FileData.ofPath(EntitlementBootstrap.bootstrapArgs().tempDir(), READ_WRITE))
)
)
),
Expand All @@ -159,7 +159,7 @@ private static PolicyManager createPolicyManager() {
"org.elasticsearch.nativeaccess",
List.of(
new LoadNativeLibrariesEntitlement(),
new FilesEntitlement(Arrays.stream(dataDirs).map(d -> new FileData(d.toString(), READ_WRITE)).toList())
new FilesEntitlement(List.of(FileData.ofRelativePath(Path.of(""), FilesEntitlement.BaseDir.DATA, READ_WRITE)))
)
)
)
Expand All @@ -175,7 +175,7 @@ private static PolicyManager createPolicyManager() {
resolver,
AGENTS_PACKAGE_NAME,
ENTITLEMENTS_MODULE,
tempDir
pathLookup
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,30 @@
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;

public final class FileAccessTree {

private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator();

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

private FileAccessTree(FilesEntitlement filesEntitlement, Path tempDir) {
private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup) {
List<String> readPaths = new ArrayList<>();
List<String> writePaths = new ArrayList<>();
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);
var paths = fileData.resolvePaths(pathLookup);
paths.forEach(path -> {
var normalized = normalizePath(path);
if (mode == FilesEntitlement.Mode.READ_WRITE) {
writePaths.add(normalized);
}
readPaths.add(normalized);
});
}

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

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

public static FileAccessTree of(FilesEntitlement filesEntitlement, Path tempDir) {
return new FileAccessTree(filesEntitlement, tempDir);
public static FileAccessTree of(FilesEntitlement filesEntitlement, PathLookup pathLookup) {
return new FileAccessTree(filesEntitlement, pathLookup);
}

boolean canRead(Path path) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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;

import java.nio.file.Path;

public record PathLookup(Path configDir, Path[] dataDirs, Path tempDir) {}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ ModuleEntitlements policyEntitlements(String componentName, List<Entitlement> en
return new ModuleEntitlements(
componentName,
entitlements.stream().collect(groupingBy(Entitlement::getClass)),
FileAccessTree.of(filesEntitlement, tempDir)
FileAccessTree.of(filesEntitlement, pathLookup)
);
}

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

public static final String ALL_UNNAMED = "ALL-UNNAMED";
Expand Down Expand Up @@ -146,7 +146,7 @@ public PolicyManager(
Function<Class<?>, String> pluginResolver,
String apmAgentPackageName,
Module entitlementsModule,
Path tempDir
PathLookup pathLookup
) {
this.serverEntitlements = buildScopeEntitlementsMap(requireNonNull(serverPolicy));
this.apmAgentEntitlements = apmAgentEntitlements;
Expand All @@ -156,9 +156,8 @@ public PolicyManager(
this.pluginResolver = pluginResolver;
this.apmAgentPackageName = apmAgentPackageName;
this.entitlementsModule = entitlementsModule;
this.defaultFileAccess = FileAccessTree.of(FilesEntitlement.EMPTY, tempDir);

this.tempDir = tempDir;
this.pathLookup = requireNonNull(pathLookup);
this.defaultFileAccess = FileAccessTree.of(FilesEntitlement.EMPTY, pathLookup);

for (var e : serverEntitlements.entrySet()) {
validateEntitlementsPerModule(SERVER_COMPONENT_NAME, e.getKey(), e.getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
package org.elasticsearch.entitlement.runtime.policy.entitlements;

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

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;
import java.util.Objects;
import java.util.stream.Stream;

/**
* Describes a file entitlement with a path and mode.
Expand All @@ -29,8 +34,104 @@ public enum Mode {
READ_WRITE
}

public record FileData(String path, Mode mode) {
public enum BaseDir {
CONFIG,
DATA
}

public sealed interface FileData {

final class AbsolutePathFileData implements FileData {
private final Path path;
private final Mode mode;

private AbsolutePathFileData(Path path, Mode mode) {
this.path = path;
this.mode = mode;
}

@Override
public Stream<Path> resolvePaths(PathLookup pathLookup) {
return Stream.of(path);
}

@Override
public Mode mode() {
return mode;
}

@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (AbsolutePathFileData) obj;
return Objects.equals(this.path, that.path) && Objects.equals(this.mode, that.mode);
}

@Override
public int hashCode() {
return Objects.hash(path, mode);
}
}

final class RelativePathFileData implements FileData {
private final Path relativePath;
private final BaseDir baseDir;
private final Mode mode;

private RelativePathFileData(Path relativePath, BaseDir baseDir, Mode mode) {
this.relativePath = relativePath;
this.baseDir = baseDir;
this.mode = mode;
}

@Override
public Stream<Path> resolvePaths(PathLookup pathLookup) {
Objects.requireNonNull(pathLookup);
switch (baseDir) {
case CONFIG:
return Stream.of(pathLookup.configDir().resolve(relativePath));
case DATA:
return Arrays.stream(pathLookup.dataDirs()).map(d -> d.resolve(relativePath));
default:
throw new IllegalArgumentException();
}
}

@Override
public Mode mode() {
return mode;
}

@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (RelativePathFileData) obj;
return Objects.equals(this.mode, that.mode)
&& Objects.equals(this.relativePath, that.relativePath)
&& Objects.equals(this.baseDir, that.baseDir);
}

@Override
public int hashCode() {
return Objects.hash(relativePath, baseDir, mode);
}
}

static FileData ofPath(Path path, Mode mode) {
assert path.isAbsolute();
return new AbsolutePathFileData(path, mode);
}

static FileData ofRelativePath(Path relativePath, BaseDir baseDir, Mode mode) {
assert relativePath.isAbsolute() == false;
return new RelativePathFileData(relativePath, baseDir, mode);
}

Stream<Path> resolvePaths(PathLookup pathLookup);

Mode mode();
}

private static Mode parseMode(String mode) {
Expand All @@ -43,6 +144,15 @@ private static Mode parseMode(String mode) {
}
}

private static BaseDir parseBaseDir(String baseDir) {
if (baseDir.equals("config")) {
return BaseDir.CONFIG;
} else if (baseDir.equals("data")) {
return BaseDir.DATA;
}
throw new PolicyValidationException("invalid relative directory: " + baseDir + ", valid values: [config, data]");
}

@ExternalEntitlement(parameterNames = { "paths" }, esModulesOnly = false)
@SuppressWarnings("unchecked")
public static FilesEntitlement build(List<Object> paths) {
Expand All @@ -52,18 +162,41 @@ public static FilesEntitlement build(List<Object> paths) {
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 pathAsString = file.remove("path");
String relativePathAsString = file.remove("relative_path");
String relativeTo = file.remove("relative_to");
String mode = file.remove("mode");

if (file.isEmpty() == false) {
throw new PolicyValidationException("unknown key(s) [" + file + "] in a listed file for files entitlement");
}
if (mode == null) {
throw new PolicyValidationException("files entitlement must contain mode for every listed file");
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");
if (pathAsString != null && relativePathAsString != null) {
throw new PolicyValidationException("a files entitlement entry cannot contain both 'path' and 'relative_path'");
}

if (relativePathAsString != null) {
if (relativeTo == 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)));
} 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)));
} else {
throw new PolicyValidationException("files entitlement must contain either 'path' or 'relative_path' for every entry");
}
filesData.add(new FileData(path, parseMode(mode)));
}
return new FilesEntitlement(filesData);
}
Expand Down
Loading

0 comments on commit 9141335

Please sign in to comment.