Skip to content

BE: RBAC: Support provider for basic auth #917

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
package io.kafbat.ui.config.auth;

import io.kafbat.ui.model.rbac.Role;
import io.kafbat.ui.model.rbac.provider.Provider;
import io.kafbat.ui.service.rbac.AccessControlService;
import io.kafbat.ui.util.StaticFileWebFilter;
import java.util.Collection;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;

@Configuration
@EnableWebFluxSecurity
@ConditionalOnProperty(value = "auth.type", havingValue = "LOGIN_FORM")
@EnableConfigurationProperties(SecurityProperties.class)
@Slf4j
public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig {
private static final String NOOP_PASSWORD_PREFIX = "{noop}";
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");

@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http) {
Expand All @@ -42,4 +60,42 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http) {
return builder.build();
}

@Bean
public ReactiveUserDetailsService reactiveUserDetailsService(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder,
AccessControlService accessControlService) {
SecurityProperties.User user = properties.getUser();

UserDetails userDetails = User.withUsername(user.getName())
.password(password(user.getPassword(), passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(user.getRoles()))
.build();

Collection<String> groups = accessControlService.getRoles().stream()
.filter(role -> role.getSubjects().stream()
.filter(subj -> Provider.BASIC_AUTH.equals(subj.getProvider()))
.filter(subj -> "user".equals(subj.getType()))
.anyMatch(subj -> user.getName().equals(subj.getValue()))
)
.map(Role::getName)
.collect(Collectors.toSet());

return new RbacUserDetailsService(new RbacBasicAuthUser(userDetails, groups));
}

private String password(String password, PasswordEncoder encoder) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we even need this method, we could delegate this to the encoder which should handle this via encoder.encode(password);

if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
return password;
}

return NOOP_PASSWORD_PREFIX + password;
}

private record RbacUserDetailsService(RbacBasicAuthUser userDetails) implements ReactiveUserDetailsService {
@Override
public Mono<UserDetails> findByUsername(String username) {
return (userDetails.getUsername().equals(username)) ? Mono.just(userDetails) : Mono.empty();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.kafbat.ui.config.auth;

import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class RbacBasicAuthUser implements UserDetails, RbacUser {
private final UserDetails userDetails;
private final Collection<String> groups;

public RbacBasicAuthUser(UserDetails userDetails, Collection<String> groups) {
this.userDetails = userDetails;
this.groups = groups;
}

@Override
public String name() {
return userDetails.getUsername();
}

@Override
public Collection<String> groups() {
return groups;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return userDetails.getAuthorities();
}

@Override
public String getPassword() {
return userDetails.getPassword();
}

@Override
public String getUsername() {
return userDetails.getUsername();
}

@Override
public boolean isAccountNonExpired() {
return userDetails.isAccountNonExpired();
}

@Override
public boolean isAccountNonLocked() {
return userDetails.isAccountNonLocked();
}

@Override
public boolean isCredentialsNonExpired() {
return userDetails.isCredentialsNonExpired();
}

@Override
public boolean isEnabled() {
return userDetails.isEnabled();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ public enum Provider {
OAUTH,

LDAP,
LDAP_AD;
LDAP_AD,

BASIC_AUTH;

@Nullable
public static Provider fromString(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public abstract class AbstractActiveDirectoryIntegrationTest {
private static final String SESSION = "SESSION";

protected static void checkUserPermissions(WebTestClient client) {
AuthenticationInfoDTO info = authenticationInfo(client, FIRST_USER_WITH_GROUP);
AuthenticationInfoDTO info = authenticationInfo(client, FIRST_USER_WITH_GROUP, PASSWORD);

assertNotNull(info);
assertTrue(info.getRbacEnabled());
Expand All @@ -40,25 +40,26 @@ protected static void checkUserPermissions(WebTestClient client) {
assertFalse(permissions.isEmpty());
assertTrue(permissions.stream().anyMatch(permission ->
permission.getClusters().contains(LOCAL) && permission.getResource() == ResourceTypeDTO.TOPIC));
assertEquals(permissions, authenticationInfo(client, SECOND_USER_WITH_GROUP).getUserInfo().getPermissions());
assertEquals(permissions, authenticationInfo(client, USER_WITHOUT_GROUP).getUserInfo().getPermissions());
assertEquals(permissions,
authenticationInfo(client, SECOND_USER_WITH_GROUP, PASSWORD).getUserInfo().getPermissions());
assertEquals(permissions, authenticationInfo(client, USER_WITHOUT_GROUP, PASSWORD).getUserInfo().getPermissions());
}

protected static void checkEmptyPermissions(WebTestClient client) {
assertTrue(Objects.requireNonNull(authenticationInfo(client, EMPTY_PERMISSIONS_USER))
assertTrue(Objects.requireNonNull(authenticationInfo(client, EMPTY_PERMISSIONS_USER, PASSWORD))
.getUserInfo()
.getPermissions()
.isEmpty()
);
}

protected static String session(WebTestClient client, String name) {
private static String session(WebTestClient client, String name, String password) {
return Objects.requireNonNull(
client
.post()
.uri("/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("username", name).with("password", PASSWORD))
.body(BodyInserters.fromFormData("username", name).with("password", password))
.exchange()
.expectStatus()
.isFound()
Expand All @@ -68,11 +69,11 @@ protected static String session(WebTestClient client, String name) {
.getValue();
}

protected static AuthenticationInfoDTO authenticationInfo(WebTestClient client, String name) {
public static AuthenticationInfoDTO authenticationInfo(WebTestClient client, String name, String password) {
return client
.get()
.uri("/api/authorization")
.cookie(SESSION, session(client, name))
.cookie(SESSION, session(client, name, password))
.exchange()
.expectStatus()
.isOk()
Expand Down
50 changes: 50 additions & 0 deletions api/src/test/java/io/kafbat/ui/BasicAuthIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.kafbat.ui;

import static io.kafbat.ui.AbstractActiveDirectoryIntegrationTest.authenticationInfo;
import static io.kafbat.ui.AbstractIntegrationTest.LOCAL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.kafbat.ui.model.AuthenticationInfoDTO;
import io.kafbat.ui.model.ResourceTypeDTO;
import io.kafbat.ui.model.UserPermissionDTO;
import io.kafbat.ui.model.rbac.permission.TopicAction;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest
@ActiveProfiles("rbac-basic-auth")
@AutoConfigureWebTestClient(timeout = "60000")
public class BasicAuthIntegrationTest {
@Autowired
private WebTestClient client;

@Test
void testUserPermissions() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding a test 😄

AuthenticationInfoDTO info = authenticationInfo(client, "admin", "pass");

assertNotNull(info);
assertTrue(info.getRbacEnabled());

List<UserPermissionDTO> permissions = info.getUserInfo().getPermissions();

assertEquals(1, permissions.size());

UserPermissionDTO permission = permissions.getFirst();
Set<TopicAction> actions = permission.getActions().stream()
.map(dto -> TopicAction.valueOf(dto.getValue()))
.collect(Collectors.toSet());

assertTrue(permission.getClusters().contains(LOCAL)
&& permission.getResource() == ResourceTypeDTO.TOPIC
&& actions.equals(Set.of(TopicAction.values())));
}
}
22 changes: 22 additions & 0 deletions api/src/test/resources/application-rbac-basic-auth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
auth:
type: LOGIN_FORM

spring:
security:
user:
name: admin
password: pass

rbac:
roles:
- name: "roleName"
clusters:
- local
subjects:
- provider: basic_auth
type: user
value: admin
permissions:
- resource: topic
value: ".*"
actions: all
Loading