Skip to content
New issue

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

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

Already on GitHub? # to your account

BE: RBAC: Subject type/value is unintended to be optional #719

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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,8 +1,7 @@
package io.kafbat.ui.config.auth;

import static io.kafbat.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST;

import io.kafbat.ui.service.rbac.AccessControlService;
import io.kafbat.ui.service.rbac.extractor.RbacActiveDirectoryAuthoritiesExtractor;
import io.kafbat.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
import java.util.Collection;
import java.util.List;
Expand Down Expand Up @@ -43,7 +42,7 @@
@EnableConfigurationProperties(LdapProperties.class)
@RequiredArgsConstructor
@Slf4j
public class LdapSecurityConfig {
public class LdapSecurityConfig extends AbstractAuthSecurityConfig {

private final LdapProperties props;

Expand All @@ -63,24 +62,39 @@ public ReactiveAuthenticationManager authenticationManager(LdapContextSource lda
ba.setUserSearch(userSearch);
}

AuthenticationManager manager = new ProviderManager(List.of(
Copy link
Member

Choose a reason for hiding this comment

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

Let's not mix the distinct issues in one PR, please. #54 is to be solved within #717 (which was planned as more of a refactoring PR first, but, welp, it's another story).

#716 has nothing to do with LDAP in particular, we'd need RBAC subject validation. In io.kafbat.ui.model.rbac.Permission and Role classes there's a validate method, perhaps implementing one for Subject as well is the way here.

Copy link
Contributor Author

@wernerdv wernerdv Dec 25, 2024

Choose a reason for hiding this comment

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

Maybe we can merge this PR as is for the task fix #54 and I'll do the RBAC subject validation (#716) in a separate PR, what do you say?

Copy link
Member

Choose a reason for hiding this comment

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

Let's rather not. This is not quite the changes we need to resolve #54, I've already started implementing the required adjustments in #717

authenticationProvider(authoritiesExtractor, rbacEnabled, ba)
));

return new ReactiveAuthenticationManagerAdapter(manager);
}

private AbstractLdapAuthenticationProvider authenticationProvider(LdapAuthoritiesPopulator authoritiesExtractor,
boolean rbacEnabled,
BindAuthenticator bindAuthenticator) {
AbstractLdapAuthenticationProvider authenticationProvider;

if (!props.isActiveDirectory()) {
authenticationProvider = rbacEnabled
? new LdapAuthenticationProvider(ba, authoritiesExtractor)
: new LdapAuthenticationProvider(ba);
? new LdapAuthenticationProvider(bindAuthenticator, authoritiesExtractor)
: new LdapAuthenticationProvider(bindAuthenticator);
} else {
authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
props.getUrls()); // TODO Issue #3741
authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(
props.getActiveDirectoryDomain(), props.getUrls()
);
authenticationProvider.setUseAuthenticationRequestCredentials(true);

if (rbacEnabled) {
((ActiveDirectoryLdapAuthenticationProvider) authenticationProvider)
.setAuthoritiesPopulator(authoritiesExtractor);
}
}

if (rbacEnabled) {
authenticationProvider.setUserDetailsContextMapper(new UserDetailsMapper());
}

AuthenticationManager am = new ProviderManager(List.of(authenticationProvider));

return new ReactiveAuthenticationManagerAdapter(am);
return authenticationProvider;
}

@Bean
Expand All @@ -94,9 +108,13 @@ public LdapContextSource ldapContextSource() {
}

