Skip to content

BE: RBAC: Impl default role #1056

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.kafbat.ui.config.auth;

import io.kafbat.ui.model.rbac.DefaultRole;
import io.kafbat.ui.model.rbac.Role;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -11,13 +13,26 @@ public class RoleBasedAccessControlProperties {

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

private DefaultRole defaultRole;

@PostConstruct
public void init() {
roles.forEach(Role::validate);
if (defaultRole != null) {
defaultRole.validate();
}
}

public List<Role> getRoles() {
return roles;
}

public void setDefaultRole(DefaultRole defaultRole) {
this.defaultRole = defaultRole;
}

@Nullable
public DefaultRole getDefaultRole() {
return defaultRole;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import io.kafbat.ui.api.AuthorizationApi;
import io.kafbat.ui.model.ActionDTO;
import io.kafbat.ui.model.AuthenticationInfoDTO;
import io.kafbat.ui.model.KafkaCluster;
import io.kafbat.ui.model.ResourceTypeDTO;
import io.kafbat.ui.model.UserInfoDTO;
import io.kafbat.ui.model.UserPermissionDTO;
import io.kafbat.ui.model.rbac.Permission;
import io.kafbat.ui.service.ClustersStorage;
import io.kafbat.ui.service.rbac.AccessControlService;
import java.security.Principal;
import java.util.Collection;
Expand All @@ -29,8 +31,15 @@
public class AuthorizationController implements AuthorizationApi {

private final AccessControlService accessControlService;
private final ClustersStorage clustersStorage;

public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExchange exchange) {
List<UserPermissionDTO> defaultRolePermissions = accessControlService.getDefaultRole() != null
? mapPermissions(
accessControlService.getDefaultRole().getPermissions(),
clustersStorage.getKafkaClusters().stream().map(KafkaCluster::getName).toList())
: Collections.emptyList();

Mono<List<UserPermissionDTO>> permissions = AccessControlService.getUser()
.map(user -> accessControlService.getRoles()
.stream()
Expand All @@ -39,6 +48,8 @@ public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExch
.flatMap(Collection::stream)
.toList()
)
// if no roles are found, return default role permissions
.map(userPermissions -> userPermissions.isEmpty() ? defaultRolePermissions : userPermissions)
.switchIfEmpty(Mono.just(Collections.emptyList()));

Mono<String> userName = ReactiveSecurityContextHolder.getContext()
Expand Down
18 changes: 18 additions & 0 deletions api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.kafbat.ui.model.rbac;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.ArrayList;
import java.util.List;
import lombok.Data;

@Data
public class DefaultRole {

private List<Permission> permissions = new ArrayList<>();

public void validate() {
permissions.forEach(Permission::validate);
permissions.forEach(Permission::transform);
}
}
1 change: 0 additions & 1 deletion api/src/main/java/io/kafbat/ui/model/rbac/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,4 @@ public void validate() {
permissions.forEach(Permission::transform);
subjects.forEach(Subject::validate);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.kafbat.ui.model.ConnectDTO;
import io.kafbat.ui.model.InternalTopic;
import io.kafbat.ui.model.rbac.AccessContext;
import io.kafbat.ui.model.rbac.DefaultRole;
import io.kafbat.ui.model.rbac.Permission;
import io.kafbat.ui.model.rbac.Role;
import io.kafbat.ui.model.rbac.Subject;
Expand Down Expand Up @@ -62,7 +63,7 @@ public class AccessControlService {

@PostConstruct
public void init() {
if (CollectionUtils.isEmpty(properties.getRoles())) {
if (CollectionUtils.isEmpty(properties.getRoles()) && properties.getDefaultRole() == null) {
log.trace("No roles provided, disabling RBAC");
return;
}
Expand All @@ -86,7 +87,7 @@ public void init() {
.flatMap(Set::stream)
.collect(Collectors.toSet());

if (!properties.getRoles().isEmpty()
if (!(properties.getRoles().isEmpty() && properties.getDefaultRole() == null)
&& "oauth2".equalsIgnoreCase(environment.getProperty("auth.type"))
&& (clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())) {
log.error("Roles are configured but no authentication methods are present. Authentication might fail.");
Expand Down Expand Up @@ -114,12 +115,20 @@ private boolean isAccessible(AuthenticatedUser user, AccessContext context) {
}

private List<Permission> getUserPermissions(AuthenticatedUser user, @Nullable String clusterName) {
return properties.getRoles()
.stream()
.filter(filterRole(user))
.filter(role -> clusterName == null || role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase))
.flatMap(role -> role.getPermissions().stream())
.toList();
List<Role> filteredRoles = properties.getRoles()
.stream()
.filter(filterRole(user))
.filter(role -> clusterName == null || role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase))
.toList();

// if no roles are found, check if default role is set
if (filteredRoles.isEmpty() && properties.getDefaultRole() != null) {
return properties.getDefaultRole().getPermissions();
}

return filteredRoles.stream()
.flatMap(role -> role.getPermissions().stream())
.toList();
}

public static Mono<AuthenticatedUser> getUser() {
Expand All @@ -132,10 +141,12 @@ public static Mono<AuthenticatedUser> getUser() {

private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) {
Assert.isTrue(StringUtils.isNotEmpty(clusterName), "cluster value is empty");
return properties.getRoles()
boolean isAccessible = properties.getRoles()
.stream()
.filter(filterRole(user))
.anyMatch(role -> role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase));

return isAccessible || properties.getDefaultRole() != null;
}

public Mono<Boolean> isClusterAccessible(ClusterDTO cluster) {
Expand Down Expand Up @@ -200,6 +211,10 @@ public List<Role> getRoles() {
return Collections.unmodifiableList(properties.getRoles());
}

public DefaultRole getDefaultRole() {
return properties.getDefaultRole();
}

private Predicate<Role> filterRole(AuthenticatedUser user) {
return role -> user.groups().contains(role.getName());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.kafbat.ui.service.rbac;

import static io.kafbat.ui.service.rbac.MockedRbacUtils.DEFAULT_ROLE;
import static io.kafbat.ui.service.rbac.MockedRbacUtils.PROD_CLUSTER;
import static io.kafbat.ui.service.rbac.MockedRbacUtils.getAccessContext;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import io.kafbat.ui.AbstractIntegrationTest;
import io.kafbat.ui.config.auth.RbacUser;
import io.kafbat.ui.config.auth.RoleBasedAccessControlProperties;
import io.kafbat.ui.model.ClusterDTO;
import io.kafbat.ui.model.rbac.AccessContext;
import io.kafbat.ui.model.rbac.DefaultRole;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.util.ReflectionTestUtils;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;


/**
* Test class for AccessControlService with default role and RBAC enabled.
*/
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class AccessControlServiceDefaultRoleRbacEnabledTest extends AbstractIntegrationTest {

@Autowired
AccessControlService accessControlService;

@Mock
SecurityContext securityContext;

@Mock
Authentication authentication;

@Mock
RbacUser user;

@Mock
DefaultRole defaultRole;

@BeforeEach
void setUp() {

RoleBasedAccessControlProperties properties = mock();
defaultRole = MockedRbacUtils.getDefaultRole();
when(properties.getDefaultRole()).thenReturn(defaultRole);
when(properties.getRoles()).thenReturn(List.of()); // Return empty list for roles


ReflectionTestUtils.setField(accessControlService, "properties", properties);
ReflectionTestUtils.setField(accessControlService, "rbacEnabled", true);

// Mock security context
when(securityContext.getAuthentication()).thenReturn(authentication);
when(authentication.getPrincipal()).thenReturn(user);
}

public void withSecurityContext(Runnable runnable) {
try (MockedStatic<ReactiveSecurityContextHolder> ctxHolder = Mockito.mockStatic(
ReactiveSecurityContextHolder.class)) {
// Mock static method to get security context
ctxHolder.when(ReactiveSecurityContextHolder::getContext).thenReturn(Mono.just(securityContext));
runnable.run();
}
}

@Test
void validateAccess() {
withSecurityContext(() -> {
when(user.groups()).thenReturn(List.of(DEFAULT_ROLE));
AccessContext context = getAccessContext(PROD_CLUSTER, true);
Mono<Void> validateAccessMono = accessControlService.validateAccess(context);
StepVerifier.create(validateAccessMono)
.expectComplete()
.verify();
});
}

@Test
void isClusterAccessible() {
withSecurityContext(() -> {
ClusterDTO clusterDto = new ClusterDTO();
clusterDto.setName(PROD_CLUSTER);
Mono<Boolean> clusterAccessibleMono = accessControlService.isClusterAccessible(clusterDto);
StepVerifier.create(clusterAccessibleMono)
.expectNext(true)
.expectComplete()
.verify();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.mockito.Mockito.when;

import io.kafbat.ui.model.rbac.AccessContext;
import io.kafbat.ui.model.rbac.DefaultRole;
import io.kafbat.ui.model.rbac.Permission;
import io.kafbat.ui.model.rbac.Resource;
import io.kafbat.ui.model.rbac.Role;
Expand All @@ -20,6 +21,7 @@ public class MockedRbacUtils {

public static final String ADMIN_ROLE = "admin_role";
public static final String DEV_ROLE = "dev_role";
public static final String DEFAULT_ROLE = "default_role";

public static final String PROD_CLUSTER = "prod";
public static final String DEV_CLUSTER = "dev";
Expand Down Expand Up @@ -99,6 +101,39 @@ public static Role getDevRole() {
return role;
}

public static DefaultRole getDefaultRole() {
Permission topicViewPermission = new Permission();
topicViewPermission.setResource(Resource.TOPIC.name());
topicViewPermission.setActions(List.of(TopicAction.VIEW.name()));
topicViewPermission.setValue(TOPIC_NAME);

Permission consumerGroupPermission = new Permission();
consumerGroupPermission.setResource(Resource.CONSUMER.name());
consumerGroupPermission.setActions(List.of(ConsumerGroupAction.VIEW.name()));
consumerGroupPermission.setValue(CONSUMER_GROUP_NAME);

Permission schemaPermission = new Permission();
schemaPermission.setResource(Resource.SCHEMA.name());
schemaPermission.setActions(List.of(SchemaAction.VIEW.name()));
schemaPermission.setValue(SCHEMA_NAME);

Permission connectPermission = new Permission();
connectPermission.setResource(Resource.CONNECT.name());
connectPermission.setActions(List.of(ConnectAction.VIEW.name()));
connectPermission.setValue(CONNECT_NAME);

List<Permission> permissions = List.of(
topicViewPermission,
consumerGroupPermission,
schemaPermission,
connectPermission
);
DefaultRole role = new DefaultRole();
role.setPermissions(permissions);
role.validate();
return role;
}

public static AccessContext getAccessContext(String cluster, Boolean resourceAccessible) {
AccessContext.ResourceAccess mockedResource = mock(AccessContext.ResourceAccess.class);
when(mockedResource.isAccessible(any())).thenReturn(resourceAccessible);
Expand Down
Loading