Browse Source

[Entitlements] Cross-platform implementation of Path.isAbsolute() (#123282)

Lorenzo Dematté 7 months ago
parent
commit
c7bcdd37f4

+ 51 - 6
libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java

@@ -21,6 +21,8 @@ import java.util.Map;
 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 @@ public record FilesEntitlement(List<FileData> filesData) implements Entitlement
         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 record FilesEntitlement(List<FileData> filesData) implements Entitlement
                     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) {

+ 41 - 0
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));
+    }
+}

+ 25 - 1
libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java

@@ -60,12 +60,36 @@ public class FilesEntitlementTests extends ESTestCase {
         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();