Skip to content

Commit 3184a89

Browse files
AntonShuvaevtvernum
authored andcommitted
Security should not reload files that haven't changed (#50207)
In security we currently monitor a set of files for changes: - config/role_mapping.yml (or alternative configured path) - config/roles.yml - config/users - config/users_roles This commit prevents unnecessary reloading when the file change actually doesn't change the internal structure. Closes: #50063 Co-authored-by: Anton Shuvaev <[email protected]>
1 parent b14b4a7 commit 3184a89

File tree

10 files changed

+136
-15
lines changed

10 files changed

+136
-15
lines changed

server/src/main/java/org/elasticsearch/common/util/Maps.java

+19
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,23 @@ public static <K, V> Map<K, V> ofEntries(final Collection<Map.Entry<K, V>> entri
9999
return map;
100100
}
101101

102+
/**
103+
* Returns {@code true} if the two specified maps are equal to one another. Two maps are considered equal if both represent identical
104+
* mappings where values are checked with Objects.deepEquals. The primary use case is to check if two maps with array values are equal.
105+
*
106+
* @param left one map to be tested for equality
107+
* @param right the other map to be tested for equality
108+
* @return {@code true} if the two maps are equal
109+
*/
110+
public static <K, V> boolean deepEquals(Map<K, V> left, Map<K, V> right) {
111+
if (left == right) {
112+
return true;
113+
}
114+
if (left == null || right == null || left.size() != right.size()) {
115+
return false;
116+
}
117+
return left.entrySet().stream()
118+
.allMatch(e -> right.containsKey(e.getKey()) && Objects.deepEquals(e.getValue(), right.get(e.getKey())));
119+
}
120+
102121
}

server/src/test/java/org/elasticsearch/common/util/MapsTests.java

+36
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@
2222
import org.elasticsearch.test.ESTestCase;
2323

2424
import java.util.ArrayList;
25+
import java.util.Arrays;
2526
import java.util.Collection;
27+
import java.util.HashMap;
2628
import java.util.HashSet;
2729
import java.util.Map;
30+
import java.util.function.Supplier;
2831
import java.util.stream.Collectors;
32+
import java.util.stream.IntStream;
2933
import java.util.stream.Stream;
3034

3135
import static java.util.Map.entry;
36+
import static java.util.stream.Collectors.toMap;
3237
import static org.hamcrest.Matchers.equalTo;
3338
import static org.hamcrest.Matchers.hasItem;
3439

@@ -103,6 +108,31 @@ public void testOfEntries() {
103108
assertMapEntriesAndImmutability(map, entries);
104109
}
105110

