Skip to content

Commit 37c47b5

Browse files
committed
Calculate changed roles on roles.yml reload
In order to optimize the use of the role cache, when the roles.yml file is reloaded we now calculate the names of removed, changed, and added roles so that they may be passed to any listeners. This allows a listener to selectively clear cache for only the roles that have been modified. The CompositeRolesStore has been adapted to do exactly that so that we limit the need to reload roles from sources such as the native roles stores or external role providers. See elastic#33205
1 parent 9d16a7b commit 37c47b5

File tree

4 files changed

+102
-20
lines changed

4 files changed

+102
-20
lines changed

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,7 @@ public CompositeRolesStore(Settings settings, FileRolesStore fileRolesStore, Nat
9797
ThreadContext threadContext, XPackLicenseState licenseState) {
9898
super(settings);
9999
this.fileRolesStore = fileRolesStore;
100-
// invalidating all on a file based role update is heavy handed to say the least, but in general this should be infrequent so the
101-
// impact isn't really worth the added complexity of only clearing the changed values
102-
fileRolesStore.addListener(this::invalidateAll);
100+
fileRolesStore.addListener(this::invalidate);
103101
this.nativeRolesStore = nativeRolesStore;
104102
this.reservedRolesStore = reservedRolesStore;
105103
this.privilegeStore = privilegeStore;
@@ -356,6 +354,23 @@ public void invalidate(String role) {
356354
negativeLookupCache.remove(role);
357355
}
358356

357+
public void invalidate(Set<String> roles) {
358+
numInvalidation.incrementAndGet();
359+
360+
// the cache cannot be modified while doing this operation per the terms of the cache iterator
361+
try (ReleasableLock ignored = writeLock.acquire()) {
362+
Iterator<Set<String>> keyIter = roleCache.keys().iterator();
363+
while (keyIter.hasNext()) {
364+
Set<String> key = keyIter.next();
365+
if (Sets.haveEmptyIntersection(key, roles) == false) {
366+
keyIter.remove();
367+
}
368+
}
369+
}
370+
371+
negativeLookupCache.removeAll(roles);
372+
}
373+
359374
public void usageStats(ActionListener<Map<String, Object>> listener) {
360375
final Map<String, Object> usage = new HashMap<>(2);
361376
usage.put("file", fileRolesStore.usageStats());

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

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.common.Nullable;
1313
import org.elasticsearch.common.component.AbstractComponent;
1414
import org.elasticsearch.common.settings.Settings;
15+
import org.elasticsearch.common.util.set.Sets;
1516
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
1617
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
1718
import org.elasticsearch.common.xcontent.XContentParser;
@@ -34,13 +35,16 @@
3435
import java.nio.file.Files;
3536
import java.nio.file.Path;
3637
import java.util.ArrayList;
38+
import java.util.Collections;
3739
import java.util.HashMap;
3840
import java.util.HashSet;
3941
import java.util.List;
4042
import java.util.Map;
4143
import java.util.Objects;
4244
import java.util.Set;
45+
import java.util.function.Consumer;
4346
import java.util.regex.Pattern;
47+
import java.util.stream.Collectors;
4448

4549
import static java.util.Collections.emptyMap;
4650
import static java.util.Collections.unmodifiableMap;
@@ -52,16 +56,16 @@ public class FileRolesStore extends AbstractComponent {
5256

5357
private final Path file;
5458
private final XPackLicenseState licenseState;
55-
private final List<Runnable> listeners = new ArrayList<>();
59+
private final List<Consumer<Set<String>>> listeners = new ArrayList<>();
5660

5761
private volatile Map<String, RoleDescriptor> permissions;
5862

5963
public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, XPackLicenseState licenseState)
6064
throws IOException {
61-
this(settings, env, watcherService, () -> {}, licenseState);
65+
this(settings, env, watcherService, null, licenseState);
6266
}
6367

64-
FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Runnable listener,
68+
FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Consumer<Set<String>> listener,
6569
XPackLicenseState licenseState) throws IOException {
6670
super(settings);
6771
this.file = resolveFile(env);
@@ -76,9 +80,10 @@ public FileRolesStore(Settings settings, Environment env, ResourceWatcherService
7680
}
7781

7882
public Set<RoleDescriptor> roleDescriptors(Set<String> roleNames) {
83+
final Map<String, RoleDescriptor> localPermissions = permissions;
7984
Set<RoleDescriptor> descriptors = new HashSet<>();
8085
roleNames.forEach((name) -> {
81-
RoleDescriptor descriptor = permissions.get(name);
86+
RoleDescriptor descriptor = localPermissions.get(name);
8287
if (descriptor != null) {
8388
descriptors.add(descriptor);
8489
}
@@ -87,12 +92,13 @@ public Set<RoleDescriptor> roleDescriptors(Set<String> roleNames) {
8792
}
8893

8994
public Map<String, Object> usageStats() {
95+
final Map<String, RoleDescriptor> localPermissions = permissions;
9096
Map<String, Object> usageStats = new HashMap<>(3);
91-
usageStats.put("size", permissions.size());
97+
usageStats.put("size", localPermissions.size());
9298

9399
boolean dls = false;
94100
boolean fls = false;
95-
for (RoleDescriptor descriptor : permissions.values()) {
101+
for (RoleDescriptor descriptor : localPermissions.values()) {
96102
for (IndicesPrivileges indicesPrivileges : descriptor.getIndicesPrivileges()) {
97103
fls = fls || indicesPrivileges.getGrantedFields() != null || indicesPrivileges.getDeniedFields() != null;
98104
dls = dls || indicesPrivileges.getQuery() != null;
@@ -107,17 +113,22 @@ public Map<String, Object> usageStats() {
107113
return usageStats;
108114
}
109115

110-
public void addListener(Runnable runnable) {
111-
Objects.requireNonNull(runnable);
116+
public void addListener(Consumer<Set<String>> consumer) {
117+
Objects.requireNonNull(consumer);
112118
synchronized (this) {
113-
listeners.add(runnable);
119+
listeners.add(consumer);
114120
}
115121
}
116122

117123
public Path getFile() {
118124
return file;
119125
}
120126

127+
// package private for testing
128+
Set<String> getAllRoleNames() {
129+
return permissions.keySet();
130+
}
131+
121132
public static Path resolveFile(Environment env) {
122133
return XPackPlugin.resolveConfigFile(env, "roles.yml");
123134
}
@@ -319,21 +330,27 @@ public void onFileDeleted(Path file) {
319330
}
320331

321332
@Override
322-
public void onFileChanged(Path file) {
333+
public synchronized void onFileChanged(Path file) {
323334
if (file.equals(FileRolesStore.this.file)) {
335+
final Map<String, RoleDescriptor> previousPermissions = permissions;
324336
try {
325337
permissions = parseFile(file, logger, settings, licenseState);
326-
logger.info("updated roles (roles file [{}] {})", file.toAbsolutePath(), Files.exists(file) ? "changed" : "removed");
338+
logger.info("updated roles (roles file [{}] {})", file.toAbsolutePath(),
339+
Files.exists(file) ? "changed" : "removed");
327340
} catch (Exception e) {
328341
logger.error(
329342
(Supplier<?>) () -> new ParameterizedMessage(
330343
"could not reload roles file [{}]. Current roles remain unmodified", file.toAbsolutePath()), e);
331344
return;
332345
}
333346

334-
synchronized (FileRolesStore.this) {
335-
listeners.forEach(Runnable::run);
336-
}
347+
final Set<String> changedOrMissingRoles = Sets.difference(previousPermissions.entrySet(), permissions.entrySet())
348+
.stream()
349+
.map(Map.Entry::getKey)
350+
.collect(Collectors.toSet());
351+
final Set<String> addedRoles = Sets.difference(permissions.keySet(), previousPermissions.keySet());
352+
final Set<String> changedRoles = Collections.unmodifiableSet(Sets.union(changedOrMissingRoles, addedRoles));
353+
listeners.forEach(c -> c.accept(changedRoles));
337354
}
338355
}
339356
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import java.util.concurrent.ExecutionException;
5454
import java.util.concurrent.atomic.AtomicInteger;
5555
import java.util.function.BiConsumer;
56+
import java.util.function.Consumer;
5657
import java.util.function.Function;
5758
import java.util.function.Predicate;
5859

@@ -213,7 +214,7 @@ public void testNegativeLookupsAreCached() {
213214
new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore,
214215
mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS),
215216
new XPackLicenseState(SECURITY_ENABLED_SETTINGS));
216-
verify(fileRolesStore).addListener(any(Runnable.class)); // adds a listener in ctor
217+
verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor
217218

218219
final String roleName = randomAlphaOfLengthBetween(1, 10);
219220
PlainActionFuture<Role> future = new PlainActionFuture<>();

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

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.nio.file.Path;
3838
import java.nio.file.StandardOpenOption;
3939
import java.util.Collections;
40+
import java.util.HashSet;
4041
import java.util.List;
4142
import java.util.Map;
4243
import java.util.Set;
@@ -317,8 +318,11 @@ public void testAutoReload() throws Exception {
317318
threadPool = new TestThreadPool("test");
318319
watcherService = new ResourceWatcherService(settings, threadPool);
319320
final CountDownLatch latch = new CountDownLatch(1);
320-
FileRolesStore store = new FileRolesStore(settings, env, watcherService, latch::countDown,
321-
new XPackLicenseState(Settings.EMPTY));
321+
final Set<String> modifiedRoles = new HashSet<>();
322+
FileRolesStore store = new FileRolesStore(settings, env, watcherService, roleSet -> {
323+
modifiedRoles.addAll(roleSet);
324+
latch.countDown();
325+
}, new XPackLicenseState(Settings.EMPTY));
322326

323327
Set<RoleDescriptor> descriptors = store.roleDescriptors(Collections.singleton("role1"));
324328
assertThat(descriptors, notNullValue());
@@ -342,6 +346,8 @@ public void testAutoReload() throws Exception {
342346
fail("Waited too long for the updated file to be picked up");
343347
}
344348

349+
assertEquals(1, modifiedRoles.size());
350+
assertTrue(modifiedRoles.contains("role5"));
345351
final TransportRequest request = mock(TransportRequest.class);
346352
descriptors = store.roleDescriptors(Collections.singleton("role5"));
347353
assertThat(descriptors, notNullValue());
@@ -352,6 +358,49 @@ public void testAutoReload() throws Exception {
352358
assertThat(role.cluster().check("cluster:monitor/foo/bar", request), is(true));
353359
assertThat(role.cluster().check("cluster:admin/foo/bar", request), is(false));
354360

361+
// truncate to remove some
362+
final Set<String> truncatedFileRolesModified = new HashSet<>();
363+
final CountDownLatch truncateLatch = new CountDownLatch(1);
364+
store = new FileRolesStore(settings, env, watcherService, roleSet -> {
365+
truncatedFileRolesModified.addAll(roleSet);
366+
truncateLatch.countDown();
367+
}, new XPackLicenseState(Settings.EMPTY));
368+
369+
final Set<String> allRolesPreTruncate = store.getAllRoleNames();
370+
try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING)) {
371+
writer.append("role5:").append(System.lineSeparator());
372+
writer.append(" cluster:").append(System.lineSeparator());
373+
writer.append(" - 'MONITOR'");
374+
}
375+
376+
truncateLatch.await();
377+
assertEquals(allRolesPreTruncate.size() - 1, truncatedFileRolesModified.size());
378+
assertTrue(allRolesPreTruncate.contains("role5"));
379+
assertFalse(truncatedFileRolesModified.contains("role5"));
380+
descriptors = store.roleDescriptors(Collections.singleton("role5"));
381+
assertThat(descriptors, notNullValue());
382+
assertEquals(1, descriptors.size());
383+
384+
// modify
385+
final Set<String> modifiedFileRolesModified = new HashSet<>();
386+
final CountDownLatch modifyLatch = new CountDownLatch(1);
387+
store = new FileRolesStore(settings, env, watcherService, roleSet -> {
388+
modifiedFileRolesModified.addAll(roleSet);
389+
modifyLatch.countDown();
390+
}, new XPackLicenseState(Settings.EMPTY));
391+
392+
try (BufferedWriter writer = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING)) {
393+
writer.append("role5:").append(System.lineSeparator());
394+
writer.append(" cluster:").append(System.lineSeparator());
395+
writer.append(" - 'ALL'");
396+
}
397+
398+
modifyLatch.await();
399+
assertEquals(1, modifiedFileRolesModified.size());
400+
assertTrue(modifiedFileRolesModified.contains("role5"));
401+
descriptors = store.roleDescriptors(Collections.singleton("role5"));
402+
assertThat(descriptors, notNullValue());
403+
assertEquals(1, descriptors.size());
355404
} finally {
356405
if (watcherService != null) {
357406
watcherService.stop();

0 commit comments

Comments
 (0)