Skip to content

Commit 98f8a36

Browse files
authored
Merge branch 'main' into custom_timezone
2 parents 0fc63be + ff51a99 commit 98f8a36

File tree

27 files changed

+432
-130
lines changed

27 files changed

+432
-130
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
# BACKEND
5+
gradle/libs.versions.toml @kafbat/backend
56
/build.gradle @kafbat/backend
67
/gradle.properties @kafbat/backend
78
/settings.gradle @kafbat/backend

.github/dependabot.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ updates:
77
interval: weekly
88
time: "10:00"
99
timezone: Europe/London
10-
reviewers:
11-
- "kafbat/backend"
1210
open-pull-requests-limit: 10
1311
labels:
1412
- "type/dependencies"
1513
- "scope/backend"
1614
groups:
17-
gradle-dependencies:
15+
spring-boot-dependencies:
16+
patterns:
17+
- "org.springframework.boot:*"
18+
- "io.spring.dependency-management"
19+
# We will handle major upgrades manually
20+
update-types:
21+
- "patch"
22+
- "minor"
23+
other-dependencies:
1824
patterns:
1925
- "*"
2026
update-types:
@@ -27,8 +33,6 @@ updates:
2733
interval: weekly
2834
time: "10:00"
2935
timezone: Europe/London
30-
reviewers:
31-
- "kafbat/backend"
3236
open-pull-requests-limit: 10
3337
ignore:
3438
- dependency-name: "azul/zulu-openjdk-alpine"
@@ -43,8 +47,6 @@ updates:
4347
interval: weekly
4448
time: "10:00"
4549
timezone: Europe/London
46-
reviewers:
47-
- "kafbat/frontend"
4850
open-pull-requests-limit: 10
4951
versioning-strategy: increase-if-necessary
5052
labels:
@@ -64,8 +66,6 @@ updates:
6466
interval: weekly
6567
time: "10:00"
6668
timezone: Europe/London
67-
reviewers:
68-
- "kafbat/devops"
6969
open-pull-requests-limit: 10
7070
labels:
7171
- "type/dependencies"

