Skip to content

Commit 74a37c7

Browse files
Make security stricter for creation of tokens. (#23333)
* #21856: Make security stricter for creation of tokens. * Add changelog and added section to UPGRADING.md * Also add migration to MigrationsModule --------- Co-authored-by: Patrick Mann <[email protected]>
1 parent 6ad0602 commit 74a37c7

File tree

5 files changed

+198
-0
lines changed

5 files changed

+198
-0
lines changed

UPGRADING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ is now used as the primary color for elements like buttons and badges in the UI.
2828
all existing users with the `Reader` role to ensure backwards compatibility. New users that will be created in the
2929
future need to be explicitly assigned to the `Cluster Configuration Reader` role if they should be able to access the
3030
page.
31+
- Only admins are allowed to create a new API token. Existing tokens are not affected by this change. Also, new tokens
32+
will expire after 30 days by default.
3133

3234
## Java API Changes
3335

changelog/unreleased/pr-23333.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type = "c"
2+
message = "Make default settings for creating new tokens more secure."
3+
4+
pulls = ["23333"]
5+
issues = ["21856"]
6+

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@ protected void configure() {
7676
addMigration(V20250219134200_DefaultTTLForNewTokens.class);
7777
addMigration(V20250506090000_AddInputTypesPermissions.class);
7878
addMigration(V20250721090000_AddClusterConfigurationPermission.class);
79+
addMigration(V20250804104500_TightenTokenSecurity.class);
7980
}
8081
}
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.migrations;
18+
19+
import jakarta.inject.Inject;
20+
import org.graylog2.plugin.cluster.ClusterConfigService;
21+
import org.graylog2.users.UserConfiguration;
22+
23+
import java.time.ZonedDateTime;
24+
import java.util.Objects;
25+
26+
/**
27+
* Updates {@link UserConfiguration} in the DB to make the security for tokens stricter.
28+
*
29+
* A possibly existing {@link UserConfiguration} is updated to restrict creation of tokens to admins and local users only.
30+
* So no regular or externally authenticated user is allowed to create a token. Also, the default time-to-live (TTL) is
31+
* set to 30 days.
32+
*/
33+
public class V20250804104500_TightenTokenSecurity extends Migration {
34+
private final ClusterConfigService configService;
35+
36+
//No breaking changes in minor versions (i.e. access must not be limited for upgrades).
37+
// When it comes to upgrading to 7.0, access should be restricted.
38+
39+
@Inject
40+
public V20250804104500_TightenTokenSecurity(ClusterConfigService configService) {
41+
this.configService = configService;
42+
}
43+
44+
@Override
45+
public ZonedDateTime createdAt() {
46+
return ZonedDateTime.parse("2025-08-04T10:45:00Z");
47+
}
48+
49+
@Override
50+
public void upgrade() {
51+
if (migrationAlreadyApplied()) {
52+
//Migration already ran, nothing more to do.
53+
return;
54+
}
55+
56+
//This migration comes with 7.0, so we're allowed to do breaking changes.
57+
final UserConfiguration newDefaults = UserConfiguration.DEFAULT_VALUES;
58+
59+
UserConfiguration configToUpdate = this.configService.get(UserConfiguration.class);
60+
if (configToUpdate == null) {
61+
//No userConfig exists, let's simply save the default for the current version:
62+
configToUpdate = newDefaults;
63+
} else {
64+
//A UserConfig already exists. We tighten the security when it comes to who is allowed to create a token
65+
// and how long it will be active. The remaining settings stay unchanged:
66+
configToUpdate = UserConfiguration.create(
67+
configToUpdate.enableGlobalSessionTimeout(),
68+
configToUpdate.globalSessionTimeoutInterval(),
69+
newDefaults.allowAccessTokenForExternalUsers(),
70+
newDefaults.restrictAccessTokenToAdmins(),
71+
newDefaults.defaultTTLForNewTokens());
72+
}
73+
74+
configService.write(configToUpdate);
75+
76+
markMigrationApplied();
77+
}
78+
79+
private boolean migrationAlreadyApplied() {
80+
return Objects.nonNull(configService.get(V20250804104500_TightenTokenSecurity.MigrationCompleted.class));
81+
}
82+
83+
private void markMigrationApplied() {
84+
this.configService.write(new V20250804104500_TightenTokenSecurity.MigrationCompleted());
85+
}
86+
87+
public record MigrationCompleted() {}
88+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 org.graylog2.plugin.cluster.ClusterConfigService;
20+
import org.graylog2.users.UserConfiguration;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
import org.mockito.Mock;
25+
import org.mockito.junit.jupiter.MockitoExtension;
26+
import org.threeten.extra.PeriodDuration;
27+
28+
import java.time.Duration;
29+
import java.time.temporal.ChronoUnit;
30+
31+
import static org.mockito.Mockito.verify;
32+
import static org.mockito.Mockito.verifyNoMoreInteractions;
33+
import static org.mockito.Mockito.when;
34+
35+
@ExtendWith(MockitoExtension.class)
36+
class V20250804104500_TightenTokenSecurityTest {
37+
//We prepare some existing config with explicitly updated values, so we can safely check they're not touched by the migration:
38+
private final UserConfiguration existingConfig = UserConfiguration.create(true, Duration.of(10, ChronoUnit.HOURS), true, false, PeriodDuration.of(Duration.ofDays(7)));
39+
40+
@Mock
41+
private ClusterConfigService configService;
42+
43+
private V20250804104500_TightenTokenSecurity testee;
44+
45+
@BeforeEach
46+
void setUp() {
47+
testee = new V20250804104500_TightenTokenSecurity(configService);
48+
}
49+
50+
@Test
51+
void doNothingIfMigrationAlreadyRanSuccessfully() {
52+
setupMocks(true, false);
53+
54+
testee.upgrade();
55+
56+
verify(configService).get(V20250804104500_TightenTokenSecurity.MigrationCompleted.class);
57+
verifyNoMoreInteractions(configService);
58+
}
59+
60+
@Test
61+
void persistDefaultValuesIfNoConfigExists() {
62+
testee = new V20250804104500_TightenTokenSecurity(configService);
63+
setupMocks(false, false);
64+
65+
testee.upgrade();
66+
67+
verify(configService).get(V20250804104500_TightenTokenSecurity.MigrationCompleted.class);
68+
verify(configService).write(UserConfiguration.DEFAULT_VALUES);
69+
verify(configService).write(new V20250804104500_TightenTokenSecurity.MigrationCompleted());
70+
verifyNoMoreInteractions(configService);
71+
}
72+
73+
@Test
74+
void existingConfigIsUpdatedWithStricterValues() {
75+
setupMocks(false, true);
76+
//Expected to be written - keeps existing values for globalSessionTimeout and -interval, but applies default values for token access management
77+
final UserConfiguration updated = UserConfiguration.create(existingConfig.enableGlobalSessionTimeout(),
78+
existingConfig.globalSessionTimeoutInterval(),
79+
UserConfiguration.DEFAULT_VALUES.allowAccessTokenForExternalUsers(),
80+
UserConfiguration.DEFAULT_VALUES.restrictAccessTokenToAdmins(),
81+
UserConfiguration.DEFAULT_VALUES.defaultTTLForNewTokens());
82+
83+
testee.upgrade();
84+
85+
verify(configService).get(V20250804104500_TightenTokenSecurity.MigrationCompleted.class);
86+
verify(configService).get(UserConfiguration.class);
87+
verify(configService).write(updated);
88+
verify(configService).write(new V20250804104500_TightenTokenSecurity.MigrationCompleted());
89+
verifyNoMoreInteractions(configService);
90+
}
91+
92+
93+
private void setupMocks(boolean migrationAlreadyRan, boolean configExists) {
94+
if (migrationAlreadyRan) {
95+
when(configService.get(V20250804104500_TightenTokenSecurity.MigrationCompleted.class)).thenReturn(new V20250804104500_TightenTokenSecurity.MigrationCompleted());
96+
} else {
97+
when(configService.get(V20250804104500_TightenTokenSecurity.MigrationCompleted.class)).thenReturn(null);
98+
when(configService.get(UserConfiguration.class)).thenReturn(configExists ? existingConfig : null);
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)