Skip to content

Commit

Permalink
Role based access control (provectus#2790)
Browse files Browse the repository at this point in the history
* Role based access control

* Fix build + checkstyle

* Refactoring, some bug fixes, review fixes

* Compile permission value patterns

* Make the resource a enum instead of a string

* Refactoring

* Make clusters required

* Fix formatting

* switch the switch case to a smart switch case

* Get rid of topic analysis actions

* Rename endpoints, fix an issue

* Return a flag indicating if rbac is on and a username

* Fix yaml indent in editorconfig

* Fix github & cognito role name fetching

* Fix case matching for actions

* Update readme

* Add an endpoint to determine if a user can create a resource

* Fix tests (I hope so)

* Fix tests

* Use spring configs instead of a separate file, rename endpoints

* Add "ALL" action
Get rid of unnecessary cache, save groups into spring auth
Review fixes

* Make "all" action case-insensitive

* Role based access control / FrontEnd  (provectus#2933)

* Initial modifications and mocking the For the RoleAccess

* fix the Suspense issue in the components , comment the Tests to implement later

* minor test comment

* Roles and configuration and santization of data

* initialize RoleCheck hook

* make the App test file visible + minor modification in the permission hook

* Structure the data so the Burger header toggle does not rerender the whole application

* add tests to the NavBar and the Page container , add tests

* NavBar and PageContainer bug fixes

* Roles Testing code modification

* covering Topics create button Actions, and Schema create button Actions

* minor typescript code modifications for the cluster required parameter in the rolesHelper

* minor typescript code modifications for the cluster required parameter in the rolesHelper

* minor code modification to describe the Permission tests more clearly

* Produce message Permissions with Tests Suites for Topic

* Add Schema Edit Permission with tests

* Minor role changes

* Add ActionButton Component to handle the Button with tooltip

* Add ActionButton Component to handle the Button with tooltip

* Add Action Button to every Button create Action

* ActionButton add test suites

* usePermission code modification to include regular expressions

* Abstract Actions Component for code repetition, add Configs Edit button Permission + add the tests suites to it.

* Schema Remove functionality Permission and Test Suites + creation of the ActionDropdownItem for Actions

* Topic Edit Clear and delete Topic , Permissions with test suites

* ActionsCell For Topic Message Overview for permissions with tests suites

* Connector Delete , Consumer Groups Permission + writing test suites

* Add Permissions to the Topics ActionCell

* Topic Table Permissions Tests Suites

* Headless Logic for the Permission Part

* add documentation for the headless Part of the permission + add modification of the data version 2 for efficient algorithmic lookup

* replace modify data logic and isPermitted function to have faster access to the data

* Add Permission helpers tests suites

* usePermission hook test suites

* BatchActionsBar add Permissions + minor modification in TopicTable tests suites

* Statistics and Metrics code Permission + add test suites

* Recreate Topic Permissions in the Topic page, add tests suites

* Actions for the Connector components

* Messages NavLink View Permission

* Test suites messages code modifications

* Permissions comment code modifications

* Replacing the Mock Data With the actual code

* Add ActionNavLink test suites

* BatchActionsBar code smell modifications

* maximizing the permissions tests suites

* maximizing the permissions tests suites

* maximizing the permissions tests suites

* Tooltip code refactoring and fix the positions issue

* permissions increase the tests coverage

* add user info at the navigation header and tests suites

* Add Global Schema Selector Permissions with test suites

* Roles minor code removal

* Change the Action Component form hook mixin approach to declarative props approach

* add isPermitted function for multiple Actions , adding tests suites for this particular case

* remove redundant Permissions test blocks from the components

* remove redundant Permissions test blocks from the components

* Action Buttons test suites' coverage + generalizing the code of the Actions

* add invalid Permission check in Action Components tests suites

* Modularization of Actions Components

* Modularization of Actions Components by adding DropDownAction to it.

* Reflect the BE Changes to the UI , by changing the default behavior or the testing of roles.

* Reflect the BE Changes to the UI , by changing the default behavior or the testing of roles.

* Get rid of not necessary usePermission mocks

* Modifications in the UserInfo data , to consider the UI without any login functionality

* minor code modifications in the BatchActionBar component

* change the Query key for the user info

* change the default message for the tooltip

* Fix the Create Role Access for Topics and Schemas

* ListPage Connector create permissions

* add Headless logic for Create Permission with test suites. + add react hook render-er

* Create Button ActionButton logic implementation

* Remove Code smells , by removing the duplications

* increase the test suites for isPermittedToCreate logic

* increase the test suites for isPermittedToCreate logic

* Change the UserResourceType Enum with the new value

* Apply New Resource Creation validation, for Topic, Schema, Connector

* Apply New Resource Creation validation, for Topic, Schema, Connector

* minor code refactor modifications

* minor code modification in the topics useCreate hook

* Async Validation for all the Create Pages

* caching test for optimal performance in async validation schemas

* Reverting the Front End Validation

* Reverting the Front End Validation

* Authorization API minor syntax modifications

* fix SmokeTests

Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
Co-authored-by: VladSenyuta <vlad.senyuta@gmail.com>

Co-authored-by: Mgrdich <46796009+Mgrdich@users.noreply.github.com>
Co-authored-by: VladSenyuta <vlad.senyuta@gmail.com>
  • Loading branch information
3 people authored Dec 22, 2022
1 parent c2be45f commit 5c723d9
Show file tree
Hide file tree
Showing 141 changed files with 5,771 additions and 1,120 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,8 @@ ij_java_wrap_long_lines = false
insert_final_newline = false
trim_trailing_whitespace = false

[*.yaml]
indent_size = 2
[*.yml]
indent_size = 2

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ the cloud.
* **Dynamic Topic Configuration** — create and configure new topics with dynamic configuration
* **Configurable Authentification** — secure your installation with optional Github/Gitlab/Google OAuth 2.0
* **Custom serialization/deserialization plugins** - use a ready-to-go serde for your data like AWS Glue or Smile, or code your own!
* **Role based access control** - manage permissions to access the UI with granular precision

# The Interface
UI for Apache Kafka wraps major functions of Apache Kafka with an intuitive user interface.
Expand Down
1 change: 1 addition & 0 deletions kafka-ui-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@
</configuration>
</execution>
</executions>

</plugin>
<plugin>
<groupId>org.antlr</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.provectus.kafka.ui.config.auth;

import java.util.Collection;
import lombok.Value;

public record AuthenticatedUser(String principal, Collection<String> groups) {

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.provectus.kafka.ui.config.auth;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;

@ConfigurationProperties("auth.oauth2")
@Data
public class OAuthProperties {
private Map<String, OAuth2Provider> client = new HashMap<>();

@PostConstruct
public void validate() {
getClient().values().forEach(this::validateProvider);
}

private void validateProvider(final OAuth2Provider provider) {
Assert.hasText(provider.getClientId(), "Client id must not be empty.");
Assert.hasText(provider.getProvider(), "Provider name must not be empty");
}

@Data
public static class OAuth2Provider {
private String provider;
private String clientId;
private String clientSecret;
private String clientName;
private String redirectUri;
private String authorizationGrantType;
private Set<String> scope;
private String issuerUri;
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private String jwkSetUri;
private String userNameAttribute;
private Map<String, String> customParams;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.provectus.kafka.ui.config.auth;

import static com.provectus.kafka.ui.config.auth.OAuthProperties.OAuth2Provider;
import static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider;
import static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class OAuthPropertiesConverter {

private static final String TYPE = "type";
private static final String GOOGLE = "google";

public static OAuth2ClientProperties convertProperties(final OAuthProperties properties) {
final var result = new OAuth2ClientProperties();
properties.getClient().forEach((key, provider) -> {
var registration = new Registration();
registration.setClientId(provider.getClientId());
registration.setClientSecret(provider.getClientSecret());
registration.setClientName(provider.getClientName());
registration.setScope(provider.getScope());
registration.setRedirectUri(provider.getRedirectUri());
registration.setAuthorizationGrantType(provider.getAuthorizationGrantType());

result.getRegistration().put(key, registration);

var clientProvider = new Provider();
applyCustomTransformations(provider);

clientProvider.setAuthorizationUri(provider.getAuthorizationUri());
clientProvider.setIssuerUri(provider.getIssuerUri());
clientProvider.setJwkSetUri(provider.getJwkSetUri());
clientProvider.setTokenUri(provider.getTokenUri());
clientProvider.setUserInfoUri(provider.getUserInfoUri());
clientProvider.setUserNameAttribute(provider.getUserNameAttribute());

result.getProvider().put(key, clientProvider);
});
return result;
}

private static void applyCustomTransformations(OAuth2Provider provider) {
applyGoogleTransformations(provider);
}

private static void applyGoogleTransformations(OAuth2Provider provider) {
if (!isGoogle(provider)) {
return;
}

String allowedDomain = provider.getCustomParams().get("allowedDomain");
if (StringUtils.isEmpty(allowedDomain)) {
return;
}

final String newUri = provider.getAuthorizationUri() + "?hd=" + allowedDomain;
provider.setAuthorizationUri(newUri);
}

private static boolean isGoogle(OAuth2Provider provider) {
return provider.getCustomParams().get(TYPE).equalsIgnoreCase(GOOGLE);
}
}

Original file line number Diff line number Diff line change
@@ -1,66 +1,131 @@
package com.provectus.kafka.ui.config.auth;

import lombok.AllArgsConstructor;
import com.provectus.kafka.ui.config.auth.logout.OAuthLogoutSuccessHandler;
import com.provectus.kafka.ui.service.rbac.AccessControlService;
import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.Nullable;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.util.ClassUtils;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import reactor.core.publisher.Mono;

@Configuration
@EnableWebFluxSecurity
@ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2")
@AllArgsConstructor
@EnableConfigurationProperties(OAuthProperties.class)
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@RequiredArgsConstructor
@Log4j2
public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {

public static final String REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME =
"org.springframework.security.oauth2.client.registration."
+ "ReactiveClientRegistrationRepository";

private static final boolean IS_OAUTH2_PRESENT = ClassUtils.isPresent(
REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME,
OAuthSecurityConfig.class.getClassLoader()
);

private final ApplicationContext context;
private final OAuthProperties properties;

@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http) {
public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) {
log.info("Configuring OAUTH2 authentication.");
http.authorizeExchange()

return http.authorizeExchange()
.pathMatchers(AUTH_WHITELIST)
.permitAll()
.anyExchange()
.authenticated();
.authenticated()

.and()
.oauth2Login()

.and()
.logout()
.logoutSuccessHandler(logoutHandler)

.and()
.csrf().disable()
.build();
}

@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserService(AccessControlService acs) {
final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
return request -> delegate.loadUser(request)
.flatMap(user -> {
String providerId = request.getClientRegistration().getRegistrationId();
final var extractor = getExtractor(providerId, acs);
if (extractor == null) {
return Mono.just(user);
}

if (IS_OAUTH2_PRESENT && OAuth2ClasspathGuard.shouldConfigure(this.context)) {
OAuth2ClasspathGuard.configure(http);
}
return extractor.extract(acs, user, Map.of("request", request))
.map(groups -> new RbacOidcUser(user, groups));
});
}

@Bean
public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> customOauth2UserService(AccessControlService acs) {
final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();
return request -> delegate.loadUser(request)
.flatMap(user -> {
String providerId = request.getClientRegistration().getRegistrationId();
final var extractor = getExtractor(providerId, acs);
if (extractor == null) {
return Mono.just(user);
}

return http.csrf().disable().build();
return extractor.extract(acs, user, Map.of("request", request))
.map(groups -> new RbacOAuth2User(user, groups));
});
}

private static class OAuth2ClasspathGuard {
static void configure(ServerHttpSecurity http) {
http
.oauth2Login()
.and()
.oauth2Client();
}

static boolean shouldConfigure(ApplicationContext context) {
ClassLoader loader = context.getClassLoader();
Class<?> reactiveClientRegistrationRepositoryClass =
ClassUtils.resolveClassName(REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, loader);
return context.getBeanNamesForType(reactiveClientRegistrationRepositoryClass).length == 1;
}
@Bean
public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
final OAuth2ClientProperties props = OAuthPropertiesConverter.convertProperties(properties);
final List<ClientRegistration> registrations =
new ArrayList<>(OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(props).values());
return new InMemoryReactiveClientRegistrationRepository(registrations);
}

@Bean
public ServerLogoutSuccessHandler defaultOidcLogoutHandler(final ReactiveClientRegistrationRepository repository) {
return new OidcClientInitiatedServerLogoutSuccessHandler(repository);
}

@Nullable
private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
final String provider = getProviderByProviderId(providerId);
Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors()
.stream()
.filter(e -> e.isApplicable(provider))
.findFirst();

return extractor.orElse(null);
}

private String getProviderByProviderId(final String providerId) {
return properties.getClient().get(providerId).getProvider();
}

}

Loading

0 comments on commit 5c723d9

Please sign in to comment.