Skip to content

Commit 4a8fa5a

Browse files
committed
BE: RBAC: Support provider for basic auth
1 parent 3048605 commit 4a8fa5a

File tree

6 files changed

+200
-9
lines changed

6 files changed

+200
-9
lines changed

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

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

3+
import io.kafbat.ui.model.rbac.Role;
4+
import io.kafbat.ui.model.rbac.provider.Provider;
5+
import io.kafbat.ui.service.rbac.AccessControlService;
36
import io.kafbat.ui.util.StaticFileWebFilter;
7+
import java.util.Collection;
8+
import java.util.regex.Pattern;
9+
import java.util.stream.Collectors;
410
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.beans.factory.ObjectProvider;
512
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
13+
import org.springframework.boot.autoconfigure.security.SecurityProperties;
14+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
615
import org.springframework.context.annotation.Bean;
716
import org.springframework.context.annotation.Configuration;
817
import org.springframework.http.HttpMethod;
918
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
1019
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
1120
import org.springframework.security.config.web.server.ServerHttpSecurity;
21+
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
22+
import org.springframework.security.core.userdetails.User;
23+
import org.springframework.security.core.userdetails.UserDetails;
24+
import org.springframework.security.crypto.password.PasswordEncoder;
1225
import org.springframework.security.web.server.SecurityWebFilterChain;
1326
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
27+
import org.springframework.util.StringUtils;
28+
import reactor.core.publisher.Mono;
1429

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

2139
@Bean
2240
public SecurityWebFilterChain configure(ServerHttpSecurity http) {
@@ -42,4 +60,42 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http) {
4260
return builder.build();
4361
}
4462

63+
@Bean
64+
public ReactiveUserDetailsService reactiveUserDetailsService(SecurityProperties properties,
65+
ObjectProvider<PasswordEncoder> passwordEncoder,
66+
AccessControlService accessControlService) {
67+
SecurityProperties.User user = properties.getUser();
68+
69+
UserDetails userDetails = User.withUsername(user.getName())
70+
.password(password(user.getPassword(), passwordEncoder.getIfAvailable()))
71+
.roles(StringUtils.toStringArray(user.getRoles()))
72+
.build();
73+
74+
Collection<String> groups = accessControlService.getRoles().stream()
75+
.filter(role -> role.getSubjects().stream()
76+
.filter(subj -> Provider.BASIC_AUTH.equals(subj.getProvider()))
77+
.filter(subj -> "user".equals(subj.getType()))
78+
.anyMatch(subj -> user.getName().equals(subj.getValue()))
79+
)
80+
.map(Role::getName)
81+
.collect(Collectors.toSet());
82+
83+
return new RbacUserDetailsService(new RbacBasicAuthUser(userDetails, groups));
84+
}
85+
86+
private String password(String password, PasswordEncoder encoder) {
87+
if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
88+
return password;
89+
}
90+
91+
return NOOP_PASSWORD_PREFIX + password;
92+
}
93+
94+
private record RbacUserDetailsService(RbacBasicAuthUser userDetails) implements ReactiveUserDetailsService {
95+
@Override
96+
public Mono<UserDetails> findByUsername(String username) {
97+
return (userDetails.getUsername().equals(username)) ? Mono.just(userDetails) : Mono.empty();
98+
}
99+
}
100+
45101
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.kafbat.ui.config.auth;
2+
3+
import java.util.Collection;
4+
import org.springframework.security.core.GrantedAuthority;
5+
import org.springframework.security.core.userdetails.UserDetails;
6+
7+
public class RbacBasicAuthUser implements UserDetails, RbacUser {
8+
private final UserDetails userDetails;
9+
private final Collection<String> groups;
10+
11+
public RbacBasicAuthUser(UserDetails userDetails, Collection<String> groups) {
12+
this.userDetails = userDetails;
13+
this.groups = groups;
14+
}
15+
16+
@Override
17+
public String name() {
18+
return userDetails.getUsername();
19+
}
20+
21+
@Override
22+
public Collection<String> groups() {
23+
return groups;
24+
}
25+
26+
@Override
27+
public Collection<? extends GrantedAuthority> getAuthorities() {
28+
return userDetails.getAuthorities();
29+
}
30+
31+
@Override
32+
public String getPassword() {
33+
return userDetails.getPassword();
34+
}
35+
36+
@Override
37+
public String getUsername() {
38+
return userDetails.getUsername();
39+
}
40+
41+
@Override
42+
public boolean isAccountNonExpired() {
43+
return userDetails.isAccountNonExpired();
44+
}
45+
46+
@Override
47+
public boolean isAccountNonLocked() {
48+
return userDetails.isAccountNonLocked();
49+
}
50+
51+
@Override
52+
public boolean isCredentialsNonExpired() {
53+
return userDetails.isCredentialsNonExpired();
54+
}
55+
56+
@Override
57+
public boolean isEnabled() {
58+
return userDetails.isEnabled();
59+
}
60+
}

