diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java index b951a4d7b42f4..0e35ef0f0c72e 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java @@ -23,8 +23,12 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.BiConsumer; import static java.util.Comparator.comparing; @@ -42,27 +46,47 @@ record ExclusiveFileEntitlement(String componentName, String moduleName, FilesEn /** * An intermediary structure to help globally validate exclusive paths, and then build exclusive paths for individual modules. */ - record ExclusivePath(String componentName, String moduleName, String path) { + record ExclusivePath(String componentName, Set moduleNames, String path) { @Override public String toString() { - return "[[" + componentName + "] [" + moduleName + "] [" + path + "]]"; + return "[[" + componentName + "] " + moduleNames + " [" + path + "]]"; } } static List buildExclusivePathList(List exclusiveFileEntitlements, PathLookup pathLookup) { - List exclusivePaths = new ArrayList<>(); + Map exclusivePaths = new HashMap<>(); for (ExclusiveFileEntitlement efe : exclusiveFileEntitlements) { for (FilesEntitlement.FileData fd : efe.filesEntitlement().filesData()) { if (fd.exclusive()) { List paths = fd.resolvePaths(pathLookup).toList(); for (Path path : paths) { - exclusivePaths.add(new ExclusivePath(efe.componentName(), efe.moduleName(), normalizePath(path))); + String normalizedPath = normalizePath(path); + var exclusivePath = exclusivePaths.computeIfAbsent( + normalizedPath, + k -> new ExclusivePath(efe.componentName(), new HashSet<>(), normalizedPath) + ); + if (exclusivePath.componentName().equals(efe.componentName()) == false) { + throw new IllegalArgumentException( + "Path [" + + normalizedPath + + "] is already exclusive to [" + + exclusivePath.componentName() + + "]" + + exclusivePath.moduleNames + + ", cannot add exclusive access for [" + + efe.componentName() + + "][" + + efe.moduleName + + "]" + ); + } + exclusivePath.moduleNames.add(efe.moduleName()); } } } } - return exclusivePaths.stream().sorted(comparing(ExclusivePath::path, PATH_ORDER)).distinct().toList(); + return exclusivePaths.values().stream().sorted(comparing(ExclusivePath::path, PATH_ORDER)).distinct().toList(); } static void validateExclusivePaths(List exclusivePaths) { @@ -97,7 +121,7 @@ private FileAccessTree( ) { List updatedExclusivePaths = new ArrayList<>(); for (ExclusivePath exclusivePath : exclusivePaths) { - if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleName().equals(moduleName) == false) { + if (exclusivePath.componentName().equals(componentName) == false || exclusivePath.moduleNames().contains(moduleName) == false) { updatedExclusivePaths.add(exclusivePath.path()); } } 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 adf234f160717..c771da019d2b6 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 @@ -164,7 +164,8 @@ public PathSettingFileData withExclusive(boolean exclusive) { public Stream resolveRelativePaths(PathLookup pathLookup) { Stream result = pathLookup.settingResolver() .apply(setting) - .filter(s -> s.toLowerCase(Locale.ROOT).startsWith("https://") == false); + .filter(s -> s.toLowerCase(Locale.ROOT).startsWith("https://") == false) + .distinct(); return result.map(Path::of); } diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java index 938a946a95c61..b6f3a18a6a50b 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTreeTests.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static org.elasticsearch.core.PathUtils.getDefaultFileSystem; import static org.elasticsearch.entitlement.runtime.policy.FileAccessTree.buildExclusivePathList; @@ -386,7 +387,7 @@ public void testDuplicateExclusivePaths() { original.moduleName(), new FilesEntitlement(List.of(originalFileData.withPlatform(WINDOWS))) ); - var originalExclusivePath = new ExclusivePath("component1", "module1", normalizePath(path("/a/b"))); + var originalExclusivePath = new ExclusivePath("component1", Set.of("module1"), normalizePath(path("/a/b"))); // Some basic tests @@ -406,27 +407,14 @@ public void testDuplicateExclusivePaths() { var distinctEntitlements = List.of(original, differentComponent, differentModule, differentPath); var distinctPaths = List.of( originalExclusivePath, - new ExclusivePath("component2", original.moduleName(), originalExclusivePath.path()), - new ExclusivePath(original.componentName(), "module2", originalExclusivePath.path()), - new ExclusivePath(original.componentName(), original.moduleName(), normalizePath(path("/c/d"))) + new ExclusivePath("component2", Set.of(original.moduleName()), originalExclusivePath.path()), + new ExclusivePath(original.componentName(), Set.of("module2"), originalExclusivePath.path()), + new ExclusivePath(original.componentName(), Set.of(original.moduleName()), normalizePath(path("/c/d"))) ); - assertEquals( - "Distinct elements should not be combined", - distinctPaths, - buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP) - ); - - // Do merge things we should - - List interleavedEntitlements = new ArrayList<>(); - distinctEntitlements.forEach(e -> { - interleavedEntitlements.add(e); - interleavedEntitlements.add(original); - }); - assertEquals( - "Identical elements should be combined wherever they are in the list", - distinctPaths, - buildExclusivePathList(interleavedEntitlements, TEST_PATH_LOOKUP) + var iae = expectThrows(IllegalArgumentException.class, () -> buildExclusivePathList(distinctEntitlements, TEST_PATH_LOOKUP)); + assertThat( + iae.getMessage(), + equalTo("Path [/a/b] is already exclusive to [component1][module1], cannot add exclusive access for [component2][module1]") ); var equivalentEntitlements = List.of(original, differentMode, differentPlatform); @@ -486,7 +474,7 @@ static FilesEntitlement entitlement(Map value) { static List exclusivePaths(String componentName, String moduleName, String... paths) { List exclusivePaths = new ArrayList<>(); for (String path : paths) { - exclusivePaths.add(new ExclusivePath(componentName, moduleName, normalizePath(path(path)))); + exclusivePaths.add(new ExclusivePath(componentName, Set.of(moduleName), normalizePath(path(path)))); } return exclusivePaths; } diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java index a0868563b87e1..90230c9e7e279 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java @@ -38,6 +38,8 @@ import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.ALL_UNNAMED; import static org.elasticsearch.entitlement.runtime.policy.PolicyManager.SERVER_COMPONENT_NAME; import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; @@ -444,9 +446,9 @@ public void testDuplicateEntitlements() { } public void testFilesEntitlementsWithExclusive() { - var baseTestPath = Path.of("/tmp").toAbsolutePath(); - var testPath1 = Path.of("/tmp/test").toAbsolutePath(); - var testPath2 = Path.of("/tmp/test/foo").toAbsolutePath(); + var baseTestPath = Path.of("/base").toAbsolutePath(); + var testPath1 = Path.of("/base/test").toAbsolutePath(); + var testPath2 = Path.of("/base/test/foo").toAbsolutePath(); var iae = expectThrows( IllegalArgumentException.class, () -> new PolicyManager( @@ -458,7 +460,7 @@ public void testFilesEntitlementsWithExclusive() { "test", List.of( new Scope( - "test", + "test.module1", List.of( new FilesEntitlement( List.of(FilesEntitlement.FileData.ofPath(testPath1, FilesEntitlement.Mode.READ).withExclusive(true)) @@ -472,7 +474,7 @@ public void testFilesEntitlementsWithExclusive() { "test", List.of( new Scope( - "test", + "test.module2", List.of( new FilesEntitlement( List.of(FilesEntitlement.FileData.ofPath(testPath1, FilesEntitlement.Mode.READ).withExclusive(true)) @@ -490,8 +492,15 @@ public void testFilesEntitlementsWithExclusive() { Set.of() ) ); - assertTrue(iae.getMessage().contains("duplicate/overlapping exclusive paths found in files entitlements:")); - assertTrue(iae.getMessage().contains(Strings.format("[test] [%s]]", testPath1.toString()))); + assertThat( + iae.getMessage(), + allOf( + containsString("Path [/base/test] is already exclusive"), + containsString("[plugin1][test.module1]"), + containsString("[plugin2][test.module2]"), + containsString("cannot add exclusive access") + ) + ); iae = expectThrows( IllegalArgumentException.class, diff --git a/server/src/main/java/org/elasticsearch/index/analysis/Analysis.java b/server/src/main/java/org/elasticsearch/index/analysis/Analysis.java index a93a480463564..992e6c6d57c1b 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/Analysis.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/Analysis.java @@ -64,7 +64,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.security.AccessControlException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -247,7 +246,7 @@ public static List getWordList( } catch (IOException ioe) { String message = Strings.format("IOException while reading %s: %s", settingPath, path); throw new IllegalArgumentException(message, ioe); - } catch (AccessControlException ace) { + } catch (SecurityException ace) { throw new IllegalArgumentException(Strings.format("Access denied trying to read file %s: %s", settingPath, path), ace); } } diff --git a/server/src/main/java/org/elasticsearch/watcher/FileWatcher.java b/server/src/main/java/org/elasticsearch/watcher/FileWatcher.java index b9ea8504f72ab..4620e65534d3e 100644 --- a/server/src/main/java/org/elasticsearch/watcher/FileWatcher.java +++ b/server/src/main/java/org/elasticsearch/watcher/FileWatcher.java @@ -20,7 +20,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; -import java.security.AccessControlException; import java.util.Arrays; import java.util.stream.StreamSupport; @@ -256,7 +255,7 @@ private Observer createChild(Path file, boolean initial) throws IOException { FileObserver child = new FileObserver(file); child.init(initial); return child; - } catch (AccessControlException e) { + } catch (SecurityException e) { // don't have permissions, use a placeholder logger.debug(() -> Strings.format("Don't have permissions to watch path [%s]", file), e); return new DeniedObserver(file); diff --git a/server/src/test/java/org/elasticsearch/index/shard/StoreRecoveryTests.java b/server/src/test/java/org/elasticsearch/index/shard/StoreRecoveryTests.java index 92afe312e5d13..bc3b44b908246 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/StoreRecoveryTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/StoreRecoveryTests.java @@ -45,7 +45,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; -import java.security.AccessControlException; import java.util.Arrays; import java.util.Map; import java.util.function.Predicate; @@ -259,7 +258,7 @@ public boolean hardLinksSupported(Path path) throws IOException { BasicFileAttributes sourceAttr = Files.readAttributes(path.resolve("foo.bar"), BasicFileAttributes.class); // we won't get here - no permission ;) return destAttr.fileKey() != null && destAttr.fileKey().equals(sourceAttr.fileKey()); - } catch (AccessControlException ex) { + } catch (SecurityException ex) { return true; // if we run into that situation we know it's supported. } catch (UnsupportedOperationException ex) { return false; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PrivilegedFileWatcher.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PrivilegedFileWatcher.java index 583bb93c2a52b..e6bdfd9cde14b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PrivilegedFileWatcher.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PrivilegedFileWatcher.java @@ -10,6 +10,7 @@ import org.elasticsearch.watcher.FileWatcher; import java.io.IOException; +import java.io.InputStream; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; @@ -34,6 +35,15 @@ public PrivilegedFileWatcher(Path path) { super(path); } + public PrivilegedFileWatcher(Path path, boolean checkFileContents) { + super(path, checkFileContents); + } + + @Override + protected InputStream newInputStream(Path path) throws IOException { + return Files.newInputStream(path); + } + @Override protected boolean fileExists(Path path) { return doPrivileged((PrivilegedAction) () -> Files.exists(path)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java index ffc14ca96a768..06606570699d9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStore.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.support.NoOpLogger; import org.elasticsearch.xpack.core.security.support.Validation; +import org.elasticsearch.xpack.security.PrivilegedFileWatcher; import org.elasticsearch.xpack.security.support.SecurityFiles; import java.io.IOException; @@ -57,7 +58,7 @@ public class FileUserRolesStore { file = resolveFile(config.env()); userRoles = parseFileLenient(file, logger); listeners = new CopyOnWriteArrayList<>(Collections.singletonList(listener)); - FileWatcher watcher = new FileWatcher(file.getParent()); + FileWatcher watcher = new PrivilegedFileWatcher(file.getParent()); watcher.addListener(new FileListener()); try { watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStore.java index 5a5d42b92bc84..dff79c56b32dc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountTokenStore.java @@ -24,6 +24,7 @@ import org.elasticsearch.xpack.core.security.action.service.TokenInfo.TokenSource; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.support.NoOpLogger; +import org.elasticsearch.xpack.security.PrivilegedFileWatcher; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.FileLineParser; @@ -59,7 +60,7 @@ public FileServiceAccountTokenStore( super(env.settings(), threadPool); this.clusterService = clusterService; file = resolveFile(env); - FileWatcher watcher = new FileWatcher(file.getParent()); + FileWatcher watcher = new PrivilegedFileWatcher(file.getParent()); watcher.addListener(new FileReloadListener(file, this::tryReload)); try { resourceWatcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java index 87378ac0b9f25..cf1afac19084f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java @@ -37,6 +37,7 @@ import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator; import org.elasticsearch.xpack.core.security.support.NoOpLogger; import org.elasticsearch.xpack.core.security.support.Validation; +import org.elasticsearch.xpack.security.PrivilegedFileWatcher; import org.elasticsearch.xpack.security.authz.FileRoleValidator; import java.io.IOException; @@ -110,7 +111,7 @@ public FileRolesStore( } this.licenseState = licenseState; this.xContentRegistry = xContentRegistry; - FileWatcher watcher = new FileWatcher(file.getParent()); + FileWatcher watcher = new PrivilegedFileWatcher(file.getParent()); watcher.addListener(new FileListener()); watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); permissions = parseFile(file, logger, settings, licenseState, xContentRegistry, roleValidator); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java index 61dc638e1d55d..088a9d30513e7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/operator/FileOperatorUsersStore.java @@ -31,6 +31,7 @@ import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.jwt.JwtRealmSettings; import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountSettings; +import org.elasticsearch.xpack.security.PrivilegedFileWatcher; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import java.io.IOException; @@ -59,7 +60,7 @@ public class FileOperatorUsersStore { public FileOperatorUsersStore(Environment env, ResourceWatcherService watcherService) { this.file = XPackPlugin.resolveConfigFile(env, "operator_users.yml"); this.operatorUsersDescriptor = parseFile(this.file, logger); - FileWatcher watcher = new FileWatcher(file.getParent(), true); + FileWatcher watcher = new PrivilegedFileWatcher(file.getParent(), true); watcher.addListener(new FileOperatorUsersStore.FileListener()); try { watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); diff --git a/x-pack/plugin/security/src/main/plugin-metadata/entitlement-policy.yaml b/x-pack/plugin/security/src/main/plugin-metadata/entitlement-policy.yaml index a6f29cb2ad7ea..f0992ef48e14c 100644 --- a/x-pack/plugin/security/src/main/plugin-metadata/entitlement-policy.yaml +++ b/x-pack/plugin/security/src/main/plugin-metadata/entitlement-policy.yaml @@ -4,6 +4,35 @@ org.elasticsearch.security: - relative_path: "" relative_to: config mode: read + - relative_path: users + relative_to: config + mode: read + exclusive: true + - relative_path: x-pack/users + relative_to: config + mode: read + exclusive: true + - path_setting: xpack.security.authc.realms.ldap.*.files.role_mapping + basedir_if_relative: config + mode: read + exclusive: true + - path_setting: xpack.security.authc.realms.pki.*.files.role_mapping + basedir_if_relative: config + mode: read + exclusive: true + - path_setting: xpack.security.authc.realms.kerberos.*.keytab.path + basedir_if_relative: config + mode: read + exclusive: true + - path_setting: xpack.security.authc.realms.jwt.*.pkc_jwkset_path + basedir_if_relative: config + mode: read + exclusive: true + - path_setting: xpack.security.authc.realms.saml.*.idp.metadata.path + basedir_if_relative: config + mode: read + exclusive: true + io.netty.transport: - manage_threads - inbound_network @@ -25,18 +54,7 @@ org.opensaml.xmlsec.impl: - org.apache.xml.security.ignoreLineBreaks org.opensaml.saml.impl: - files: - - relative_path: idp-docs-metadata.xml - relative_to: config - mode: read - - relative_path: idp-metadata.xml - relative_to: config - mode: read - - relative_path: saml-metadata.xml - relative_to: config - mode: read - - relative_path: metadata.xml - relative_to: config - mode: read - - relative_path: "saml/" - relative_to: config + - path_setting: xpack.security.authc.realms.saml.*.idp.metadata.path + basedir_if_relative: config mode: read + exclusive: true