111+
public void testDeepEquals() {
112+
final Supplier<String> keyGenerator = () -> randomAlphaOfLengthBetween(1, 5);
113+
final Supplier<int[]> arrayValueGenerator = () -> random().ints(randomInt(5)).toArray();
114+
final Map<String, int[]> map = randomMap(randomInt(5), keyGenerator, arrayValueGenerator);
115+
final Map<String, int[]> mapCopy = map.entrySet().stream()
116+
.collect(toMap(Map.Entry::getKey, e -> Arrays.copyOf(e.getValue(), e.getValue().length)));
117+
118+
assertTrue(Maps.deepEquals(map, mapCopy));
119+
120+
final Map<String, int[]> mapModified = mapCopy;
121+
if (mapModified.isEmpty()) {
122+
mapModified.put(keyGenerator.get(), arrayValueGenerator.get());
123+
} else {
124+
if (randomBoolean()) {
125+
final String randomKey = mapModified.keySet().toArray(new String[0])[randomInt(mapModified.size() - 1)];
126+
final int[] value = mapModified.get(randomKey);
127+
mapModified.put(randomKey, randomValueOtherThanMany((v) -> Arrays.equals(v, value), arrayValueGenerator));
128+
} else {
129+
mapModified.put(randomValueOtherThanMany(mapModified::containsKey, keyGenerator), arrayValueGenerator.get());
130+
}
131+
}
132+
133+
assertFalse(Maps.deepEquals(map, mapModified));
134+
}
135+
106136
private void assertMapEntries(final Map<String, String> map, final Collection<Map.Entry<String, String>> entries) {
107137
for (var entry : entries) {
108138
assertThat("map [" + map + "] does not contain key [" + entry.getKey() + "]", map.keySet(), hasItem(entry.getKey()));
@@ -160,4 +190,10 @@ private void assertMapEntriesAndImmutability(
160190
assertMapImmutability(map);
161191
}
162192

193+
private static <K, V> Map<K, V> randomMap(int size, Supplier<K> keyGenerator, Supplier<V> valueGenerator) {
194+
final Map<K, V> map = new HashMap<>();
195+
IntStream.range(0, size).forEach(i -> map.put(keyGenerator.get(), valueGenerator.get()));
196+
return map;
197+
}
198+
163199
}

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
*/
66
package org.elasticsearch.xpack.security.authc.file;
77

8-
import org.apache.logging.log4j.Logger;
98
import org.apache.logging.log4j.LogManager;
9+
import org.apache.logging.log4j.Logger;
1010
import org.apache.logging.log4j.message.ParameterizedMessage;
1111
import org.apache.logging.log4j.util.Supplier;
1212
import org.elasticsearch.ElasticsearchException;
1313
import org.elasticsearch.common.Nullable;
1414
import org.elasticsearch.common.settings.SecureString;
1515
import org.elasticsearch.common.settings.Settings;
16+
import org.elasticsearch.common.util.Maps;
1617
import org.elasticsearch.env.Environment;
1718
import org.elasticsearch.watcher.FileChangesListener;
1819
import org.elasticsearch.watcher.FileWatcher;
@@ -191,9 +192,13 @@ public void onFileDeleted(Path file) {
191192
@Override
192193
public void onFileChanged(Path file) {
193194
if (file.equals(FileUserPasswdStore.this.file)) {
194-
logger.info("users file [{}] changed. updating users... )", file.toAbsolutePath());
195+
final Map<String, char[]> previousUsers = users;
195196
users = parseFileLenient(file, logger, settings);
196-
notifyRefresh();
197+
198+
if (Maps.deepEquals(previousUsers, users) == false) {
199+
logger.info("users file [{}] changed. updating users... )", file.toAbsolutePath());
200+
notifyRefresh();
201+
}
197202
}
198203
}
199204
}

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
*/
66
package org.elasticsearch.xpack.security.authc.file;
77

8-
import org.apache.logging.log4j.Logger;
98
import org.apache.logging.log4j.LogManager;
9+
import org.apache.logging.log4j.Logger;
1010
import org.apache.logging.log4j.message.ParameterizedMessage;
1111
import org.apache.logging.log4j.util.Supplier;
1212
import org.elasticsearch.ElasticsearchException;
1313
import org.elasticsearch.common.Nullable;
1414
import org.elasticsearch.common.Strings;
15+
import org.elasticsearch.common.util.Maps;
1516
import org.elasticsearch.env.Environment;
1617
import org.elasticsearch.watcher.FileChangesListener;
1718
import org.elasticsearch.watcher.FileWatcher;
@@ -206,9 +207,13 @@ public void onFileDeleted(Path file) {
206207
@Override
207208
public void onFileChanged(Path file) {
208209
if (file.equals(FileUserRolesStore.this.file)) {
209-
logger.info("users roles file [{}] changed. updating users roles...", file.toAbsolutePath());
210+
final Map<String, String[]> previousUserRoles = userRoles;
210211
userRoles = parseFileLenient(file, logger);
211-
notifyRefresh();
212+
213+
if (Maps.deepEquals(previousUserRoles, userRoles) == false) {
214+
logger.info("users roles file [{}] changed. updating users roles...", file.toAbsolutePath());
215+
notifyRefresh();
216+
}
212217
}
213218
}
214219
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapper.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,14 @@ public void onFileDeleted(Path file) {
223223
@Override
224224
public void onFileChanged(Path file) {
225225
if (file.equals(DnRoleMapper.this.file)) {
226-
logger.info("role mappings file [{}] changed for realm [{}/{}]. updating mappings...", file.toAbsolutePath(),
227-
config.type(), config.name());
226+
final Map<String, List<String>> previousDnRoles = dnRoles;
228227
dnRoles = parseFileLenient(file, logger, config.type(), config.name());
229-
notifyRefresh();
228+
229+
if (previousDnRoles.equals(dnRoles) == false) {
230+
logger.info("role mappings file [{}] changed for realm [{}/{}]. updating mappings...", file.toAbsolutePath(),
231+
config.type(), config.name());
232+
notifyRefresh();
233+
}
230234
}
231235
}
232236
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/FileRolesStore.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,6 @@ public synchronized void onFileChanged(Path file) {
368368
final Map<String, RoleDescriptor> previousPermissions = permissions;
369369
try {
370370
permissions = parseFile(file, logger, settings, licenseState, xContentRegistry);
371-
logger.info("updated roles (roles file [{}] {})", file.toAbsolutePath(),
372-
Files.exists(file) ? "changed" : "removed");
373371
} catch (Exception e) {
374372
logger.error(
375373
(Supplier<?>) () -> new ParameterizedMessage(
@@ -383,7 +381,11 @@ public synchronized void onFileChanged(Path file) {
383381
.collect(Collectors.toSet());
384382
final Set<String> addedRoles = Sets.difference(permissions.keySet(), previousPermissions.keySet());
385383
final Set<String> changedRoles = Collections.unmodifiableSet(Sets.union(changedOrMissingRoles, addedRoles));
386-
listeners.forEach(c -> c.accept(changedRoles));
384+
if (changedRoles.isEmpty() == false) {
385+
logger.info("updated roles (roles file [{}] {})", file.toAbsolutePath(),
386+
Files.exists(file) ? "changed" : "removed");
387+
listeners.forEach(c -> c.accept(changedRoles));
388+
}
387389
}
388390
}
389391
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class FileUserPasswdStoreTests extends ESTestCase {
5353
@Before
5454
public void init() {
5555
settings = Settings.builder()
56-
.put("resource.reload.interval.high", "2s")
56+
.put("resource.reload.interval.high", "100ms")
5757
.put("path.home", createTempDir())
5858
.put("xpack.security.authc.password_hashing.algorithm", randomFrom("bcrypt", "bcrypt11", "pbkdf2", "pbkdf2_1000",
5959
"pbkdf2_50000"))
@@ -103,6 +103,20 @@ public void testStore_AutoReload() throws Exception {
103103

104104
watcherService.start();
105105

106+
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
107+
writer.append("\n");
108+
}
109+
110+
watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH);
111+
if (latch.getCount() != 1) {
112+
fail("Listener should not be called as users passwords are not changed.");
113+
}
114+
115+
assertThat(store.userExists(username), is(true));
116+
result = store.verifyPassword(username, new SecureString("test123"), () -> user);
117+
assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS));
118+
assertThat(result.getUser(), is(user));
119+
106120
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
107121
writer.newLine();
108122
writer.append("foobar:").append(new String(hasher.hash(new SecureString("barfoo"))));

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public class FileUserRolesStoreTests extends ESTestCase {
5454
@Before
5555
public void init() {
5656
settings = Settings.builder()
57-
.put("resource.reload.interval.high", "2s")
57+
.put("resource.reload.interval.high", "100ms")
5858
.put("path.home", createTempDir())
5959
.build();
6060
env = TestEnvironment.newEnvironment(settings);
@@ -102,6 +102,17 @@ public void testStoreAutoReload() throws Exception {
102102

103103
watcherService.start();
104104

105+
try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
106+
writer.append("\n");
107+
}
108+
109+
watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH);
110+
if (latch.getCount() != 1) {
111+
fail("Listener should not be called as users roles are not changed.");
112+
}
113+
114+
assertThat(store.roles("user1"), arrayContaining("role1", "role2", "role3"));
115+
105116
try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
106117
writer.newLine();
107118
writer.append("role4:user4\nrole5:user4\n");

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ public void testMapper_AutoReload() throws Exception {
112112

113113
watcherService.start();
114114

115+
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
116+
writer.append("\n");
117+
}
118+
119+
watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH);
120+
if (latch.getCount() != 1) {
121+
fail("Listener should not be called as roles mapping is not changed.");
122+
}
123+
124+
roles = mapper.resolveRoles("", Collections.singletonList("cn=shield,ou=marvel,o=superheros"));
125+
assertThat(roles, contains("security"));
126+
115127
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
116128
writer.newLine();
117129
writer.append("fantastic_four:\n")

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ public void testAutoReload() throws Exception {
332332
}
333333

334334
Settings.Builder builder = Settings.builder()
335-
.put("resource.reload.interval.high", "500ms")
335+
.put("resource.reload.interval.high", "100ms")
336336
.put("path.home", home);
337337
Settings settings = builder.build();
338338
Environment env = TestEnvironment.newEnvironment(settings);
@@ -354,6 +354,19 @@ public void testAutoReload() throws Exception {
354354

355355
watcherService.start();
356356

357+
try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
358+
writer.append("\n");
359+
}
360+
361+
watcherService.notifyNow(ResourceWatcherService.Frequency.HIGH);
362+
if (latch.getCount() != 1) {
363+
fail("Listener should not be called as roles are not changed.");
364+
}
365+
366+
descriptors = store.roleDescriptors(Collections.singleton("role1"));
367+
assertThat(descriptors, notNullValue());
368+
assertEquals(1, descriptors.size());
369+
357370
try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
358371
writer.newLine();
359372
writer.newLine();

0 commit comments

Comments
 (0)