@Bean
public DefaultLdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context,
BaseLdapPathContextSource contextSource,
AccessControlService acs) {
public LdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context,
BaseLdapPathContextSource contextSource,
AccessControlService acs) {
if (props.isActiveDirectory()) {
return new RbacActiveDirectoryAuthoritiesExtractor(acs);
}

var rbacEnabled = acs != null && acs.isRbacEnabled();

DefaultLdapAuthoritiesPopulator extractor;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.kafbat.ui.service.rbac.extractor;

import io.kafbat.ui.model.rbac.Role;
import io.kafbat.ui.model.rbac.provider.Provider;
import io.kafbat.ui.service.rbac.AccessControlService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;

@Slf4j
public class RbacActiveDirectoryAuthoritiesExtractor implements LdapAuthoritiesPopulator {
private final AccessControlService controlService;

public RbacActiveDirectoryAuthoritiesExtractor(AccessControlService controlService) {
this.controlService = controlService;
}

@Override
public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
String[] groups = userData.getStringAttributes("memberOf");

if (log.isDebugEnabled() && groups != null && groups.length > 0) {
log.debug("'memberOf' attribute values: {}", Arrays.asList(groups));
}

if (controlService != null) {
return controlService.getRoles().stream()
.filter(role -> role.getSubjects().stream()
.filter(subject -> Provider.LDAP_AD.equals(subject.getProvider()))
.anyMatch(subject -> switch (subject.getType()) {
case "user" -> username.equals(subject.getValue());
case "group" -> groups != null && Arrays.stream(groups)
.map(this::groupName)
.anyMatch(name -> name.equals(subject.getValue()));
default -> false;
})
)
.map(Role::getName)
.peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
} else {
if (groups == null) {
return AuthorityUtils.NO_AUTHORITIES;
}

List<GrantedAuthority> authorities = new ArrayList<>(groups.length);

for (String group : groups) {
authorities.add(new SimpleGrantedAuthority(groupName(group)));
}

return authorities;
}
}

@SuppressWarnings("deprecation")
private String groupName(String group) {
return new DistinguishedName(group).removeLast().getValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public abstract class AbstractIntegrationTest {
private static final boolean IS_ARM =
System.getProperty("os.arch").contains("arm") || System.getProperty("os.arch").contains("aarch64");

private static final String CONFLUENT_PLATFORM_VERSION = IS_ARM ? "7.8.0.arm64" : "7.8.0";
public static final String CONFLUENT_PLATFORM_VERSION = IS_ARM ? "7.8.0.arm64" : "7.8.0";

public static final KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka").withTag(CONFLUENT_PLATFORM_VERSION))
Expand Down
157 changes: 157 additions & 0 deletions api/src/test/java/io/kafbat/ui/ActiveDirectoryIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package io.kafbat.ui;

import static io.kafbat.ui.AbstractIntegrationTest.CONFLUENT_PLATFORM_VERSION;
import static io.kafbat.ui.AbstractIntegrationTest.LOCAL;
import static io.kafbat.ui.container.ActiveDirectoryContainer.DOMAIN;
import static io.kafbat.ui.container.ActiveDirectoryContainer.EMPTY_PERMISSIONS_USER;
import static io.kafbat.ui.container.ActiveDirectoryContainer.FIRST_GROUP_USER;
import static io.kafbat.ui.container.ActiveDirectoryContainer.PASSWORD;
import static io.kafbat.ui.container.ActiveDirectoryContainer.SECOND_GROUP_USER;
import static io.kafbat.ui.container.ActiveDirectoryContainer.USER_WITHOUT_GROUP;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.kafbat.ui.container.ActiveDirectoryContainer;
import io.kafbat.ui.model.AuthenticationInfoDTO;
import io.kafbat.ui.model.ResourceTypeDTO;
import io.kafbat.ui.model.TopicCreationDTO;
import io.kafbat.ui.model.TopicDTO;
import io.kafbat.ui.model.UserPermissionDTO;
import java.util.List;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
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.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.utility.DockerImageName;

@SpringBootTest
@ActiveProfiles("rbac-ad")
@AutoConfigureWebTestClient(timeout = "60000")
@ContextConfiguration(initializers = {ActiveDirectoryIntegrationTest.Initializer.class})
public class ActiveDirectoryIntegrationTest {
Copy link
Member

Choose a reason for hiding this comment

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

This one is really cool tho, can you raise a separate PR with these tests so we can merge this?

Btw, we don't quite need kafka container here, the app can perfectly start with no defined clusters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Btw, we don't quite need kafka container here, the app can perfectly start with no defined clusters.

Yes, but there's a test that checks the creation of a topic on a cluster.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but there's a test that checks the creation of a topic on a cluster.

this sounds redundant. Authenticating successfully should suffice

Copy link
Contributor Author

@wernerdv wernerdv Dec 26, 2024

Choose a reason for hiding this comment

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

can you raise a separate PR with these tests so we can merge this?

Opened the PR with the tests #726

private static final String SESSION = "SESSION";

private static final KafkaContainer KAFKA = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka").withTag(CONFLUENT_PLATFORM_VERSION))
.withNetwork(Network.SHARED);

private static final ActiveDirectoryContainer ACTIVE_DIRECTORY = new ActiveDirectoryContainer();

@Autowired
private WebTestClient webTestClient;

@BeforeAll
public static void setup() {
KAFKA.start();
ACTIVE_DIRECTORY.start();
}

@AfterAll
public static void shutdown() {
ACTIVE_DIRECTORY.stop();
KAFKA.stop();
}

@Test
public void testUserPermissions() {
AuthenticationInfoDTO info = authenticationInfo(FIRST_GROUP_USER);

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

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

assertFalse(permissions.isEmpty());
assertTrue(permissions.stream().anyMatch(permission ->
permission.getClusters().contains(LOCAL) && permission.getResource() == ResourceTypeDTO.TOPIC));
assertEquals(permissions, authenticationInfo(SECOND_GROUP_USER).getUserInfo().getPermissions());
assertEquals(permissions, authenticationInfo(USER_WITHOUT_GROUP).getUserInfo().getPermissions());
}

@Test
public void testCreateTopic() {
TopicCreationDTO topic = new TopicCreationDTO()
.name("new")
.partitions(10);

TopicDTO result = webTestClient
.post()
.uri("/api/clusters/{clusterName}/topics", LOCAL)
.cookie(SESSION, session(FIRST_GROUP_USER))
.bodyValue(topic)
.exchange()
.expectStatus()
.isOk()
.returnResult(TopicDTO.class)
.getResponseBody()
.blockFirst();

assertNotNull(result);
assertEquals(topic.getName(), result.getName());
assertEquals(topic.getPartitions(), result.getPartitionCount());
}

@Test
public void testEmptyPermissions() {
assertTrue(Objects.requireNonNull(authenticationInfo(EMPTY_PERMISSIONS_USER))
.getUserInfo()
.getPermissions()
.isEmpty()
);
}

private String session(String name) {
return Objects.requireNonNull(
webTestClient
.post()
.uri("/#")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("username", name).with("password", PASSWORD))
.exchange()
.expectStatus()
.isFound()
.returnResult(String.class)
.getResponseCookies()
.getFirst(SESSION))
.getValue();
}

private AuthenticationInfoDTO authenticationInfo(String name) {
return webTestClient
.get()
.uri("/api/authorization")
.cookie(SESSION, session(name))
.exchange()
.expectStatus()
.isOk()
.returnResult(AuthenticationInfoDTO.class)
.getResponseBody()
.blockFirst();
}

public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(@NotNull ConfigurableApplicationContext context) {
System.setProperty("kafka.clusters.0.name", LOCAL);
System.setProperty("kafka.clusters.0.bootstrapServers", KAFKA.getBootstrapServers());
System.setProperty("spring.ldap.urls", ACTIVE_DIRECTORY.getLdapUrl());
System.setProperty("oauth2.ldap.activeDirectory", "true");
System.setProperty("oauth2.ldap.activeDirectory.domain", DOMAIN);
}
}
}
Loading
Loading