api/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ dependencies {
1414
implementation project(":contract")
1515
implementation project(":serde-api")
1616
implementation libs.spring.starter.webflux
17-
implementation libs.spring.starter.security
17+
implementation(libs.spring.starter.security){
18+
exclude group: 'com.nimbusds', module: 'nimbus-jose-jwt' because("Temporary overwrite to fix CVE-2025-53864. See https://avd.aquasec.com/nvd/2025/cve-2025-53864/")
19+
}
20+
implementation(libs.nimbus.jose.jwt){
21+
because("Fixes CVE-2025-5386. See https://avd.aquasec.com/nvd/2025/cve-2025-53864/")
22+
}
1823
implementation libs.spring.starter.actuator
1924
implementation libs.spring.starter.logging
2025
implementation libs.spring.starter.oauth2.client

api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.kafbat.ui.config.auth;
22

3+
import io.kafbat.ui.model.rbac.DefaultRole;
34
import io.kafbat.ui.model.rbac.Role;
5+
import jakarta.annotation.Nullable;
46
import jakarta.annotation.PostConstruct;
57
import java.util.ArrayList;
68
import java.util.List;
@@ -11,13 +13,26 @@ public class RoleBasedAccessControlProperties {
1113

1214
private final List<Role> roles = new ArrayList<>();
1315

16+
private DefaultRole defaultRole;
17+
1418
@PostConstruct
1519
public void init() {
1620
roles.forEach(Role::validate);
21+
if (defaultRole != null) {
22+
defaultRole.validate();
23+
}
1724
}
1825

1926
public List<Role> getRoles() {
2027
return roles;
2128
}
2229

30+
public void setDefaultRole(DefaultRole defaultRole) {
31+
this.defaultRole = defaultRole;
32+
}
33+
34+
@Nullable
35+
public DefaultRole getDefaultRole() {
36+
return defaultRole;
37+
}
2338
}

api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import io.kafbat.ui.api.AuthorizationApi;
44
import io.kafbat.ui.model.ActionDTO;
55
import io.kafbat.ui.model.AuthenticationInfoDTO;
6+
import io.kafbat.ui.model.KafkaCluster;
67
import io.kafbat.ui.model.ResourceTypeDTO;
78
import io.kafbat.ui.model.UserInfoDTO;
89
import io.kafbat.ui.model.UserPermissionDTO;
910
import io.kafbat.ui.model.rbac.Permission;
11+
import io.kafbat.ui.service.ClustersStorage;
1012
import io.kafbat.ui.service.rbac.AccessControlService;
1113
import java.security.Principal;
1214
import java.util.Collection;
@@ -29,8 +31,15 @@
2931
public class AuthorizationController implements AuthorizationApi {
3032

3133
private final AccessControlService accessControlService;
34+
private final ClustersStorage clustersStorage;
3235

3336
public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExchange exchange) {
37+
List<UserPermissionDTO> defaultRolePermissions = accessControlService.getDefaultRole() != null
38+
? mapPermissions(
39+
accessControlService.getDefaultRole().getPermissions(),
40+
clustersStorage.getKafkaClusters().stream().map(KafkaCluster::getName).toList())
41+
: Collections.emptyList();
42+
3443
Mono<List<UserPermissionDTO>> permissions = AccessControlService.getUser()
3544
.map(user -> accessControlService.getRoles()
3645
.stream()
@@ -39,6 +48,8 @@ public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExch
3948
.flatMap(Collection::stream)
4049
.toList()
4150
)
51+
// if no roles are found, return default role permissions
52+
.map(userPermissions -> userPermissions.isEmpty() ? defaultRolePermissions : userPermissions)
4253
.switchIfEmpty(Mono.just(Collections.emptyList()));
4354

4455
Mono<String> userName = ReactiveSecurityContextHolder.getContext()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.kafbat.ui.model.rbac;
2+
3+
import static com.google.common.base.Preconditions.checkArgument;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
import lombok.Data;
8+
9+
@Data
10+
public class DefaultRole {
11+
12+
private List<Permission> permissions = new ArrayList<>();
13+
14+
public void validate() {
15+
permissions.forEach(Permission::validate);
16+
permissions.forEach(Permission::transform);
17+
}
18+
}

api/src/main/java/io/kafbat/ui/model/rbac/Role.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,4 @@ public void validate() {
2121
permissions.forEach(Permission::transform);
2222
subjects.forEach(Subject::validate);
2323
}
24-
2524
}

api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.kafbat.ui.model.ConnectDTO;
88
import io.kafbat.ui.model.InternalTopic;
99
import io.kafbat.ui.model.rbac.AccessContext;
10+
import io.kafbat.ui.model.rbac.DefaultRole;
1011
import io.kafbat.ui.model.rbac.Permission;
1112
import io.kafbat.ui.model.rbac.Role;
1213
import io.kafbat.ui.model.rbac.Subject;
@@ -62,7 +63,7 @@ public class AccessControlService {
6263

6364
@PostConstruct
6465
public void init() {
65-
if (CollectionUtils.isEmpty(properties.getRoles())) {
66+
if (CollectionUtils.isEmpty(properties.getRoles()) && properties.getDefaultRole() == null) {
6667
log.trace("No roles provided, disabling RBAC");
6768
return;
6869
}
@@ -86,7 +87,8 @@ public void init() {
8687
.flatMap(Set::stream)
8788
.collect(Collectors.toSet());
8889

89-
if (!properties.getRoles().isEmpty()
90+
boolean hasRolesConfigured = !properties.getRoles().isEmpty() || properties.getDefaultRole() != null;
91+
if (hasRolesConfigured
9092
&& "oauth2".equalsIgnoreCase(environment.getProperty("auth.type"))
9193
&& (clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())) {
9294
log.error("Roles are configured but no authentication methods are present. Authentication might fail.");
@@ -114,12 +116,20 @@ private boolean isAccessible(AuthenticatedUser user, AccessContext context) {
114116
}
115117

116118
private List<Permission> getUserPermissions(AuthenticatedUser user, @Nullable String clusterName) {
117-
return properties.getRoles()
118-
.stream()
119-
.filter(filterRole(user))
120-
.filter(role -> clusterName == null || role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase))
121-
.flatMap(role -> role.getPermissions().stream())
122-
.toList();
119+
List<Role> filteredRoles = properties.getRoles()
120+
.stream()
121+
.filter(filterRole(user))
122+
.filter(role -> clusterName == null || role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase))
123+
.toList();
124+
125+
// if no roles are found, check if default role is set
126+
if (filteredRoles.isEmpty() && properties.getDefaultRole() != null) {
127+
return properties.getDefaultRole().getPermissions();
128+
}
129+
130+
return filteredRoles.stream()
131+
.flatMap(role -> role.getPermissions().stream())
132+
.toList();
123133
}
124134

125135
public static Mono<AuthenticatedUser> getUser() {
@@ -132,10 +142,12 @@ public static Mono<AuthenticatedUser> getUser() {
132142

133143
private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) {
134144
Assert.isTrue(StringUtils.isNotEmpty(clusterName), "cluster value is empty");
135-
return properties.getRoles()
145+
boolean isAccessible = properties.getRoles()
136146
.stream()
137147
.filter(filterRole(user))
138148
.anyMatch(role -> role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase));
149+
150+
return isAccessible || properties.getDefaultRole() != null;
139151
}
140152

141153
public Mono<Boolean> isClusterAccessible(ClusterDTO cluster) {
@@ -200,6 +212,10 @@ public List<Role> getRoles() {
200212
return Collections.unmodifiableList(properties.getRoles());
201213
}
202214

215+
public DefaultRole getDefaultRole() {
216+
return properties.getDefaultRole();
217+
}
218+
203219
private Predicate<Role> filterRole(AuthenticatedUser user) {
204220
return role -> user.groups().contains(role.getName());
205221
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package io.kafbat.ui.service.rbac;
2+
3+
import static io.kafbat.ui.service.rbac.MockedRbacUtils.DEFAULT_ROLE;
4+
import static io.kafbat.ui.service.rbac.MockedRbacUtils.PROD_CLUSTER;
5+
import static io.kafbat.ui.service.rbac.MockedRbacUtils.getAccessContext;
6+
import static org.mockito.Mockito.mock;
7+
import static org.mockito.Mockito.when;
8+
9+
import io.kafbat.ui.AbstractIntegrationTest;
10+
import io.kafbat.ui.config.auth.RbacUser;
11+
import io.kafbat.ui.config.auth.RoleBasedAccessControlProperties;
12+
import io.kafbat.ui.model.ClusterDTO;
13+
import io.kafbat.ui.model.rbac.AccessContext;
14+
import io.kafbat.ui.model.rbac.DefaultRole;
15+
import java.util.List;
16+
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.Test;
18+
import org.mockito.Mock;
19+
import org.mockito.MockedStatic;
20+
import org.mockito.Mockito;
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.security.core.Authentication;
23+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
24+
import org.springframework.security.core.context.SecurityContext;
25+
import org.springframework.test.annotation.DirtiesContext;
26+
import org.springframework.test.util.ReflectionTestUtils;
27+
import reactor.core.publisher.Mono;
28+
import reactor.test.StepVerifier;
29+
30+
31+
/**
32+
* Test class for AccessControlService with default role and RBAC enabled.
33+
*/
34+
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
35+
public class AccessControlServiceDefaultRoleRbacEnabledTest extends AbstractIntegrationTest {
36+
37+
@Autowired
38+
AccessControlService accessControlService;
39+
40+
@Mock
41+
SecurityContext securityContext;
42+
43+
@Mock
44+
Authentication authentication;
45+
46+
@Mock
47+
RbacUser user;
48+
49+
@Mock
50+
DefaultRole defaultRole;
51+
52+
@BeforeEach
53+
void setUp() {
54+
55+
RoleBasedAccessControlProperties properties = mock();
56+
defaultRole = MockedRbacUtils.getDefaultRole();
57+
when(properties.getDefaultRole()).thenReturn(defaultRole);
58+
when(properties.getRoles()).thenReturn(List.of()); // Return empty list for roles
59+
60+
61+
ReflectionTestUtils.setField(accessControlService, "properties", properties);
62+
ReflectionTestUtils.setField(accessControlService, "rbacEnabled", true);
63+
64+
// Mock security context
65+
when(securityContext.getAuthentication()).thenReturn(authentication);
66+
when(authentication.getPrincipal()).thenReturn(user);
67+
}
68+
69+
public void withSecurityContext(Runnable runnable) {
70+
try (MockedStatic<ReactiveSecurityContextHolder> ctxHolder = Mockito.mockStatic(
71+
ReactiveSecurityContextHolder.class)) {
72+
// Mock static method to get security context
73+
ctxHolder.when(ReactiveSecurityContextHolder::getContext).thenReturn(Mono.just(securityContext));
74+
runnable.run();
75+
}
76+
}
77+
78+
@Test
79+
void validateAccess() {
80+
withSecurityContext(() -> {
81+
when(user.groups()).thenReturn(List.of(DEFAULT_ROLE));
82+
AccessContext context = getAccessContext(PROD_CLUSTER, true);
83+
Mono<Void> validateAccessMono = accessControlService.validateAccess(context);
84+
StepVerifier.create(validateAccessMono)
85+
.expectComplete()
86+
.verify();
87+
});
88+
}
89+
90+
@Test
91+
void isClusterAccessible() {
92+
withSecurityContext(() -> {
93+
ClusterDTO clusterDto = new ClusterDTO();
94+
clusterDto.setName(PROD_CLUSTER);
95+
Mono<Boolean> clusterAccessibleMono = accessControlService.isClusterAccessible(clusterDto);
96+
StepVerifier.create(clusterAccessibleMono)
97+
.expectNext(true)
98+
.expectComplete()
99+
.verify();
100+
});
101+
}
102+
}

0 commit comments

Comments
 (0)