Skip to content

Commit 1f2db3f

Browse files
rjernstelasticsearchmachine
andauthored
Add exclusive access files for security module (elastic#123676) (elastic#124485)
* Add exclusive access files for security module (elastic#123676) This commit fills out missing entitlements for the security module. Specifically they are config files which require exclusive access. * Use a consistent ordering in policy manager exclulsive tests * use better assertion * [CI] Auto commit changes from spotless --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent 224eeee commit 1f2db3f

File tree

13 files changed

+111
-60
lines changed

13 files changed

+111
-60
lines changed

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@
2323
import java.nio.file.Paths;
2424
import java.util.ArrayList;
2525
import java.util.Arrays;
26+
import java.util.HashMap;
27+
import java.util.HashSet;
2628
import java.util.List;
29+
import java.util.Map;
2730
import java.util.Objects;
31+
import java.util.Set;
2832
import java.util.function.BiConsumer;
2933

3034
import static java.util.Comparator.comparing;
@@ -42,27 +46,47 @@ record ExclusiveFileEntitlement(String componentName, String moduleName, FilesEn
4246
/**
4347
* An intermediary structure to help globally validate exclusive paths, and then build exclusive paths for individual modules.
4448
*/
45-
record ExclusivePath(String componentName, String moduleName, String path) {
49+
record ExclusivePath(String componentName, Set<String> moduleNames, String path) {
4650

4751
@Override
4852
public String toString() {
49-
return "[[" + componentName + "] [" + moduleName + "] [" + path + "]]";
53+
return "[[" + componentName + "] " + moduleNames + " [" + path + "]]";
5054
}
5155
}
5256

5357
static List<ExclusivePath> buildExclusivePathList(List<ExclusiveFileEntitlement> exclusiveFileEntitlements, PathLookup pathLookup) {
54-
List<ExclusivePath> exclusivePaths = new ArrayList<>();
58+
Map<String, ExclusivePath> exclusivePaths = new HashMap<>();
5559
for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) {
5660
for (FilesEntitlement.FileData fd : efe.filesEntitlement().filesData()) {
5761
if (fd.exclusive()) {
5862
List<Path> paths = fd.resolvePaths(pathLookup).toList();
5963
for (Path path : paths) {
60-
exclusivePaths.add(new ExclusivePath(efe.componentName(), efe.moduleName(), normalizePath(path)));
64+
String normalizedPath = normalizePath(path);
65+
var exclusivePath = exclusivePaths.computeIfAbsent(
66+
normalizedPath,
67+
k -> new ExclusivePath(efe.componentName(), new HashSet<>(), normalizedPath)
68+
);
69+
if (exclusivePath.componentName().equals(efe.componentName()) == false) {
70+
throw new IllegalArgumentException(
71+
"Path ["
72+
+ normalizedPath
73+
+ "] is already exclusive to ["
74+
+ exclusivePath.componentName()
75+
+ "]"
76+
+ exclusivePath.moduleNames
77+
+ ", cannot add exclusive access for ["
78+
+ efe.componentName()
79+
+ "]["
80+
+ efe.moduleName
81+
+ "]"
82+
);
83+
}
84+
exclusivePath.moduleNames.add(efe.moduleName());
6185
}
6286
}
6387
}
6488
}
65-
return exclusivePaths.stream().sorted(comparing(ExclusivePath::path, PATH_ORDER)).distinct().toList();
89+
return exclusivePaths.values().stream().sorted(comparing(ExclusivePath::path, PATH_ORDER)).distinct().toList();
6690
}
6791

6892
static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) {
@@ -97,7 +121,7 @@ private FileAccessTree(
97121
) {
98122
List<String> updatedExclusivePaths = new ArrayList<>();
99123
for (ExclusivePath exclusivePath : exclusivePaths) {
100-
if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleName().equals(moduleName) == false) {
124+
if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleNames().contains(moduleName) == false) {
101125
updatedExclusivePaths.add(exclusivePath.path());
102126
}
103127
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ public PathSettingFileData withExclusive(boolean exclusive) {
164164
public Stream<Path> resolveRelativePaths(PathLookup pathLookup) {
165165
Stream<String> result = pathLookup.settingResolver()
166166
.apply(setting)
167-
.filter(s -> s.toLowerCase(Locale.ROOT).startsWith("https://") == false);
167+
.filter(s -> s.toLowerCase(Locale.ROOT).startsWith("https://") == false)
168+
.distinct();
168169
return result.map(Path::of);
169170
}
170171

libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.HashMap;
2727
import java.util.List;
2828
import java.util.Map;
29+
import java.util.Set;
2930

3031
import static org.elasticsearch.core.PathUtils.getDefaultFileSystem;
3132
import static org.elasticsearch.entitlement.runtime.policy.FileAccessTree.buildExclusivePathList;
@@ -386,7 +387,7 @@ public void testDuplicateExclusivePaths() {
386387
original.moduleName(),
387388
new FilesEntitlement(List.of(originalFileData.withPlatform(WINDOWS)))
388389
);
389-
var originalExclusivePath = new ExclusivePath("component1", "module1", normalizePath(path("/a/b")));
390+
var originalExclusivePath = new ExclusivePath("component1", Set.of("module1"), normalizePath(path("/a/b")));
390391

391392
// Some basic tests
392393

@@ -406,27 +407,14 @@ public void testDuplicateExclusivePaths() {
406407
var distinctEntitlements = List.of(original, differentComponent, differentModule, differentPath);
407408
var distinctPaths = List.of(
408409
originalExclusivePath,
409-
new ExclusivePath("component2", original.moduleName(), originalExclusivePath.path()),
410-
new ExclusivePath(original.componentName(), "module2", originalExclusivePath.path()),
411-
new ExclusivePath(original.componentName(), original.moduleName(), normalizePath(path("/c/d")))
410+
new ExclusivePath("component2", Set.of(original.moduleName()), originalExclusivePath.path()),
411+
new ExclusivePath(original.componentName(), Set.of("module2"), originalExclusivePath.path()),
412+
new ExclusivePath(original.componentName(), Set.of(original.moduleName()), normalizePath(path("/c/d")))
412413
);
413-
assertEquals(
414-
"Distinct elements should not be combined",
415-
distinctPaths,
416-
buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP)
417-
);
418-
419-
// Do merge things we should
420-
421-
List<ExclusiveFileEntitlement> interleavedEntitlements = new ArrayList<>();
422-
distinctEntitlements.forEach(e -> {
423-
interleavedEntitlements.add(e);
424-
interleavedEntitlements.add(original);
425-
});
426-
assertEquals(
427-
"Identical elements should be combined wherever they are in the list",
428-
distinctPaths,
429-
buildExclusivePathList(interleavedEntitlements, TEST_PATH_LOOKUP)
414+
var iae = expectThrows(IllegalArgumentException.class, () -> buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP));
415+
assertThat(
416+
iae.getMessage(),
417+
equalTo("Path [/a/b] is already exclusive to [component1][module1], cannot add exclusive access for [component2][module1]")
430418
);
431419

432420
var equivalentEntitlements = List.of(original, differentMode, differentPlatform);
@@ -486,7 +474,7 @@ static FilesEntitlement entitlement(Map<String, String> value) {
486474
static List<ExclusivePath> exclusivePaths(String componentName, String moduleName, String... paths) {
487475
List<ExclusivePath> exclusivePaths = new ArrayList<>();
488476
for (String path : paths) {
489-
exclusivePaths.add(new ExclusivePath(componentName, moduleName, normalizePath(path(path))));
477+
exclusivePaths.add(new ExclusivePath(componentName, Set.of(moduleName), normalizePath(path(path))));
490478
}
491479
return exclusivePaths;
492480
}

libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED;
3939
import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.SERVER_COMPONENT_NAME;
4040
import static org.hamcrest.Matchers.aMapWithSize;
41+
import static org.hamcrest.Matchers.allOf;
42+
import static org.hamcrest.Matchers.containsString;
4143
import static org.hamcrest.Matchers.is;
4244
import static org.hamcrest.Matchers.sameInstance;
4345

@@ -444,9 +446,9 @@ public void testDuplicateEntitlements() {
444446
}
445447

446448
public void testFilesEntitlementsWithExclusive() {
447-
var baseTestPath = Path.of("/tmp").toAbsolutePath();
448-
var testPath1 = Path.of("/tmp/test").toAbsolutePath();
449-
var testPath2 = Path.of("/tmp/test/foo").toAbsolutePath();
449+
var baseTestPath = Path.of("/base").toAbsolutePath();
450+
var testPath1 = Path.of("/base/test").toAbsolutePath();
451+
var testPath2 = Path.of("/base/test/foo").toAbsolutePath();
450452
var iae = expectThrows(
451453
IllegalArgumentException.class,
452454
() -> new PolicyManager(
@@ -458,7 +460,7 @@ public void testFilesEntitlementsWithExclusive() {
458460
"test",
459461
List.of(
460462
new Scope(
461-
"test",
463+
"test.module1",
462464
List.of(
463465
new FilesEntitlement(
464466
List.of(FilesEntitlement.FileData.ofPath(testPath1, FilesEntitlement.Mode.READ).withExclusive(true))
@@ -472,7 +474,7 @@ public void testFilesEntitlementsWithExclusive() {
472474
"test",
473475
List.of(
474476
new Scope(
475-
"test",
477+
"test.module2",
476478
List.of(
477479
new FilesEntitlement(
478480
List.of(FilesEntitlement.FileData.ofPath(testPath1, FilesEntitlement.Mode.READ).withExclusive(true))
@@ -490,8 +492,15 @@ public void testFilesEntitlementsWithExclusive() {
490492
Set.of()
491493
)
492494
);
493-
assertTrue(iae.getMessage().contains("duplicate/overlapping exclusive paths found in files entitlements:"));
494-
assertTrue(iae.getMessage().contains(Strings.format("[test] [%s]]", testPath1.toString())));
495+
assertThat(
496+
iae.getMessage(),
497+
allOf(
498+
containsString("Path [/base/test] is already exclusive"),
499+
containsString("[plugin1][test.module1]"),
500+
containsString("[plugin2][test.module2]"),
501+
containsString("cannot add exclusive access")
502+
)
503+
);
495504

496505
iae = expectThrows(
497506
IllegalArgumentException.class,

server/src/main/java/org/elasticsearch/index/analysis/Analysis.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
import java.nio.charset.StandardCharsets;
6767
import java.nio.file.Files;
6868
import java.nio.file.Path;
69-
import java.security.AccessControlException;
7069
import java.util.ArrayList;
7170
import java.util.Collection;
7271
import java.util.Collections;
@@ -261,7 +260,7 @@ public static List<String> getWordList(
261260
} catch (IOException ioe) {
262261
String message = Strings.format("IOException while reading %s: %s", settingPath, path);
263262
throw new IllegalArgumentException(message, ioe);
264-
} catch (AccessControlException ace) {
263+
} catch (SecurityException ace) {
265264
throw new IllegalArgumentException(Strings.format("Access denied trying to read file %s: %s", settingPath, path), ace);
266265
}
267266
}

server/src/main/java/org/elasticsearch/watcher/FileWatcher.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.nio.file.Files;
2121
import java.nio.file.Path;
2222
import java.nio.file.attribute.BasicFileAttributes;
23-
import java.security.AccessControlException;
2423
import java.util.Arrays;
2524
import java.util.stream.StreamSupport;
2625

@@ -256,7 +255,7 @@ private Observer createChild(Path file, boolean initial) throws IOException {
256255
FileObserver child = new FileObserver(file);
257256
child.init(initial);
258257
return child;
259-
} catch (AccessControlException e) {
258+
} catch (SecurityException e) {
260259
// don't have permissions, use a placeholder
261260
logger.debug(() -> Strings.format("Don't have permissions to watch path [%s]", file), e);
262261
return new DeniedObserver(file);

server/src/test/java/org/elasticsearch/index/shard/StoreRecoveryTests.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
import java.nio.file.Files;
4646
import java.nio.file.Path;
4747
import java.nio.file.attribute.BasicFileAttributes;
48-
import java.security.AccessControlException;
4948
import java.util.Arrays;
5049
import java.util.Map;
5150
import java.util.function.Predicate;
@@ -259,7 +258,7 @@ public boolean hardLinksSupported(Path path) throws IOException {
259258
BasicFileAttributes sourceAttr = Files.readAttributes(path.resolve("foo.bar"), BasicFileAttributes.class);
260259
// we won't get here - no permission ;)
261260
return destAttr.fileKey() != null && destAttr.fileKey().equals(sourceAttr.fileKey());
262-
} catch (AccessControlException ex) {
261+
} catch (SecurityException ex) {
263262
return true; // if we run into that situation we know it's supported.
264263
} catch (UnsupportedOperationException ex) {
265264
return false;

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PrivilegedFileWatcher.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.watcher.FileWatcher;
1111

1212
import java.io.IOException;
13+
import java.io.InputStream;
1314
import java.nio.file.DirectoryStream;
1415
import java.nio.file.Files;
1516
import java.nio.file.Path;
@@ -34,6 +35,15 @@ public PrivilegedFileWatcher(Path path) {
3435
super(path);
3536
}
3637

38+
public PrivilegedFileWatcher(Path path, boolean checkFileContents) {
39+
super(path, checkFileContents);
40+
}
41+
42+
@Override
43+
protected InputStream newInputStream(Path path) throws IOException {
44+
return Files.newInputStream(path);
45+
}
46+
3747
@Override
3848
protected boolean fileExists(Path path) {
3949
return doPrivileged((PrivilegedAction<Boolean>) () -> Files.exists(path));

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
2121
import org.elasticsearch.xpack.core.security.support.NoOpLogger;
2222
import org.elasticsearch.xpack.core.security.support.Validation;
23+
import org.elasticsearch.xpack.security.PrivilegedFileWatcher;
2324
import org.elasticsearch.xpack.security.support.SecurityFiles;
2425

2526
import java.io.IOException;
@@ -57,7 +58,7 @@ public class FileUserRolesStore {
5758
file = resolveFile(config.env());
5859
userRoles = parseFileLenient(file, logger);
5960
listeners = new CopyOnWriteArrayList<>(Collections.singletonList(listener));
60-
FileWatcher watcher = new FileWatcher(file.getParent());
61+
FileWatcher watcher = new PrivilegedFileWatcher(file.getParent());
6162
watcher.addListener(new FileListener());
6263
try {
6364
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStore.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource;
2525
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
2626
import org.elasticsearch.xpack.core.security.support.NoOpLogger;
27+
import org.elasticsearch.xpack.security.PrivilegedFileWatcher;
2728
import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId;
2829
import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry;
2930
import org.elasticsearch.xpack.security.support.FileLineParser;
@@ -59,7 +60,7 @@ public FileServiceAccountTokenStore(
5960
super(env.settings(), threadPool);
6061
this.clusterService = clusterService;
6162
file = resolveFile(env);
62-
FileWatcher watcher = new FileWatcher(file.getParent());
63+
FileWatcher watcher = new PrivilegedFileWatcher(file.getParent());
6364
watcher.addListener(new FileReloadListener(file, this::tryReload));
6465
try {
6566
resourceWatcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);

0 commit comments

Comments
 (0)