Skip to content

Commit

Permalink
Improve hawkBit user management (#1666)
Browse files Browse the repository at this point in the history
1. Definded with properties users (static) are configured using property map (no need of indexes)
2. AuthenticationProvider that authenticates them is always registered (if not needed - don't configure them)
3. UserDetailsService (in case of missing - won't be registered)
4. Spring security user (spring.security.username) will be registered together with other users (if any). If any - it will be system-wide, otherwise tenant-scoped.
5. UserPrincipal renamed to TenantAwareUser in order to match its purpose.
6. Some if its fields are removes as not needed - to be closer to spring security user
7. DefaultRolloutApprovalStrategy now use UserAuthoritiesResolver instead of UserDetailsService as the central point of truth

Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
  • Loading branch information
avgustinmm committed Feb 26, 2024
1 parent 783a5be commit 24d7082
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 327 deletions.
Expand Up @@ -11,129 +11,155 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import org.eclipse.hawkbit.im.authentication.MultitenancyIndicator;
import org.eclipse.hawkbit.im.authentication.PermissionUtils;
import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.im.authentication.UserPrincipal;
import org.eclipse.hawkbit.im.authentication.UserTenantAware;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.ObjectUtils;

/**
* Auto-configuration for the in-memory-user-management.
*
* Autoconfiguration for the in-memory-user-management.
*/
@Configuration
@ConditionalOnMissingBean(UserDetailsService.class)
@EnableConfigurationProperties({ MultiUserProperties.class })
@Order(Ordered.HIGHEST_PRECEDENCE)
@EnableConfigurationProperties({ TenantAwareUserProperties.class })
public class InMemoryUserManagementAutoConfiguration extends GlobalAuthenticationConfigurerAdapter {

private static final String DEFAULT_TENANT = "DEFAULT";

private final SecurityProperties securityProperties;

private final MultiUserProperties multiUserProperties;
private final UserDetailsService userDetailsService;

InMemoryUserManagementAutoConfiguration(final SecurityProperties securityProperties,
final MultiUserProperties multiUserProperties) {
this.securityProperties = securityProperties;
this.multiUserProperties = multiUserProperties;
InMemoryUserManagementAutoConfiguration(
final SecurityProperties securityProperties,
final TenantAwareUserProperties userTenantAwareProperties,
final Optional<PasswordEncoder> passwordEncoder) {
userDetailsService = userDetailsService(
securityProperties, userTenantAwareProperties, passwordEncoder.orElse(null));
}

@Override
public void configure(final AuthenticationManagerBuilder auth) throws Exception {
public void configure(final AuthenticationManagerBuilder auth) {
final DaoAuthenticationProvider userDaoAuthenticationProvider = new TenantDaoAuthenticationProvider();
userDaoAuthenticationProvider.setUserDetailsService(userDetailsService());
userDaoAuthenticationProvider.setUserDetailsService(userDetailsService);
auth.authenticationProvider(userDaoAuthenticationProvider);
}

/**
* @return the user details service to load a user from memory user manager.
*/
@Bean
@ConditionalOnMissingBean
UserDetailsService userDetailsService() {

final List<UserPrincipal> userPrincipals = new ArrayList<>();
for (final MultiUserProperties.User user : multiUserProperties.getUsers()) {
final List<String> permissions = user.getPermissions();
List<GrantedAuthority> authorityList;
// Allows ALL as a shorthand for all permissions
if (permissions.size() == 1 && "ALL".equals(permissions.get(0))) {
authorityList = PermissionUtils.createAllAuthorityList();
} else {
authorityList = createAuthoritiesFromList(permissions);
}

final UserPrincipal userPrincipal = new UserPrincipal(user.getUsername(), user.getPassword(),
user.getFirstname(), user.getLastname(), user.getUsername(), user.getEmail(), DEFAULT_TENANT,
authorityList);
private static UserDetailsService userDetailsService(
final SecurityProperties securityProperties,
final TenantAwareUserProperties userTenantAwareProperties,
final PasswordEncoder passwordEncoder) {
final List<User> userPrincipals = new ArrayList<>();
userTenantAwareProperties.getUsers().forEach((username, user) -> {
final UserTenantAware userPrincipal = new UserTenantAware(
username, password(user.getPassword(), passwordEncoder),
createAuthorities(user.getRoles(), Collections::emptyList),
ObjectUtils.isEmpty(user.getTenant()) ? DEFAULT_TENANT : user.getTenant());
userPrincipals.add(userPrincipal);
}
});

// If no users are configured through the multi user properties, set up
// the default user from security properties
// If no tenant users are configured through the tenant user properties, set up
// the default user from spring security properties as super DEFAULT tenant user
if (userPrincipals.isEmpty()) {
final String name = securityProperties.getUser().getName();
final String password = securityProperties.getUser().getPassword();
final List<String> roles = securityProperties.getUser().getRoles();
final List<GrantedAuthority> authorityList = roles.isEmpty() ? PermissionUtils.createAllAuthorityList()
: createAuthoritiesFromList(roles);
userPrincipals
.add(new UserPrincipal(name, password, name, name, name, null, DEFAULT_TENANT, authorityList));
.add(new UserTenantAware(
securityProperties.getUser().getName(),
password(securityProperties.getUser().getPassword(), passwordEncoder),
createAuthorities(
securityProperties.getUser().getRoles(), PermissionUtils::createAllAuthorityList),
DEFAULT_TENANT));
} else if (securityProperties != null && securityProperties.getUser() != null &&
!securityProperties.getUser().isPasswordGenerated()) {
// otherwise if the security user is explicitly setup (no autogenerated password)
// set it up as generic non tenant user
userPrincipals
.add(new User(
securityProperties.getUser().getName(),
password(securityProperties.getUser().getPassword(), passwordEncoder),
createAuthorities(
securityProperties.getUser().getRoles(), PermissionUtils::createAllAuthorityList)));
}

return new FixedInMemoryUserPrincipalUserDetailsService(userPrincipals);
return new FixedInMemoryTenantAwareUserDetailsService(userPrincipals);
}

private static List<GrantedAuthority> createAuthoritiesFromList(final List<String> userAuthorities) {
final List<GrantedAuthority> grantedAuthorityList = new ArrayList<>(userAuthorities.size());
for (final String permission : userAuthorities) {
private static String password(final String password, final PasswordEncoder passwordEncoder) {
return passwordEncoder == null && !Pattern.compile("^\\{.+}.*$").matcher(password).matches() ?
"{noop}" + password : password;
}

private static List<GrantedAuthority> createAuthorities(
final List<String> userPermissions, final Supplier<List<GrantedAuthority>> defaultRolesSupplier) {
if (ObjectUtils.isEmpty(userPermissions)) {
return defaultRolesSupplier.get();
}

// Allows ALL as a shorthand for all permissions
if (userPermissions.size() == 1 && "ALL".equals(userPermissions.get(0))) {
return PermissionUtils.createAllAuthorityList();
}

final List<GrantedAuthority> grantedAuthorityList = new ArrayList<>(userPermissions.size());
for (final String permission : userPermissions) {
grantedAuthorityList.add(new SimpleGrantedAuthority(permission));
grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_" + permission));
}
return grantedAuthorityList;
}

private static class FixedInMemoryUserPrincipalUserDetailsService implements UserDetailsService {
private final HashMap<String, UserPrincipal> userPrincipalMap = new HashMap<>();
private static class FixedInMemoryTenantAwareUserDetailsService implements UserDetailsService {

public FixedInMemoryUserPrincipalUserDetailsService(final Collection<UserPrincipal> userPrincipals) {
for (final UserPrincipal user : userPrincipals) {
userPrincipalMap.put(user.getUsername(), user);
}
}
private final HashMap<String, User> userMap = new HashMap<>();

private static UserPrincipal clone(final UserPrincipal a) {
return new UserPrincipal(a.getUsername(), a.getPassword(), a.getFirstname(), a.getLastname(),
a.getLoginname(), a.getEmail(), a.getTenant(), a.getAuthorities());
private FixedInMemoryTenantAwareUserDetailsService(final Collection<User> userPrincipals) {
for (final User user : userPrincipals) {
userMap.put(user.getUsername(), user);
}
}

@Override
public UserDetails loadUserByUsername(final String username) {
final UserPrincipal userPrincipal = userPrincipalMap.get(username);
if (userPrincipal == null) {
final User user = userMap.get(username);
if (user == null) {
throw new UsernameNotFoundException("No such user");
}
// Spring mutates the data, so we must return a copy here
return clone(userPrincipal);
return clone(user);
}

private static User clone(final User user) {
if (user instanceof UserTenantAware) {
return new UserTenantAware(user.getUsername(), user.getPassword(), user.getAuthorities(),
((UserTenantAware)user).getTenant());
} else {
return new User(user.getUsername(), user.getPassword(), user.getAuthorities());
}
}
}

/**
Expand All @@ -156,4 +182,4 @@ protected Authentication createSuccessAuthentication(final Object principal,
return result;
}
}
}
}

This file was deleted.

Expand Up @@ -46,7 +46,6 @@
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
Expand Down
Expand Up @@ -15,7 +15,7 @@
import java.util.stream.Collectors;

import org.eclipse.hawkbit.ContextAware;
import org.eclipse.hawkbit.autoconfigure.security.MultiUserProperties.User;
import org.eclipse.hawkbit.autoconfigure.security.TenantAwareUserProperties.User;
import org.eclipse.hawkbit.im.authentication.PermissionService;
import org.eclipse.hawkbit.security.DdiSecurityProperties;
import org.eclipse.hawkbit.security.InMemoryUserAuthoritiesResolver;
Expand Down Expand Up @@ -47,18 +47,17 @@
* {@link EnableAutoConfiguration Auto-configuration} for security.
*/
@Configuration
@EnableConfigurationProperties({ SecurityProperties.class, DdiSecurityProperties.class, HawkbitSecurityProperties.class,
MultiUserProperties.class })
@EnableConfigurationProperties({
SecurityProperties.class,
DdiSecurityProperties.class, HawkbitSecurityProperties.class, TenantAwareUserProperties.class })
public class SecurityAutoConfiguration {

/**
* Creates a {@link ContextAware} (hence {@link TenantAware}) bean based on the given
* {@link UserAuthoritiesResolver} and {@link SecurityContextSerializer}.
*
* @param authoritiesResolver
* The user authorities/roles resolver
* @param securityContextSerializer
* The security context serializer.
* @param authoritiesResolver The user authorities/roles resolver
* @param securityContextSerializer The security context serializer.
*
* @return the {@link ContextAware} singleton bean.
*/
Expand All @@ -74,21 +73,19 @@ public ContextAware contextAware(
* Creates a {@link UserAuthoritiesResolver} bean that is responsible for
* resolving user authorities/roles.
*
* @param securityProperties
* The Spring {@link SecurityProperties} for the security user
* @param multiUserProperties
* The {@link MultiUserProperties} for the managed users
*
* @param securityProperties The Spring {@link SecurityProperties} for the security user
* @param userTenantAwareProperties The {@link TenantAwareUserProperties} for the managed users
* @return an {@link InMemoryUserAuthoritiesResolver} bean
*/
@Bean
@ConditionalOnMissingBean
public UserAuthoritiesResolver inMemoryAuthoritiesResolver(final SecurityProperties securityProperties,
final MultiUserProperties multiUserProperties) {
final List<User> multiUsers = multiUserProperties.getUsers();
final TenantAwareUserProperties userTenantAwareProperties) {
final Map<String, User> userTenantAwares = userTenantAwareProperties.getUsers();
final Map<String, List<String>> usersToPermissions;
if (!CollectionUtils.isEmpty(multiUsers)) {
usersToPermissions = multiUsers.stream().collect(Collectors.toMap(User::getUsername, User::getPermissions));
if (!CollectionUtils.isEmpty(userTenantAwares)) {
usersToPermissions = userTenantAwares.entrySet().stream().collect(
Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getRoles()));
} else {
usersToPermissions = Collections.singletonMap(securityProperties.getUser().getName(),
securityProperties.getUser().getRoles());
Expand All @@ -108,7 +105,7 @@ public PermissionService permissionService() {

/**
* Creates the auditor aware.
*
*
* @return the spring security auditor aware
*/
@Bean
Expand Down

0 comments on commit 24d7082

Please sign in to comment.