Skip to content

Commit 411709f

Browse files
rjernstldematte
andauthored
[Entitlements] Cross-platform implementation of Path.isAbsolute() (#123282) (#123317)
Co-authored-by: Lorenzo Dematté <[email protected]>
1 parent dff78ec commit 411709f

File tree

3 files changed

+117
-7
lines changed

3 files changed

+117
-7
lines changed

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlement.java

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.Objects;
2222
import java.util.stream.Stream;
2323

24+
import static java.lang.Character.isLetter;
25+
2426
/**
2527
* Describes a file entitlement with a path and mode.
2628
*/
@@ -61,6 +63,51 @@ static FileData ofPathSetting(String setting, Mode mode) {
6163
static FileData ofRelativePathSetting(String setting, BaseDir baseDir, Mode mode) {
6264
return new RelativePathSettingFileData(setting, baseDir, mode);
6365
}
66+
67+
/**
68+
* Tests if a path is absolute or relative, taking into consideration both Unix and Windows conventions.
69+
* Note that this leads to a conflict, resolved in favor of Unix rules: `/foo` can be either a Unix absolute path, or a Windows
70+
* relative path with "wrong" directory separator (using non-canonical slash in Windows).
71+
*/
72+
static boolean isAbsolutePath(String path) {
73+
if (path.isEmpty()) {
74+
return false;
75+
}
76+
if (path.charAt(0) == '/') {
77+
// Unix/BSD absolute
78+
return true;
79+
}
80+
81+
return isWindowsAbsolutePath(path);
82+
}
83+
84+
private static boolean isSlash(char c) {
85+
return (c == '\\') || (c == '/');
86+
}
87+
88+
private static boolean isWindowsAbsolutePath(String input) {
89+
// if a prefix is present, we expected (long) UNC or (long) absolute
90+
if (input.startsWith("\\\\?\\")) {
91+
return true;
92+
}
93+
94+
if (input.length() > 1) {
95+
char c0 = input.charAt(0);
96+
char c1 = input.charAt(1);
97+
char c = 0;
98+
int next = 2;
99+
if (isSlash(c0) && isSlash(c1)) {
100+
// Two slashes or more: UNC
101+
return true;
102+
}
103+
if (isLetter(c0) && c1 == ':') {
104+
// A drive: absolute
105+
return true;
106+
}
107+
}
108+
// Otherwise relative
109+
return false;
110+
}
64111
}
65112

66113
private sealed interface RelativeFileData extends FileData {
@@ -198,17 +245,15 @@ public static FilesEntitlement build(List<Object> paths) {
198245
throw new PolicyValidationException("files entitlement with a 'relative_path' must specify 'relative_to'");
199246
}
200247

201-
Path relativePath = Path.of(relativePathAsString);
202-
if (relativePath.isAbsolute()) {
248+
if (FileData.isAbsolutePath(relativePathAsString)) {
203249
throw new PolicyValidationException("'relative_path' [" + relativePathAsString + "] must be relative");
204250
}
205-
filesData.add(FileData.ofRelativePath(relativePath, baseDir, mode));
251+
filesData.add(FileData.ofRelativePath(Path.of(relativePathAsString), baseDir, mode));
206252
} else if (pathAsString != null) {
207-
Path path = Path.of(pathAsString);
208-
if (path.isAbsolute() == false) {
253+
if (FileData.isAbsolutePath(pathAsString) == false) {
209254
throw new PolicyValidationException("'path' [" + pathAsString + "] must be absolute");
210255
}
211-
filesData.add(FileData.ofPath(path, mode));
256+
filesData.add(FileData.ofPath(Path.of(pathAsString), mode));
212257
} else if (pathSetting != null) {
213258
filesData.add(FileData.ofPathSetting(pathSetting, mode));
214259
} else if (relativePathSetting != null) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.runtime.policy.entitlements;
11+
12+
import org.elasticsearch.test.ESTestCase;
13+
14+
import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.FileData.isAbsolutePath;
15+
import static org.hamcrest.Matchers.is;
16+
17+
public class FileDataTests extends ESTestCase {
18+
19+
public void testPathIsAbsolute() {
20+
var windowsNamedPipe = "\\\\.\\pipe";
21+
var windowsDosAbsolutePath = "C:\\temp";
22+
var unixAbsolutePath = "/tmp/foo";
23+
var unixStyleUncPath = "//C/temp";
24+
var uncPath = "\\\\C\\temp";
25+
var longPath = "\\\\?\\C:\\temp";
26+
27+
var relativePath = "foo";
28+
var headingSlashRelativePath = "\\foo";
29+
30+
assertThat(isAbsolutePath(windowsNamedPipe), is(true));
31+
assertThat(isAbsolutePath(windowsDosAbsolutePath), is(true));
32+
assertThat(isAbsolutePath(unixAbsolutePath), is(true));
33+
assertThat(isAbsolutePath(unixStyleUncPath), is(true));
34+
assertThat(isAbsolutePath(uncPath), is(true));
35+
assertThat(isAbsolutePath(longPath), is(true));
36+
37+
assertThat(isAbsolutePath(relativePath), is(false));
38+
assertThat(isAbsolutePath(headingSlashRelativePath), is(false));
39+
assertThat(isAbsolutePath(""), is(false));
40+
}
41+
}

libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/entitlements/FilesEntitlementTests.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,36 @@ public void testInvalidRelativeDirectory() {
6161
assertThat(ex.getMessage(), is("invalid relative directory: bar, valid values: [config, data, home]"));
6262
}
6363

64-
public void testFileDataRelativeWithEmptyDirectory() {
64+
public void testFileDataRelativeWithAbsoluteDirectoryFails() {
6565
var fileData = FileData.ofRelativePath(Path.of(""), FilesEntitlement.BaseDir.DATA, READ_WRITE);
6666
var dataDirs = fileData.resolvePaths(TEST_PATH_LOOKUP);
6767
assertThat(dataDirs.toList(), contains(Path.of("/data1/"), Path.of("/data2")));
6868
}
6969

70+
public void testFileDataAbsoluteWithRelativeDirectoryFails() {
71+
var ex = expectThrows(
72+
PolicyValidationException.class,
73+
() -> FilesEntitlement.build(List.of((Map.of("path", "foo", "mode", "read"))))
74+
);
75+
76+
assertThat(ex.getMessage(), is("'path' [foo] must be absolute"));
77+
}
78+
79+
public void testFileDataRelativeWithEmptyDirectory() {
80+
var ex = expectThrows(
81+
PolicyValidationException.class,
82+
() -> FilesEntitlement.build(List.of((Map.of("relative_path", "/foo", "mode", "read", "relative_to", "config"))))
83+
);
84+
85+
var ex2 = expectThrows(
86+
PolicyValidationException.class,
87+
() -> FilesEntitlement.build(List.of((Map.of("relative_path", "C:\\foo", "mode", "read", "relative_to", "config"))))
88+
);
89+
90+
assertThat(ex.getMessage(), is("'relative_path' [/foo] must be relative"));
91+
assertThat(ex2.getMessage(), is("'relative_path' [C:\\foo] must be relative"));
92+
}
93+
7094
public void testPathSettingResolve() {
7195
var entitlement = FilesEntitlement.build(List.of(Map.of("path_setting", "foo.bar", "mode", "read")));
7296
var filesData = entitlement.filesData();

0 commit comments

Comments
 (0)