diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java index 21b54ba51ca87..7f356daea8324 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java @@ -21,6 +21,8 @@ import java.util.Objects; import java.util.stream.Stream; +import static java.lang.Character.isLetter; + /** * Describes a file entitlement with a path and mode. */ @@ -60,6 +62,51 @@ static FileData ofPathSetting(String setting, Mode mode) { static FileData ofRelativePathSetting(String setting, BaseDir baseDir, Mode mode) { return new RelativePathSettingFileData(setting, baseDir, mode); } + + /** + * Tests if a path is absolute or relative, taking into consideration both Unix and Windows conventions. + * Note that this leads to a conflict, resolved in favor of Unix rules: `/foo` can be either a Unix absolute path, or a Windows + * relative path with "wrong" directory separator (using non-canonical slash in Windows). + */ + static boolean isAbsolutePath(String path) { + if (path.isEmpty()) { + return false; + } + if (path.charAt(0) == '/') { + // Unix/BSD absolute + return true; + } + + return isWindowsAbsolutePath(path); + } + + private static boolean isSlash(char c) { + return (c == '\\') || (c == '/'); + } + + private static boolean isWindowsAbsolutePath(String input) { + // if a prefix is present, we expected (long) UNC or (long) absolute + if (input.startsWith("\\\\?\\")) { + return true; + } + + if (input.length() > 1) { + char c0 = input.charAt(0); + char c1 = input.charAt(1); + char c = 0; + int next = 2; + if (isSlash(c0) && isSlash(c1)) { + // Two slashes or more: UNC + return true; + } + if (isLetter(c0) && c1 == ':') { + // A drive: absolute + return true; + } + } + // Otherwise relative + return false; + } } private sealed interface RelativeFileData extends FileData { @@ -190,17 +237,15 @@ public static FilesEntitlement build(List paths) { throw new PolicyValidationException("files entitlement with a 'relative_path' must specify 'relative_to'"); } - Path relativePath = Path.of(relativePathAsString); - if (relativePath.isAbsolute()) { + if (FileData.isAbsolutePath(relativePathAsString)) { throw new PolicyValidationException("'relative_path' [" + relativePathAsString + "] must be relative"); } - filesData.add(FileData.ofRelativePath(relativePath, baseDir, mode)); + filesData.add(FileData.ofRelativePath(Path.of(relativePathAsString), baseDir, mode)); } else if (pathAsString != null) { - Path path = Path.of(pathAsString); - if (path.isAbsolute() == false) { + if (FileData.isAbsolutePath(pathAsString) == false) { throw new PolicyValidationException("'path' [" + pathAsString + "] must be absolute"); } - filesData.add(FileData.ofPath(path, mode)); + filesData.add(FileData.ofPath(Path.of(pathAsString), mode)); } else if (pathSetting != null) { filesData.add(FileData.ofPathSetting(pathSetting, mode)); } else if (relativePathSetting != null) { diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FileDataTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FileDataTests.java new file mode 100644 index 0000000000000..12ac8e2d63053 --- /dev/null +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FileDataTests.java @@ -0,0 +1,41 @@ +/* + * 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.test.ESTestCase; + +import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.FileData.isAbsolutePath; +import static org.hamcrest.Matchers.is; + +public class FileDataTests extends ESTestCase { + + public void testPathIsAbsolute() { + var windowsNamedPipe = "\\\\.\\pipe"; + var windowsDosAbsolutePath = "C:\\temp"; + var unixAbsolutePath = "/tmp/foo"; + var unixStyleUncPath = "//C/temp"; + var uncPath = "\\\\C\\temp"; + var longPath = "\\\\?\\C:\\temp"; + + var relativePath = "foo"; + var headingSlashRelativePath = "\\foo"; + + assertThat(isAbsolutePath(windowsNamedPipe), is(true)); + assertThat(isAbsolutePath(windowsDosAbsolutePath), is(true)); + assertThat(isAbsolutePath(unixAbsolutePath), is(true)); + assertThat(isAbsolutePath(unixStyleUncPath), is(true)); + assertThat(isAbsolutePath(uncPath), is(true)); + assertThat(isAbsolutePath(longPath), is(true)); + + assertThat(isAbsolutePath(relativePath), is(false)); + assertThat(isAbsolutePath(headingSlashRelativePath), is(false)); + assertThat(isAbsolutePath(""), is(false)); + } +} diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java index b871e8ccf3e6f..ceabf686d28f8 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java @@ -60,12 +60,36 @@ public void testInvalidRelativeDirectory() { assertThat(ex.getMessage(), is("invalid relative directory: bar, valid values: [config, data, home]")); } - public void testFileDataRelativeWithEmptyDirectory() { + public void testFileDataRelativeWithAbsoluteDirectoryFails() { var fileData = FileData.ofRelativePath(Path.of(""), FilesEntitlement.BaseDir.DATA, READ_WRITE); var dataDirs = fileData.resolvePaths(TEST_PATH_LOOKUP); assertThat(dataDirs.toList(), contains(Path.of("/data1/"), Path.of("/data2"))); } + public void testFileDataAbsoluteWithRelativeDirectoryFails() { + var ex = expectThrows( + PolicyValidationException.class, + () -> FilesEntitlement.build(List.of((Map.of("path", "foo", "mode", "read")))) + ); + + assertThat(ex.getMessage(), is("'path' [foo] must be absolute")); + } + + public void testFileDataRelativeWithEmptyDirectory() { + var ex = expectThrows( + PolicyValidationException.class, + () -> FilesEntitlement.build(List.of((Map.of("relative_path", "/foo", "mode", "read", "relative_to", "config")))) + ); + + var ex2 = expectThrows( + PolicyValidationException.class, + () -> FilesEntitlement.build(List.of((Map.of("relative_path", "C:\\foo", "mode", "read", "relative_to", "config")))) + ); + + assertThat(ex.getMessage(), is("'relative_path' [/foo] must be relative")); + assertThat(ex2.getMessage(), is("'relative_path' [C:\\foo] must be relative")); + } + public void testPathSettingResolve() { var entitlement = FilesEntitlement.build(List.of(Map.of("path_setting", "foo.bar", "mode", "read"))); var filesData = entitlement.filesData();