Skip to content

Commit d517932

Browse files
authored
Replace daemon thread in InMemoryRolePermissionResolver with RoleChangedEvent (#22507)
* Replace daemon thread in InMemoryRolePermissionResolver with RoleChangedEvent * code cleanup
1 parent 1e0fba7 commit d517932

File tree

6 files changed

+244
-29
lines changed

6 files changed

+244
-29
lines changed

graylog2-server/src/main/java/org/graylog2/security/InMemoryRolePermissionResolver.java

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,25 @@
1717
package org.graylog2.security;
1818

1919
import com.google.common.collect.ImmutableMap;
20+
import com.google.common.eventbus.EventBus;
21+
import com.google.common.eventbus.Subscribe;
22+
import jakarta.inject.Inject;
23+
import jakarta.inject.Singleton;
2024
import org.apache.shiro.authz.Permission;
2125
import org.apache.shiro.authz.permission.AllPermission;
2226
import org.apache.shiro.authz.permission.RolePermissionResolver;
2327
import org.graylog.security.permissions.CaseSensitiveWildcardPermission;
2428
import org.graylog2.shared.users.Role;
29+
import org.graylog2.users.RoleChangedEvent;
2530
import org.graylog2.users.RoleService;
2631
import org.slf4j.Logger;
2732
import org.slf4j.LoggerFactory;
2833

2934
import javax.annotation.Nonnull;
30-
31-
import jakarta.inject.Inject;
32-
import jakarta.inject.Named;
33-
import jakarta.inject.Singleton;
34-
3535
import java.util.Collection;
3636
import java.util.Collections;
3737
import java.util.Map;
3838
import java.util.Set;
39-
import java.util.concurrent.ScheduledExecutorService;
40-
import java.util.concurrent.TimeUnit;
4139
import java.util.concurrent.atomic.AtomicReference;
4240
import java.util.stream.Collectors;
4341

@@ -50,15 +48,12 @@ public class InMemoryRolePermissionResolver implements RolePermissionResolver {
5048

5149
@Inject
5250
public InMemoryRolePermissionResolver(RoleService roleService,
53-
@Named("daemonScheduler") ScheduledExecutorService daemonScheduler) {
51+
EventBus eventBus) {
5452
this.roleService = roleService;
55-
final RoleUpdater updater = new RoleUpdater();
5653

54+
eventBus.register(this);
5755
// eagerly load rules
58-
updater.run();
59-
60-
// update rules every second in the background
61-
daemonScheduler.scheduleAtFixedRate(updater, 1, 1, TimeUnit.SECONDS);
56+
updateRoles();
6257
}
6358

6459
@Override
@@ -93,15 +88,17 @@ public Set<String> resolveStringPermission(String roleId) {
9388
}
9489

9590

96-
private class RoleUpdater implements Runnable {
97-
@Override
98-
public void run() {
99-
try {
100-
final Map<String, Role> index = roleService.loadAllIdMap();
101-
InMemoryRolePermissionResolver.this.idToRoleIndex.set(ImmutableMap.copyOf(index));
102-
} catch (Exception e) {
103-
log.error("Could not find roles collection, no user roles updated.", e);
104-
}
91+
@Subscribe
92+
public void handleRoleChangedEvent(RoleChangedEvent event) {
93+
updateRoles();
94+
}
95+
96+
private void updateRoles() {
97+
try {
98+
final Map<String, Role> index = roleService.loadAllIdMap();
99+
this.idToRoleIndex.set(ImmutableMap.copyOf(index));
100+
} catch (Exception e) {
101+
log.error("Could not find roles collection, no user roles updated.", e);
105102
}
106103
}
107104
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog2.users;
18+
19+
import org.graylog2.shared.users.Role;
20+
21+
/**
22+
* Fired every time a role is persisted or deleted.
23+
*
24+
* @param roleName name from {@link Role#getName()}
25+
*/
26+
public record RoleChangedEvent(String roleName) {
27+
}

graylog2-server/src/main/java/org/graylog2/users/RoleServiceImpl.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.graylog2.database.MongoCollections;
3434
import org.graylog2.database.NotFoundException;
3535
import org.graylog2.database.utils.MongoUtils;
36+
import org.graylog2.events.ClusterEventBus;
3637
import org.graylog2.plugin.database.ValidationException;
3738
import org.graylog2.shared.security.Permissions;
3839
import org.graylog2.shared.users.Role;
@@ -66,14 +67,16 @@ public class RoleServiceImpl implements RoleService {
6667
private final String adminRoleObjectId;
6768
private final String readerRoleObjectId;
6869
private final MongoCollection<RoleImpl> collection;
70+
private final ClusterEventBus clusterEventBus;
6971

7072
@Inject
7173
public RoleServiceImpl(MongoCollections mongoCollections,
7274
Permissions permissions,
73-
Validator validator) {
75+
Validator validator, ClusterEventBus clusterEventBus) {
7476
this.validator = validator;
7577

7678
collection = mongoCollections.nonEntityCollection(ROLES_COLLECTION_NAME, RoleImpl.class);
79+
this.clusterEventBus = clusterEventBus;
7780
// lower case role names are unique, this allows arbitrary naming, but still uses an index
7881
collection.createIndex(Indexes.ascending(NAME_LOWER), new IndexOptions().unique(true));
7982

@@ -179,17 +182,19 @@ public Map<String, Role> loadAllLowercaseNameMap() {
179182
}
180183

181184
@Override
182-
public RoleImpl save(Role role1) throws ValidationException {
185+
public RoleImpl save(Role role) throws ValidationException {
183186
// sucky but necessary because of graylog2-shared not knowing about mongodb :(
184-
if (!(role1 instanceof final RoleImpl role)) {
187+
if (!(role instanceof final RoleImpl roleImpl)) {
185188
throw new IllegalArgumentException("invalid Role implementation class");
186189
}
187-
final Set<ConstraintViolation<Role>> violations = validate(role);
190+
final Set<ConstraintViolation<Role>> violations = validate(roleImpl);
188191
if (!violations.isEmpty()) {
189192
throw new ValidationException("Validation failed.", violations.toString());
190193
}
191-
return collection.findOneAndReplace(eq(NAME_LOWER, role.nameLower()), role,
194+
final RoleImpl result = collection.findOneAndReplace(eq(NAME_LOWER, roleImpl.nameLower()), roleImpl,
192195
new FindOneAndReplaceOptions().returnDocument(ReturnDocument.AFTER).upsert(true));
196+
triggerChangeEvent(roleImpl.getName());
197+
return result;
193198
}
194199

195200
@Override
@@ -202,7 +207,17 @@ public int delete(String roleName) {
202207
final var nameMatchesAndNotReadonly = Filters.and(
203208
eq(READ_ONLY, false),
204209
eq(NAME_LOWER, roleName.toLowerCase(Locale.ENGLISH)));
205-
return Ints.saturatedCast(collection.deleteOne(nameMatchesAndNotReadonly).getDeletedCount());
210+
final int result = Ints.saturatedCast(collection.deleteOne(nameMatchesAndNotReadonly).getDeletedCount());
211+
triggerChangeEvent(roleName);
212+
return result;
213+
}
214+
215+
/**
216+
* Notify other parts of the system and cluster that there was a change in roles.
217+
* @param roleName which role has been persisted or deleted.
218+
*/
219+
private void triggerChangeEvent(String roleName) {
220+
clusterEventBus.post(new RoleChangedEvent(roleName));
206221
}
207222

208223
@Override

graylog2-server/src/test/java/org/graylog2/migrations/V20200803120800_GrantsMigrations/RolesToGrantsMigrationTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.graylog2.bindings.providers.MongoJackObjectMapperProvider;
3434
import org.graylog2.database.MongoCollections;
3535
import org.graylog2.database.NotFoundException;
36+
import org.graylog2.events.ClusterEventBus;
3637
import org.graylog2.plugin.database.users.User;
3738
import org.graylog2.shared.security.Permissions;
3839
import org.graylog2.shared.users.UserService;
@@ -82,7 +83,7 @@ void setUp(MongoDBTestService mongodb,
8283
this.grnRegistry = grnRegistry;
8384

8485
roleService = new RoleServiceImpl(
85-
new MongoCollections(mongoJackObjectMapperProvider, mongodb.mongoConnection()), permissions, validator);
86+
new MongoCollections(mongoJackObjectMapperProvider, mongodb.mongoConnection()), permissions, validator, new ClusterEventBus());
8687

8788
this.dbGrantService = new DBGrantService(new MongoCollections(mongoJackObjectMapperProvider, mongodb.mongoConnection()));
8889
this.userService = userService;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog2.security;
18+
19+
import com.google.common.eventbus.EventBus;
20+
import jakarta.annotation.Nonnull;
21+
import jakarta.validation.Validator;
22+
import org.apache.shiro.authz.permission.AllPermission;
23+
import org.apache.shiro.authz.permission.RolePermissionResolver;
24+
import org.assertj.core.api.Assertions;
25+
import org.graylog.security.permissions.CaseSensitiveWildcardPermission;
26+
import org.graylog.testing.mongodb.MongoDBExtension;
27+
import org.graylog2.database.MongoCollections;
28+
import org.graylog2.database.NotFoundException;
29+
import org.graylog2.events.ClusterEventBus;
30+
import org.graylog2.plugin.database.ValidationException;
31+
import org.graylog2.shared.security.Permissions;
32+
import org.graylog2.shared.security.RestPermissions;
33+
import org.graylog2.shared.users.Role;
34+
import org.graylog2.users.RoleImpl;
35+
import org.graylog2.users.RoleService;
36+
import org.graylog2.users.RoleServiceImpl;
37+
import org.junit.jupiter.api.Test;
38+
import org.junit.jupiter.api.extension.ExtendWith;
39+
import org.mockito.Mockito;
40+
41+
import java.util.Collections;
42+
import java.util.Set;
43+
44+
@ExtendWith(MongoDBExtension.class)
45+
class InMemoryRolePermissionResolverTest {
46+
@Test
47+
void testRoleChangedEventHandling(MongoCollections mongoCollections) throws NotFoundException, ValidationException {
48+
final EventBus eventBus = new EventBus();
49+
final ClusterEventBus clusterEventBus = new ClusterEventBus() {
50+
@Override
51+
public void post(@Nonnull Object event) {
52+
eventBus.post(event);
53+
}
54+
};
55+
final RoleService service = new RoleServiceImpl(mongoCollections, new Permissions(Collections.emptySet()), Mockito.mock(Validator.class), clusterEventBus);
56+
final RolePermissionResolver resolver = new InMemoryRolePermissionResolver(service, eventBus);
57+
58+
// test that the built-in roles are present right after resolver init
59+
Assertions.assertThat(resolver.resolvePermissionsInRole(service.getAdminRoleObjectId()))
60+
.anySatisfy(permission -> permission.implies(new AllPermission()));
61+
62+
// now let the service create a role. This information should be immediately propagated to the resolver
63+
final Role createdRole = service.save(createRole("inputs_manager", "manages inputs", Set.of(RestPermissions.INPUTS_READ, RestPermissions.INPUTS_CREATE)));
64+
65+
// without explicitly updating the resolver (relying on the event bus), let's check that the role and its permissions
66+
// are available.
67+
Assertions.assertThat(resolver.resolvePermissionsInRole(createdRole.getId()))
68+
.hasSize(2)
69+
.anySatisfy(permission -> permission.implies(new CaseSensitiveWildcardPermission(RestPermissions.INPUTS_READ)))
70+
.anySatisfy(permission -> permission.implies(new CaseSensitiveWildcardPermission(RestPermissions.INPUTS_CREATE)));
71+
72+
// now let's delete a role and check that the resolver got rid of it as well
73+
service.delete("inputs_manager");
74+
Assertions.assertThat(resolver.resolvePermissionsInRole(createdRole.getId()))
75+
.isEmpty();
76+
}
77+
78+
79+
@Nonnull
80+
private static RoleImpl createRole(String name, String description, Set<String> permissions) {
81+
final RoleImpl role = new RoleImpl();
82+
role.setName(name);
83+
role.setDescription(description);
84+
role.setPermissions(permissions);
85+
role.setReadOnly(false);
86+
return role;
87+
}
88+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog2.users;
18+
19+
import com.google.common.eventbus.EventBus;
20+
import com.google.common.eventbus.Subscribe;
21+
import jakarta.annotation.Nonnull;
22+
import jakarta.validation.Validator;
23+
import org.assertj.core.api.Assertions;
24+
import org.graylog.testing.mongodb.MongoDBExtension;
25+
import org.graylog2.database.MongoCollections;
26+
import org.graylog2.events.ClusterEventBus;
27+
import org.graylog2.plugin.database.ValidationException;
28+
import org.graylog2.shared.security.Permissions;
29+
import org.graylog2.shared.security.RestPermissions;
30+
import org.junit.jupiter.api.Test;
31+
import org.junit.jupiter.api.extension.ExtendWith;
32+
import org.mockito.Mockito;
33+
34+
import java.util.Collections;
35+
import java.util.LinkedList;
36+
import java.util.List;
37+
import java.util.Set;
38+
39+
@ExtendWith(MongoDBExtension.class)
40+
class RoleServiceImplTest {
41+
42+
@Test
43+
void testChangeEvents(MongoCollections mongoCollections) throws ValidationException {
44+
final EventsCollector eventsCollector = new EventsCollector();
45+
final EventBus eventBus = new EventBus();
46+
47+
final ClusterEventBus clusterEventBus = new ClusterEventBus() {
48+
@Override
49+
public void post(@Nonnull Object event) {
50+
eventBus.post(event);
51+
}
52+
};
53+
54+
eventBus.register(eventsCollector);
55+
final RoleService service = new RoleServiceImpl(mongoCollections, new Permissions(Collections.emptySet()), Mockito.mock(Validator.class), clusterEventBus);
56+
service.save(createRole("inputs_manager", "manages inputs", Set.of(RestPermissions.INPUTS_READ, RestPermissions.INPUTS_CREATE), false));
57+
service.save(createRole("inputs_reader", "reads inputs", Set.of(RestPermissions.INPUTS_READ), false));
58+
59+
Assertions.assertThat(eventsCollector.getEvents())
60+
.hasSize(4) // two built-in roles, Admin and Reader + whatever we add here
61+
.map(RoleChangedEvent::roleName)
62+
.contains("Admin", "Reader", "inputs_manager", "inputs_reader");
63+
}
64+
65+
@Nonnull
66+
private static RoleImpl createRole(String name, String description, Set<String> permissions, boolean readOnly) {
67+
final RoleImpl role = new RoleImpl();
68+
role.setName(name);
69+
role.setDescription(description);
70+
role.setPermissions(permissions);
71+
role.setReadOnly(readOnly);
72+
return role;
73+
}
74+
75+
private class EventsCollector {
76+
private final List<RoleChangedEvent> events = new LinkedList<>();
77+
78+
@Subscribe
79+
public void subscribe(RoleChangedEvent event) {
80+
this.events.add(event);
81+
}
82+
83+
public List<RoleChangedEvent> getEvents() {
84+
return events;
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)