-
-
Notifications
You must be signed in to change notification settings - Fork 147
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
|
@@ -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) { | ||
Haarolean marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 |
---|---|---|
@@ -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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()))); | ||
} | ||
} |
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 |
Uh oh!
There was an error while loading. Please reload this page.