Skip to content

Commit

Permalink
Add multi-user support (#829)
Browse files Browse the repository at this point in the history
This allows optionally configuring multiple static users with varying
permissions. If used, Spring Security user/password are ignored.
Otherwise, the old behavior is retained.

Signed-off-by: Stefan Schake <stefan.schake@devolo.de>
  • Loading branch information
stschake authored and Dominic Schabel committed May 21, 2019
1 parent d34e7f3 commit 7c04ca1
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 30 deletions.
22 changes: 21 additions & 1 deletion docs/content/concepts/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Authorization is handled separately for _Direct Device Integration (DDI) API_ an

However, keep in mind that hawkBit does not offer an off the shelf authentication provider to leverage these permissions and the underlying multi user/tenant capabilities of hawkBit. Check out [Spring security documentation](http://projects.spring.io/spring-security/) for further information. In hawkBit [SecurityAutoConfiguration](https://github.com/eclipse/hawkbit/blob/master/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java) is a good starting point for integration.

The default implementation is single user/tenant with basic auth and the logged in user is provided with all permissions.
The default implementation is single user/tenant with basic auth and the logged in user is provided with all permissions. Additionally, the application properties may be configured for multiple static users; see [Multiple Users](#multiple-users) for details.

## DDI API
An authenticated target is permitted to:
Expand All @@ -21,6 +21,26 @@ A target might be permitted to download artifacts without authentication (if ena

## Management API and UI

### Multiple Users
hawkBit optionally supports configuring multiple static users through the application properties. In this case, the user and password Spring security properties are ignored.
An example configuration is given below.

hawkbit.server.im.users[0].username=admin
hawkbit.server.im.users[0].password={noop}admin
hawkbit.server.im.users[0].firstname=Test
hawkbit.server.im.users[0].lastname=Admin
hawkbit.server.im.users[0].email=admin@test.de
hawkbit.server.im.users[0].permissions=ALL

hawkbit.server.im.users[1].username=test
hawkbit.server.im.users[1].password={noop}test
hawkbit.server.im.users[1].firstname=Test
hawkbit.server.im.users[1].lastname=Tester
hawkbit.server.im.users[1].email=test@tester.com
hawkbit.server.im.users[1].permissions=READ_TARGET,UPDATE_TARGET,CREATE_TARGET,DELETE_TARGET

A permissions value of `ALL` will provide that user will all possible permissions. Passwords need to be specified with the used password encoder in brackets. In this example, `noop` is used as the plaintext encoder. For production use, it is recommended to use a hash function designed for passwords such as *bcrypt*. See this [blog post](https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-format) for more information on password encoders in Spring Security.

### Delivered Permissions
- READ_/UPDATE_/CREATE_/DELETE_TARGETS for:
- Target entities including metadata (that includes also the installed and assigned distribution sets)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
package org.eclipse.hawkbit.autoconfigure.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

import org.eclipse.hawkbit.im.authentication.MultitenancyIndicator;
Expand All @@ -17,31 +19,37 @@
import org.eclipse.hawkbit.im.authentication.UserPrincipal;
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.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.userdetails.User;
import org.springframework.security.core.userdetails.User.UserBuilder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

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

private final SecurityProperties securityProperties;

InMemoryUserManagementAutoConfiguration(final SecurityProperties securityProperties) {
private final MultiUserProperties multiUserProperties;

InMemoryUserManagementAutoConfiguration(final SecurityProperties securityProperties,
final MultiUserProperties multiUserProperties) {
this.securityProperties = securityProperties;
this.multiUserProperties = multiUserProperties;
}

@Override
Expand All @@ -57,17 +65,62 @@ public void configure(final AuthenticationManagerBuilder auth) throws Exception
@Bean
@ConditionalOnMissingBean
UserDetailsService userDetailsService() {
final InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserPrincipalDetailsManager();
inMemoryUserDetailsManager.setAuthenticationManager(null);
final SecurityProperties.User user = securityProperties.getUser();
final UserBuilder userBuilder = User.builder().username(user.getName()).password(user.getPassword())
.authorities(PermissionUtils.createAllAuthorityList());
final List<String> roles = user.getRoles();
if (!roles.isEmpty()) {
userBuilder.roles(roles.toArray(new String[roles.size()]));
final String defaultTenant = "DEFAULT";
final List<UserPrincipal> userPrincipals = new ArrayList<>();
for (MultiUserProperties.User user : multiUserProperties.getUsers()) {
List<GrantedAuthority> authorityList;
// Allows ALL as a shorthand for all permissions
if (user.getPermissions().size() == 1 && user.getPermissions().get(0).equals("ALL")) {
authorityList = PermissionUtils.createAllAuthorityList();
} else {
authorityList = new ArrayList<>(user.getPermissions().size());
for (final String permission : user.getPermissions()) {
authorityList.add(new SimpleGrantedAuthority(permission));
authorityList.add(new SimpleGrantedAuthority("ROLE_" + permission));
}
}

final UserPrincipal userPrincipal = new UserPrincipal(user.getUsername(), user.getPassword(),
user.getFirstname(), user.getLastname(), user.getUsername(), user.getEmail(), defaultTenant,
authorityList);
userPrincipals.add(userPrincipal);
}

// If no users are configured through the multi user properties, set up
// the default user from security properties
if (userPrincipals.isEmpty()) {
final String name = securityProperties.getUser().getName();
final String password = securityProperties.getUser().getPassword();
userPrincipals.add(new UserPrincipal(name, password, name, name, name, null, defaultTenant,
PermissionUtils.createAllAuthorityList()));
}

return new FixedInMemoryUserPrincipalUserDetailsService(userPrincipals);
}

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

public FixedInMemoryUserPrincipalUserDetailsService(Collection<UserPrincipal> userPrincipals) {
for (UserPrincipal user : userPrincipals) {
userPrincipalMap.put(user.getUsername(), user);
}
}

private static UserPrincipal clone(UserPrincipal a) {
return new UserPrincipal(a.getUsername(), a.getPassword(), a.getFirstname(), a.getLastname(),
a.getLoginname(), a.getEmail(), a.getTenant(), a.getAuthorities());
}
inMemoryUserDetailsManager.createUser(userBuilder.build());
return inMemoryUserDetailsManager;

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

}

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

private static final class InMemoryUserPrincipalDetailsManager extends InMemoryUserDetailsManager {

private InMemoryUserPrincipalDetailsManager() {
super(new ArrayList<>());
}

@Override
public UserDetails loadUserByUsername(final String username) {
final UserDetails loadUserByUsername = super.loadUserByUsername(username);
return new UserPrincipal(loadUserByUsername.getUsername(), loadUserByUsername.getPassword(),
loadUserByUsername.getUsername(), loadUserByUsername.getUsername(),
loadUserByUsername.getUsername(), null, "DEFAULT", loadUserByUsername.getAuthorities());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2019 devolo AG and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.autoconfigure.security;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("hawkbit.server.im")
public class MultiUserProperties {
private List<User> users = new ArrayList<>();

public List<User> getUsers() {
return users;
}

public void setUsers(List<User> users) {
this.users = users;
}

public static class User {
private String username;
private String password;
private String firstname;
private String lastname;
private String email;
private List<String> permissions = new ArrayList<>();

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getFirstname() {
return firstname;
}

public void setFirstname(String firstname) {
this.firstname = firstname;
}

public String getLastname() {
return lastname;
}

public void setLastname(String lastname) {
this.lastname = lastname;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public List<String> getPermissions() {
return permissions;
}

public void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
}
}
6 changes: 6 additions & 0 deletions licenses/LICENSE_HEADER_TEMPLATE_DEVOLO_19.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Copyright (c) 2019 devolo AG and others.

All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
which accompanies this distribution, and is available at
http://www.eclipse.org/legal/epl-v10.html
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_BOSCH.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_MICROSOFT_18.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_BOSCH_18.txt</validHeader>
<validHeader>licenses/LICENSE_HEADER_TEMPLATE_DEVOLO_19.txt</validHeader>
</validHeaders>
<excludes>
<exclude>**/banner.txt</exclude>
Expand Down

0 comments on commit 7c04ca1

Please sign in to comment.