api/src/main/java/io/kafbat/ui/model/rbac/provider/Provider.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public enum Provider {
1313
OAUTH,
1414

1515
LDAP,
16-
LDAP_AD;
16+
LDAP_AD,
17+
18+
BASIC_AUTH;
1719

1820
@Nullable
1921
public static Provider fromString(String name) {

api/src/test/java/io/kafbat/ui/AbstractActiveDirectoryIntegrationTest.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public abstract class AbstractActiveDirectoryIntegrationTest {
3030
private static final String SESSION = "SESSION";
3131

3232
protected static void checkUserPermissions(WebTestClient client) {
33-
AuthenticationInfoDTO info = authenticationInfo(client, FIRST_USER_WITH_GROUP);
33+
AuthenticationInfoDTO info = authenticationInfo(client, FIRST_USER_WITH_GROUP, PASSWORD);
3434

3535
assertNotNull(info);
3636
assertTrue(info.getRbacEnabled());
@@ -40,25 +40,26 @@ protected static void checkUserPermissions(WebTestClient client) {
4040
assertFalse(permissions.isEmpty());
4141
assertTrue(permissions.stream().anyMatch(permission ->
4242
permission.getClusters().contains(LOCAL) && permission.getResource() == ResourceTypeDTO.TOPIC));
43-
assertEquals(permissions, authenticationInfo(client, SECOND_USER_WITH_GROUP).getUserInfo().getPermissions());
44-
assertEquals(permissions, authenticationInfo(client, USER_WITHOUT_GROUP).getUserInfo().getPermissions());
43+
assertEquals(permissions,
44+
authenticationInfo(client, SECOND_USER_WITH_GROUP, PASSWORD).getUserInfo().getPermissions());
45+
assertEquals(permissions, authenticationInfo(client, USER_WITHOUT_GROUP, PASSWORD).getUserInfo().getPermissions());
4546
}
4647

4748
protected static void checkEmptyPermissions(WebTestClient client) {
48-
assertTrue(Objects.requireNonNull(authenticationInfo(client, EMPTY_PERMISSIONS_USER))
49+
assertTrue(Objects.requireNonNull(authenticationInfo(client, EMPTY_PERMISSIONS_USER, PASSWORD))
4950
.getUserInfo()
5051
.getPermissions()
5152
.isEmpty()
5253
);
5354
}
5455

55-
protected static String session(WebTestClient client, String name) {
56+
private static String session(WebTestClient client, String name, String password) {
5657
return Objects.requireNonNull(
5758
client
5859
.post()
5960
.uri("/login")
6061
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
61-
.body(BodyInserters.fromFormData("username", name).with("password", PASSWORD))
62+
.body(BodyInserters.fromFormData("username", name).with("password", password))
6263
.exchange()
6364
.expectStatus()
6465
.isFound()
@@ -68,11 +69,11 @@ protected static String session(WebTestClient client, String name) {
6869
.getValue();
6970
}
7071

71-
protected static AuthenticationInfoDTO authenticationInfo(WebTestClient client, String name) {
72+
public static AuthenticationInfoDTO authenticationInfo(WebTestClient client, String name, String password) {
7273
return client
7374
.get()
7475
.uri("/api/authorization")
75-
.cookie(SESSION, session(client, name))
76+
.cookie(SESSION, session(client, name, password))
7677
.exchange()
7778
.expectStatus()
7879
.isOk()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.kafbat.ui;
2+
3+
import static io.kafbat.ui.AbstractActiveDirectoryIntegrationTest.authenticationInfo;
4+
import static io.kafbat.ui.AbstractIntegrationTest.LOCAL;
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertNotNull;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import io.kafbat.ui.model.AuthenticationInfoDTO;
10+
import io.kafbat.ui.model.ResourceTypeDTO;
11+
import io.kafbat.ui.model.UserPermissionDTO;
12+
import io.kafbat.ui.model.rbac.permission.TopicAction;
13+
import java.util.List;
14+
import java.util.Set;
15+
import java.util.stream.Collectors;
16+
import org.junit.jupiter.api.Test;
17+
import org.springframework.beans.factory.annotation.Autowired;
18+
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
19+
import org.springframework.boot.test.context.SpringBootTest;
20+
import org.springframework.test.context.ActiveProfiles;
21+
import org.springframework.test.web.reactive.server.WebTestClient;
22+
23+
@SpringBootTest
24+
@ActiveProfiles("rbac-basic-auth")
25+
@AutoConfigureWebTestClient(timeout = "60000")
26+
public class BasicAuthIntegrationTest {
27+
@Autowired
28+
private WebTestClient client;
29+
30+
@Test
31+
void testUserPermissions() {
32+
AuthenticationInfoDTO info = authenticationInfo(client, "admin", "pass");
33+
34+
assertNotNull(info);
35+
assertTrue(info.getRbacEnabled());
36+
37+
List<UserPermissionDTO> permissions = info.getUserInfo().getPermissions();
38+
39+
assertEquals(1, permissions.size());
40+
41+
UserPermissionDTO permission = permissions.getFirst();
42+
Set<TopicAction> actions = permission.getActions().stream()
43+
.map(dto -> TopicAction.valueOf(dto.getValue()))
44+
.collect(Collectors.toSet());
45+
46+
assertTrue(permission.getClusters().contains(LOCAL)
47+
&& permission.getResource() == ResourceTypeDTO.TOPIC
48+
&& actions.equals(Set.of(TopicAction.values())));
49+
}
50+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
auth:
2+
type: LOGIN_FORM
3+
4+
spring:
5+
security:
6+
user:
7+
name: admin
8+
password: pass
9+
10+
rbac:
11+
roles:
12+
- name: "roleName"
13+
clusters:
14+
- local
15+
subjects:
16+
- provider: basic_auth
17+
type: user
18+
value: admin
19+
permissions:
20+
- resource: topic
21+
value: ".*"
22+
actions: all

0 commit comments

Comments
 (0)