Skip to content

Commit c7bcdd3

Browse files
authored
[Entitlements] Cross-platform implementation of Path.isAbsolute() (#123282)
1 parent d007dae commit c7bcdd3

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

65112
private sealed interface RelativeFileData extends FileData {
@@ -190,17 +237,15 @@ public static FilesEntitlement build(List<Object> paths) {
190237
throw new PolicyValidationException("files entitlement with a 'relative_path' must specify 'relative_to'");
191238
}
192239

193-
Path relativePath = Path.of(relativePathAsString);
194-
if (relativePath.isAbsolute()) {
240+
if (FileData.isAbsolutePath(relativePathAsString)) {
195241
throw new PolicyValidationException("'relative_path' [" + relativePathAsString + "] must be relative");
196242
}
197-
filesData.add(FileData.ofRelativePath(relativePath, baseDir, mode));
243+
filesData.add(FileData.ofRelativePath(Path.of(relativePathAsString), baseDir, mode));
198244
} else if (pathAsString != null) {
199-
Path path = Path.of(pathAsString);
200-
if (path.isAbsolute() == false) {
245+
if (FileData.isAbsolutePath(pathAsString) == false) {
201246
throw new PolicyValidationException("'path' [" + pathAsString + "] must be absolute");
202247
}
203-
filesData.add(FileData.ofPath(path, mode));
248+
filesData.add(FileData.ofPath(Path.of(pathAsString), mode));
204249
} else if (pathSetting != null) {
205250
filesData.add(FileData.ofPathSetting(pathSetting, mode));
206251
} 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
@@ -60,12 +60,36 @@ public void testInvalidRelativeDirectory() {
6060
assertThat(ex.getMessage(), is("invalid relative directory: bar, valid values: [config, data, home]"));
6161
}
6262

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

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

0 commit comments

Comments
 (0)