Fixing recent activity for non-admins. (#24080)

* Fixing recent activity for non-admins.

* Adding changelog snippet.

* Fixing up call.

* Adjusting tests.

* Also take static permissions (e.g. through roles) into account when checking activity visibility.

* Fix links for dashboards.

* Fixing up after merge.
This commit is contained in:
Dennis Oelkers
2025-11-21 12:32:57 +01:00
committed by GitHub
parent 00d660706b
commit 722ad4dc8b
7 changed files with 87 additions and 21 deletions

View File

@@ -0,0 +1,5 @@
type = "f"
message = "Fixing recent activity for non-admins: Including update events to accessible entities."
pulls = ["24080"]
issues = ["Graylog2/graylog-plugin-enterprise#12427"]

View File

@@ -79,7 +79,9 @@ public class DBEventDefinitionService {
@Inject
public DBEventDefinitionService(MongoCollections mongoCollections,
DBEventProcessorStateService stateService,
EntityRegistrar entityRegistrar, EntityScopeService entityScopeService, SearchFiltersReFetcher searchFiltersRefetcher) {
EntityRegistrar entityRegistrar,
EntityScopeService entityScopeService,
SearchFiltersReFetcher searchFiltersRefetcher) {
this.collection = mongoCollections.collection(COLLECTION_NAME, EventDefinitionDto.class);
this.mongoUtils = mongoCollections.utils(collection);
this.scopedEntityMongoUtils = mongoCollections.scopedEntityUtils(collection, entityScopeService);

View File

@@ -16,27 +16,37 @@
*/
package org.graylog.plugins.views.startpage.recentActivities;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import com.mongodb.BasicDBObject;
import jakarta.inject.Singleton;
import org.graylog2.database.MongoCollection;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.Filters;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.graylog.grn.GRN;
import org.graylog.grn.GRNRegistry;
import org.graylog.grn.GRNType;
import org.graylog.grn.GRNTypes;
import org.graylog.plugins.views.search.permissions.SearchUser;
import org.graylog.security.Capability;
import org.graylog.security.CapabilityRegistry;
import org.graylog.security.DBGrantService;
import org.graylog.security.GrantDTO;
import org.graylog.security.PermissionAndRoleResolver;
import org.graylog.security.shares.GranteeService;
import org.graylog.security.shares.PluggableEntityService;
import org.graylog2.database.MongoCollection;
import org.graylog2.database.MongoCollections;
import org.graylog2.database.MongoConnection;
import org.graylog2.database.PaginatedList;
import org.graylog2.database.pagination.MongoPaginationHelper;
import org.graylog2.plugin.database.users.User;
import org.graylog2.plugin.security.Permission;
import org.graylog2.rest.models.SortOrder;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Singleton
public class RecentActivityService {
@@ -48,14 +58,23 @@ public class RecentActivityService {
private static final long MAXIMUM_RECENT_ACTIVITIES = 10000;
private final MongoCollection<RecentActivityDTO> db;
private final MongoPaginationHelper<RecentActivityDTO> pagination;
private final DBGrantService grantService;
private final GranteeService granteeService;
private final PluggableEntityService pluggableEntityService;
private CapabilityRegistry capabilityRegistry;
@Inject
public RecentActivityService(final MongoCollections mongoCollections,
final MongoConnection mongoConnection,
final EventBus eventBus,
final GRNRegistry grnRegistry,
final PermissionAndRoleResolver permissionAndRoleResolver) {
this(mongoCollections, mongoConnection, eventBus, grnRegistry, permissionAndRoleResolver, MAXIMUM_RECENT_ACTIVITIES);
final PermissionAndRoleResolver permissionAndRoleResolver,
final DBGrantService grantService,
final GranteeService granteeService,
final PluggableEntityService pluggableEntityService,
final CapabilityRegistry capabilityRegistry) {
this(mongoCollections, mongoConnection, eventBus, grnRegistry, permissionAndRoleResolver, MAXIMUM_RECENT_ACTIVITIES,
grantService, granteeService, pluggableEntityService, capabilityRegistry);
}
/*
@@ -66,7 +85,15 @@ public class RecentActivityService {
final EventBus eventBus,
final GRNRegistry grnRegistry,
final PermissionAndRoleResolver permissionAndRoleResolver,
final long maximum) {
final long maximum,
final DBGrantService grantService,
final GranteeService granteeService,
final PluggableEntityService pluggableEntityService,
final CapabilityRegistry capabilityRegistry) {
this.grantService = grantService;
this.granteeService = granteeService;
this.pluggableEntityService = pluggableEntityService;
this.capabilityRegistry = capabilityRegistry;
final var mongodb = mongoConnection.getMongoDatabase();
if (!mongodb.listCollectionNames().into(new HashSet<>()).contains(COLLECTION_NAME)) {
mongodb.createCollection(COLLECTION_NAME, new CreateCollectionOptions().capped(true).sizeInBytes(maximum * 1024).maxDocuments(maximum));
@@ -122,15 +149,33 @@ public class RecentActivityService {
// filter relevant activities by permissions
final var principal = grnRegistry.newGRN(GRNTypes.USER, user.getUser().getId());
final var grns = permissionAndRoleResolver.resolveGrantees(principal).stream().map(GRN::toString).toList();
var query = Filters.in(RecentActivityDTO.FIELD_GRANTEE, grns);
final var grantees = permissionAndRoleResolver.resolveGrantees(principal).stream().map(GRN::toString).toList();
final var sharedEntities = getShareGRNsFor(principal);
return pagination
.perPage(perPage)
.sort(sort)
.filter(query)
.includeGrandTotal(true)
.grandTotalFilter(query)
.page(page);
.page(page, activity -> {
final var itemGRN = activity.itemGrn();
final var hasAnyPermission = capabilityRegistry.getPermissions(Capability.VIEW, itemGRN.grnType())
.stream()
.map(Permission::permission)
.anyMatch(permission -> user.isPermitted(permission, itemGRN.entity()));
return hasAnyPermission || grantees.contains(activity.grantee()) || sharedEntities.contains(itemGRN);
});
}
private Set<GRN> getShareGRNsFor(GRN grantee) {
// Get all aliases for the grantee to make sure we find all entities the grantee has access to
final Set<GRN> granteeAliases = granteeService.getGranteeAliases(grantee);
final ImmutableSet<GrantDTO> grants = grantService.getForGranteesOrGlobalWithCapability(granteeAliases, Capability.VIEW);
return grants.stream()
.map(GrantDTO::target)
.flatMap(pluggableEntityService::expand)
.filter(pluggableEntityService.excludeTypesFilter())
.collect(Collectors.toSet());
}
public void deleteAllEntriesForEntity(GRN grn) {

View File

@@ -31,7 +31,11 @@ import org.graylog.plugins.views.startpage.lastOpened.LastOpenedForUserDTO;
import org.graylog.plugins.views.startpage.lastOpened.LastOpenedService;
import org.graylog.plugins.views.startpage.recentActivities.RecentActivityService;
import org.graylog.plugins.views.startpage.title.StartPageItemTitleRetriever;
import org.graylog.security.CapabilityRegistry;
import org.graylog.security.DBGrantService;
import org.graylog.security.PermissionAndRoleResolver;
import org.graylog.security.shares.GranteeService;
import org.graylog.security.shares.PluggableEntityService;
import org.graylog.testing.GRNExtension;
import org.graylog.testing.TestUserServiceExtension;
import org.graylog.testing.mongodb.MongoDBExtension;
@@ -75,11 +79,9 @@ public class StartPageServiceTest {
MongoCollections mongoCollections,
GRNRegistry grnRegistry) {
var admin = TestUser.builder().withId("637748db06e1d74da0a54331").withUsername("local:admin").isLocalAdmin(true).build();
var user = TestUser.builder().withId("637748db06e1d74da0a54330").withUsername("test").isLocalAdmin(false).build();
this.searchUser = TestSearchUser.builder().withUser(user).build();
this.grnRegistry = grnRegistry;
var searchAdmin = TestSearchUser.builder().withUser(admin).build();
var permissionAndRoleResolver = new PermissionAndRoleResolver() {
@Override
@@ -101,7 +103,8 @@ public class StartPageServiceTest {
var eventbus = new EventBus();
final var connection = mongodb.mongoConnection();
var lastOpenedService = new LastOpenedService(mongoCollections, eventbus);
var recentActivityService = new RecentActivityService(mongoCollections, connection, eventbus, grnRegistry, permissionAndRoleResolver);
var recentActivityService = new RecentActivityService(mongoCollections, connection, eventbus, grnRegistry, permissionAndRoleResolver,
new DBGrantService(mongoCollections), mock(GranteeService.class), new PluggableEntityService(Set.of()), mock(CapabilityRegistry.class));
catalog = mock(Catalog.class);
doReturn(Optional.of(new Catalog.Entry("", ""))).when(catalog).getEntry(any());
startPageService = new StartPageService(grnRegistry, lastOpenedService, recentActivityService, eventbus, new StartPageItemTitleRetriever(catalog, Map.of()));

View File

@@ -23,7 +23,11 @@ import org.graylog.grn.GRNTypes;
import org.graylog.plugins.views.search.permissions.SearchUser;
import org.graylog.plugins.views.search.rest.TestSearchUser;
import org.graylog.plugins.views.search.rest.TestUser;
import org.graylog.security.CapabilityRegistry;
import org.graylog.security.DBGrantService;
import org.graylog.security.PermissionAndRoleResolver;
import org.graylog.security.shares.GranteeService;
import org.graylog.security.shares.PluggableEntityService;
import org.graylog.testing.GRNExtension;
import org.graylog.testing.TestUserService;
import org.graylog.testing.TestUserServiceExtension;
@@ -40,6 +44,7 @@ import java.util.Objects;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@ExtendWith(MongoDBExtension.class)
@ExtendWith(MongoJackExtension.class)
@@ -89,12 +94,18 @@ public class RecentActivityServiceTest {
this.testUserService = testUserService;
this.grnRegistry = grnRegistry;
this.recentActivityService = new RecentActivityService(mongoCollections,
this.recentActivityService = new RecentActivityService(
mongoCollections,
mongodb.mongoConnection(),
null,
grnRegistry,
permissionAndRoleResolver,
MAXIMUM);
MAXIMUM,
new DBGrantService(mongoCollections),
mock(GranteeService.class),
new PluggableEntityService(Set.of()),
mock(CapabilityRegistry.class)
);
}
@Test

View File

@@ -17,18 +17,17 @@
import { useMemo } from 'react';
import useCurrentUser from 'hooks/useCurrentUser';
import getPermissionPrefixByType from 'util/getPermissionPrefixByType';
import { isPermitted } from 'util/PermissionsMixin';
import { getValuesFromGRN } from 'logic/permissions/GRN';
import usePermissions from 'hooks/usePermissions';
const useHasEntityPermissionByGRN = (grn: string, permissionType: string = 'read') => {
const { id, type } = getValuesFromGRN(grn);
const { permissions } = useCurrentUser();
const { isPermitted } = usePermissions();
return useMemo(
() => isPermitted(permissions, `${getPermissionPrefixByType(type, id)}${permissionType}:${id}`),
[id, permissionType, permissions, type],
() => isPermitted(`${getPermissionPrefixByType(type, id)}${permissionType}:${id}`),
[id, permissionType, isPermitted, type],
);
};

View File

@@ -37,6 +37,7 @@ const typePrefixCornerCasesMap = {
event_definition: 'eventdefinitions:',
notification: 'eventnotifications:',
search: 'view:',
dashboard: 'view:',
};
const standardEntityPermissionsMapper: EntityPermissionsMapper = {