Skip to content

Commit b3f94bb

Browse files
moesterheldtodvora
andauthored
Add new Cluster Configuration Reader role (#23248)
* add new Cluster Configuration Reader role * cl --------- Co-authored-by: Tomas Dvorak <[email protected]>
1 parent a3e0878 commit b3f94bb

File tree

7 files changed

+239
-4
lines changed

7 files changed

+239
-4
lines changed

UPGRADING.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ can only talk to Kafka brokers with version 2.1 or newer.
1717

1818
## Default Configuration Changes
1919

20-
- tbd
20+
- The permission to view the "Cluster Configuration" page was removed from the `Reader` role. This permission is now
21+
available with the `Cluster Configuration Reader` role. There is an automatic one-time migration to add this role to
22+
all existing users with the `Reader` role to ensure backwards compatibility. New users that will be created in the
23+
future need to be explicitly assigned to the `Cluster Configuration Reader` role if they should be able to access the
24+
page.
2125

2226
## Java API Changes
2327

changelog/unreleased/pr-23248.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type = "a"
2+
message = "Change default visibility of Cluster Configuration page to users with new role Cluster Configuration Reader."
3+
4+
issues = ["graylog-plugin-enterprise#10605"]
5+
pulls = ["23248"]

graylog2-server/src/main/java/org/graylog2/migrations/MigrationsModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,6 @@ protected void configure() {
7575
addMigration(V20250206105400_TokenManagementConfiguration.class);
7676
addMigration(V20250219134200_DefaultTTLForNewTokens.class);
7777
addMigration(V20250506090000_AddInputTypesPermissions.class);
78+
addMigration(V20250721090000_AddClusterConfigurationPermission.class);
7879
}
7980
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.migrations;
18+
19+
import jakarta.inject.Inject;
20+
import org.graylog2.database.NotFoundException;
21+
import org.graylog2.plugin.cluster.ClusterConfigService;
22+
import org.graylog2.plugin.database.ValidationException;
23+
import org.graylog2.shared.users.Role;
24+
import org.graylog2.shared.users.UserService;
25+
import org.graylog2.users.RoleService;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
import java.time.ZonedDateTime;
30+
import java.util.Set;
31+
32+
public class V20250721090000_AddClusterConfigurationPermission extends Migration {
33+
34+
private static final Logger LOG = LoggerFactory.getLogger(V20250721090000_AddClusterConfigurationPermission.class);
35+
36+
static final String CLUSTER_CONFIGURATION_READER_ROLE = "Cluster Configuration Reader";
37+
38+
private final ClusterConfigService clusterConfigService;
39+
private final RoleService roleService;
40+
private final UserService userService;
41+
42+
@Inject
43+
public V20250721090000_AddClusterConfigurationPermission(final ClusterConfigService clusterConfigService,
44+
RoleService roleService, UserService userService) {
45+
this.clusterConfigService = clusterConfigService;
46+
this.roleService = roleService;
47+
this.userService = userService;
48+
}
49+
50+
@Override
51+
public ZonedDateTime createdAt() {
52+
return ZonedDateTime.parse("2025-07-21T09:00:00Z");
53+
}
54+
55+
@Override
56+
public void upgrade() {
57+
if (clusterConfigService.get(V20250721090000_AddClusterConfigurationPermission.MigrationCompleted.class) != null) {
58+
LOG.debug("Migration already completed.");
59+
return;
60+
}
61+
LOG.debug("Starting migration to add cluster configuration reader role to users with reader role.");
62+
63+
try {
64+
Role readerRole = roleService.loadById(roleService.getReaderRoleObjectId());
65+
Role clusterConfigurationReaderRole = roleService.load(CLUSTER_CONFIGURATION_READER_ROLE);
66+
67+
userService.loadAllForRole(readerRole).stream()
68+
.peek(user -> {
69+
Set<String> roleIds = user.getRoleIds();
70+
roleIds.add(clusterConfigurationReaderRole.getId());
71+
user.setRoleIds(roleIds);
72+
}).forEach(user -> {
73+
LOG.debug("Updating user {} with new cluster configuration reader role", user.getName());
74+
try {
75+
userService.save(user);
76+
} catch (ValidationException e) {
77+
LOG.error("Error updating user.", e);
78+
}
79+
});
80+
81+
82+
} catch (NotFoundException e) {
83+
LOG.error("Built-in role not found. Cannot add cluster configuration role to users with reader role: {}", e.getMessage());
84+
} finally {
85+
clusterConfigService.write(new V20250721090000_AddClusterConfigurationPermission.MigrationCompleted());
86+
}
87+
88+
}
89+
90+
public record MigrationCompleted() {
91+
}
92+
}

graylog2-server/src/main/java/org/graylog2/shared/security/RestPermissions.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class RestPermissions implements PluginPermissions {
5151
public static final String CLUSTER_CONFIG_ENTRY_EDIT = "clusterconfigentry:edit";
5252
public static final String CLUSTER_CONFIG_ENTRY_READ = "clusterconfigentry:read";
5353
public static final String CAPABILITIES_READ = "capabilities:read";
54+
public static final String CLUSTER_CONFIGURATION_READ = "clusterconfiguration:read";
5455
public static final String CONTENT_PACK_CREATE = "contentpack:create";
5556
public static final String CONTENT_PACK_DELETE = "contentpack:delete";
5657
public static final String CONTENT_PACK_READ = "contentpack:read";
@@ -212,6 +213,7 @@ public class RestPermissions implements PluginPermissions {
212213
.add(create(CLUSTER_CONFIG_ENTRY_DELETE, ""))
213214
.add(create(CLUSTER_CONFIG_ENTRY_EDIT, ""))
214215
.add(create(CLUSTER_CONFIG_ENTRY_READ, ""))
216+
.add(create(CLUSTER_CONFIGURATION_READ, ""))
215217
.add(create(DASHBOARDS_CREATE, ""))
216218
.add(create(DASHBOARDS_EDIT, "").withManageCapabilityFor(GRNTypes.DASHBOARD))
217219
.add(create(DASHBOARDS_READ, "").withViewCapabilityFor(GRNTypes.DASHBOARD))
@@ -378,6 +380,9 @@ public class RestPermissions implements PluginPermissions {
378380
)),
379381
BuiltinRole.create("User Inspector", "Allows listing all user accounts (built-in)", ImmutableSet.of(
380382
RestPermissions.USERS_READ, RestPermissions.USERS_LIST
383+
)),
384+
BuiltinRole.create("Cluster Configuration Reader", "Allows viewing the Cluster Configuration page", ImmutableSet.of(
385+
RestPermissions.CLUSTER_CONFIGURATION_READ
381386
))
382387
).build();
383388

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
18+
package org.graylog2.migrations;
19+
20+
import org.graylog2.database.NotFoundException;
21+
import org.graylog2.plugin.cluster.ClusterConfigService;
22+
import org.graylog2.plugin.database.ValidationException;
23+
import org.graylog2.plugin.database.users.User;
24+
import org.graylog2.shared.users.Role;
25+
import org.graylog2.shared.users.UserService;
26+
import org.graylog2.users.RoleService;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.extension.ExtendWith;
30+
import org.mockito.Mock;
31+
import org.mockito.junit.jupiter.MockitoExtension;
32+
33+
import java.util.HashSet;
34+
import java.util.List;
35+
import java.util.Set;
36+
import java.util.UUID;
37+
38+
import static org.mockito.ArgumentMatchers.any;
39+
import static org.mockito.Mockito.ignoreStubs;
40+
import static org.mockito.Mockito.mock;
41+
import static org.mockito.Mockito.times;
42+
import static org.mockito.Mockito.verify;
43+
import static org.mockito.Mockito.verifyNoInteractions;
44+
import static org.mockito.Mockito.verifyNoMoreInteractions;
45+
import static org.mockito.Mockito.when;
46+
47+
@ExtendWith(MockitoExtension.class)
48+
public class V20250721090000_AddClusterConfigurationPermissionTest {
49+
50+
@Mock
51+
ClusterConfigService clusterConfigService;
52+
@Mock
53+
RoleService roleService;
54+
@Mock
55+
UserService userService;
56+
57+
V20250721090000_AddClusterConfigurationPermission migration;
58+
59+
@BeforeEach
60+
public void setUp() {
61+
when(clusterConfigService.get(V20250721090000_AddClusterConfigurationPermission.MigrationCompleted.class))
62+
.thenReturn(null);
63+
migration = new V20250721090000_AddClusterConfigurationPermission(clusterConfigService, roleService, userService);
64+
}
65+
66+
@Test
67+
public void nothingMigratedIfReaderRoleNotFound() throws NotFoundException {
68+
String readerId = UUID.randomUUID().toString();
69+
when(roleService.getReaderRoleObjectId()).thenReturn(readerId);
70+
when(roleService.loadById(readerId)).thenThrow(NotFoundException.class);
71+
migration.upgrade();
72+
verifyNoInteractions(userService);
73+
verify(clusterConfigService, times(1)).write(any());
74+
}
75+
76+
@Test
77+
public void nothingMigratedIfClusterConfigurationReaderRoleNotFound() throws NotFoundException {
78+
String readerId = UUID.randomUUID().toString();
79+
when(roleService.getReaderRoleObjectId()).thenReturn(readerId);
80+
when(roleService.loadById(readerId)).thenReturn(mock(Role.class));
81+
when(roleService.load(V20250721090000_AddClusterConfigurationPermission.CLUSTER_CONFIGURATION_READER_ROLE))
82+
.thenThrow(NotFoundException.class);
83+
migration.upgrade();
84+
verifyNoInteractions(userService);
85+
verify(clusterConfigService, times(1)).write(any());
86+
}
87+
88+
@Test
89+
public void roleAddedToAllUsersWithReaderRole() throws NotFoundException, ValidationException {
90+
String readerId = UUID.randomUUID().toString();
91+
String clusterConfigurationReaderRoleId = UUID.randomUUID().toString();
92+
when(roleService.getReaderRoleObjectId()).thenReturn(readerId);
93+
Role readerRole = mock(Role.class);
94+
when(roleService.loadById(readerId)).thenReturn(readerRole);
95+
Role clusterConfigurationReaderRole = mock(Role.class);
96+
when(clusterConfigurationReaderRole.getId()).thenReturn(clusterConfigurationReaderRoleId);
97+
when(roleService.load(V20250721090000_AddClusterConfigurationPermission.CLUSTER_CONFIGURATION_READER_ROLE))
98+
.thenReturn(clusterConfigurationReaderRole);
99+
100+
User user1 = mock(User.class);
101+
when(user1.getRoleIds()).thenReturn(new HashSet<>(List.of(readerId)));
102+
User user2 = mock(User.class);
103+
when(user2.getRoleIds()).thenReturn(new HashSet<>(List.of(readerId)));
104+
105+
when(userService.loadAllForRole(readerRole)).thenReturn(List.of(user1, user2));
106+
107+
migration.upgrade();
108+
109+
verify(user1, times(1)).setRoleIds(Set.of(readerId, clusterConfigurationReaderRoleId));
110+
verify(user2, times(1)).setRoleIds(Set.of(readerId, clusterConfigurationReaderRoleId));
111+
verify(userService, times(2)).save(any());
112+
verify(clusterConfigService, times(1)).write(any());
113+
}
114+
115+
@Test
116+
public void migrationRunsOnlyOnce() {
117+
when(clusterConfigService.get(V20250721090000_AddClusterConfigurationPermission.MigrationCompleted.class))
118+
.thenReturn(new V20250721090000_AddClusterConfigurationPermission.MigrationCompleted());
119+
migration.upgrade();
120+
verifyNoMoreInteractions(ignoreStubs(clusterConfigService));
121+
verifyNoInteractions(roleService, userService);
122+
}
123+
124+
}

graylog2-web-interface/src/components/navigation/bindings.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
* <http://www.mongodb.com/licensing/server-side-public-license>.
1616
*/
1717

18-
import type { PluginExports } from 'graylog-web-plugin/plugin';
18+
import type {PluginExports} from 'graylog-web-plugin/plugin';
1919

2020
import Routes from 'routing/Routes';
21-
import filterMenuItems, { filterCloudMenuItems } from 'util/conditional/filterMenuItems';
21+
import filterMenuItems, {filterCloudMenuItems} from 'util/conditional/filterMenuItems';
2222
import AppConfig from 'util/AppConfig';
2323

2424
export const SYSTEM_DROPDOWN_TITLE = 'System';
@@ -55,7 +55,11 @@ const navigationBindings: PluginExports = {
5555
description: 'Configurations',
5656
permissions: ['clusterconfigentry:read'],
5757
},
58-
{ path: Routes.SYSTEM.CLUSTER.NODES, description: 'Cluster Configuration', permissions: ['datanode:read'] },
58+
{
59+
path: Routes.SYSTEM.CLUSTER.NODES,
60+
description: 'Cluster Configuration',
61+
permissions: ['clusterconfiguration:read'],
62+
},
5963
{ path: Routes.SYSTEM.INPUTS, description: 'Inputs', permissions: ['inputs:read'] },
6064
{ path: Routes.SYSTEM.OUTPUTS, description: 'Outputs', permissions: ['outputs:read'] },
6165
{ path: Routes.SYSTEM.INDICES.LIST, description: 'Indices', permissions: ['indices:read'] },

0 commit comments

Comments
 (0)