Skip to content

Commit

Permalink
Rework authentication granted authorities #233 (#2209)
Browse files Browse the repository at this point in the history
Role names are stored as strings in the Authentication object;
RoleGrantedAuthorityUtils class must be used for role granted authorities creation;
BaseRoleRepository inheritance is replaced by delegating to new RoleRepositoryProviderUtils;
Spring cache is used for resource and row-level roles;
Add jmix-security-starter to build.gradle in project templates;
  • Loading branch information
gorbunkov committed Sep 11, 2023
1 parent 76df4f1 commit 44f5b2b
Show file tree
Hide file tree
Showing 42 changed files with 973 additions and 644 deletions.
Expand Up @@ -19,21 +19,19 @@
import io.jmix.authserver.AuthServerProperties;
import io.jmix.authserver.roleassignment.RegisteredClientRoleAssignment;
import io.jmix.authserver.roleassignment.RegisteredClientRoleAssignmentRepository;
import io.jmix.security.authentication.RoleGrantedAuthority;
import io.jmix.security.role.ResourceRoleRepository;
import io.jmix.security.role.RowLevelRoleRepository;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

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

/**
* The class is used for converting a list of role codes specified for the client in properties file into the list of
* {@link RoleGrantedAuthority}.
* {@link GrantedAuthority}.
*
* @see AuthServerProperties
*/
Expand All @@ -42,47 +40,37 @@ public class TokenIntrospectorRolesHelper {

private static final Logger log = LoggerFactory.getLogger(TokenIntrospectorRolesHelper.class);

private ResourceRoleRepository resourceRoleRepository;

private RowLevelRoleRepository rowLevelRoleRepository;

private RegisteredClientRoleAssignmentRepository clientRoleAssignmentRepository;

public TokenIntrospectorRolesHelper(ResourceRoleRepository resourceRoleRepository,
RowLevelRoleRepository rowLevelRoleRepository,
RegisteredClientRoleAssignmentRepository clientRoleAssignmentRepository) {
this.resourceRoleRepository = resourceRoleRepository;
this.rowLevelRoleRepository = rowLevelRoleRepository;
private RoleGrantedAuthorityUtils roleGrantedAuthorityUtils;

public TokenIntrospectorRolesHelper(RegisteredClientRoleAssignmentRepository clientRoleAssignmentRepository,
RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
this.clientRoleAssignmentRepository = clientRoleAssignmentRepository;
this.roleGrantedAuthorityUtils = roleGrantedAuthorityUtils;
}

/**
* Converts a list of roles specified for the client in properties file into the list of
* {@link RoleGrantedAuthority}.
* {@link GrantedAuthority}.
*
* @param clientId a client id
* @return a list of RoleGrantedAuthority
*/
public List<RoleGrantedAuthority> getClientGrantedAuthorities(String clientId) {
public List<GrantedAuthority> getClientGrantedAuthorities(String clientId) {
Collection<RegisteredClientRoleAssignment> roleAssignments = clientRoleAssignmentRepository.findByClientId(clientId);

List<String> resourceRoles = roleAssignments.stream()
List<GrantedAuthority> resourceRoleAuthorities = roleAssignments.stream()
.flatMap(roleAssignment -> roleAssignment.resourceRoles().stream())
.toList();
List<RoleGrantedAuthority> resourceRoleAuthorities = resourceRoles.stream()
.map(resourceRoleRepository::getRoleByCode)
.map(RoleGrantedAuthority::ofResourceRole)
.map(roleCode -> roleGrantedAuthorityUtils.createResourceRoleGrantedAuthority(roleCode))
.toList();

List<String> rowLevelRoles = roleAssignments.stream()
List<GrantedAuthority> rowLevelRolesAuthorities = roleAssignments.stream()
.flatMap(roleAssignment -> roleAssignment.rowLevelRoles().stream())
.toList();
List<RoleGrantedAuthority> rowLevelRolesAuthorities = rowLevelRoles.stream()
.map(rowLevelRoleRepository::getRoleByCode)
.map(RoleGrantedAuthority::ofRowLevelRole)
.map(roleCode -> roleGrantedAuthorityUtils.createRowLevelRoleGrantedAuthority(roleCode))
.toList();

List<RoleGrantedAuthority> authorities = new ArrayList<>();
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.addAll(resourceRoleAuthorities);
authorities.addAll(rowLevelRolesAuthorities);

Expand Down
Expand Up @@ -16,12 +16,13 @@

package io.jmix.ldap.userdetails;

import com.google.common.base.Strings;
import io.jmix.core.SaveContext;
import io.jmix.core.UnconstrainedDataManager;
import io.jmix.core.entity.EntityValues;
import io.jmix.core.security.UserRepository;
import io.jmix.ldap.LdapProperties;
import io.jmix.security.authentication.RoleGrantedAuthority;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import io.jmix.security.role.assignment.RoleAssignmentRoleType;
import io.jmix.securitydata.entity.RoleAssignmentEntity;
import org.slf4j.Logger;
Expand Down Expand Up @@ -63,6 +64,9 @@ public abstract class AbstractLdapUserDetailsSynchronizationStrategy<T extends U
@Autowired
protected LdapProperties ldapProperties;

@Autowired
protected RoleGrantedAuthorityUtils roleGrantedAuthorityUtils;

@Override
@SuppressWarnings("unchecked")
public UserDetails synchronizeUserDetails(DirContextOperations ctx, String username,
Expand Down Expand Up @@ -119,16 +123,17 @@ public UserDetails synchronizeUserDetails(DirContextOperations ctx, String usern
protected Collection<RoleAssignmentEntity> buildRoleAssignments(Collection<GrantedAuthority> grantedAuthorities,
String username) {
List<RoleAssignmentEntity> roleAssignmentEntities = new ArrayList<>();
String defaultRolePrefix = roleGrantedAuthorityUtils.getDefaultRolePrefix();
String defaultRowLevelRolePrefix = roleGrantedAuthorityUtils.getDefaultRowLevelRolePrefix();
for (GrantedAuthority grantedAuthority : grantedAuthorities) {
if (grantedAuthority instanceof RoleGrantedAuthority) {
RoleGrantedAuthority roleGrantedAuthority = (RoleGrantedAuthority) grantedAuthority;
String roleCode = roleGrantedAuthority.getAuthority();
String roleType;
if (roleCode.startsWith(ROW_LEVEL_ROLE_PREFIX)) {
String roleCode = grantedAuthority.getAuthority();
if (!Strings.isNullOrEmpty(roleCode)) {
String roleType = RoleAssignmentRoleType.RESOURCE;
if (roleCode.startsWith(defaultRolePrefix)) {
roleCode = roleCode.substring(defaultRolePrefix.length());
} else if (roleCode.startsWith(defaultRowLevelRolePrefix)) {
roleCode = roleCode.substring(defaultRowLevelRolePrefix.length());
roleType = RoleAssignmentRoleType.ROW_LEVEL;
roleCode = roleCode.substring(ROW_LEVEL_ROLE_PREFIX.length());
} else {
roleType = RoleAssignmentRoleType.RESOURCE;
}
RoleAssignmentEntity roleAssignmentEntity = dataManager.create(RoleAssignmentEntity.class);
roleAssignmentEntity.setRoleCode(roleCode);
Expand Down
Expand Up @@ -16,10 +16,10 @@

package io.jmix.ldap.userdetails;

import io.jmix.security.authentication.RoleGrantedAuthority;
import io.jmix.security.model.ResourceRole;
import io.jmix.security.model.RowLevelRole;
import io.jmix.security.role.ResourceRoleRepository;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import io.jmix.security.role.RowLevelRoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
Expand All @@ -29,11 +29,11 @@
import java.util.*;

/**
* GrantedAuthoritiesMapper that maps authorities to {@link RoleGrantedAuthority}s.
* GrantedAuthoritiesMapper that maps authorities to {@link GrantedAuthority}s.
* <p>
* First, it tries to map provided authorities to Jmix role codes if implementation of {@link LdapAuthorityToJmixRoleCodesMapper}
* is provided. After that, it searches resource and row-level roles with such codes, in case both resource and row-level roles
* with the same code exist both will be returned.
* First, it tries to map provided authorities to Jmix role codes if implementation of
* {@link LdapAuthorityToJmixRoleCodesMapper} is provided. After that, it searches resource and row-level roles with
* such codes, in case both resource and row-level roles with the same code exist both will be returned.
*
* @see LdapAuthorityToJmixRoleCodesMapper
*/
Expand All @@ -42,6 +42,7 @@ public class JmixLdapGrantedAuthoritiesMapper implements GrantedAuthoritiesMappe
private ResourceRoleRepository resourceRoleRepository;
private RowLevelRoleRepository rowLevelRoleRepository;
private LdapAuthorityToJmixRoleCodesMapper authorityToJmixRoleCodeMapper;
private RoleGrantedAuthorityUtils roleGrantedAuthorityUtils;

private List<String> defaultRoles;

Expand All @@ -60,6 +61,11 @@ public void setLdapAuthorityToJmixRoleCodeMapper(LdapAuthorityToJmixRoleCodesMap
this.authorityToJmixRoleCodeMapper = authorityToJmixRoleCodeMapper;
}

@Autowired
public void setRoleGrantedAuthorityUtils(RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
this.roleGrantedAuthorityUtils = roleGrantedAuthorityUtils;
}

public void setDefaultRoles(List<String> roles) {
Assert.notNull(roles, "roles list cannot be null");
this.defaultRoles = roles;
Expand All @@ -69,11 +75,7 @@ public void setDefaultRoles(List<String> roles) {
public Set<GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
Set<GrantedAuthority> mapped = new HashSet<>(authorities.size());
for (GrantedAuthority authority : authorities) {
if (authority instanceof RoleGrantedAuthority) {
mapped.add(authority);
} else {
mapped.addAll(mapAuthority(authority.getAuthority()));
}
mapped.addAll(mapAuthority(authority.getAuthority()));
}

if (this.defaultRoles != null) {
Expand All @@ -99,12 +101,12 @@ protected List<GrantedAuthority> mapRoleCodesToAuthority(Collection<String> role
for (String roleCode : roleCodes) {
ResourceRole resourceRole = resourceRoleRepository.findRoleByCode(roleCode);
if (resourceRole != null) {
authorities.add(RoleGrantedAuthority.ofResourceRole(resourceRole));
authorities.add(roleGrantedAuthorityUtils.createResourceRoleGrantedAuthority(resourceRole));
}

RowLevelRole rowLevelRole = rowLevelRoleRepository.findRoleByCode(roleCode);
if (rowLevelRole != null) {
authorities.add(RoleGrantedAuthority.ofRowLevelRole(rowLevelRole));
authorities.add(roleGrantedAuthorityUtils.createRowLevelRoleGrantedAuthority(rowLevelRole));
}
}

Expand Down
Expand Up @@ -30,6 +30,7 @@
import io.jmix.security.SecurityConfigurers;
import io.jmix.security.configurer.SessionManagementConfigurer;
import io.jmix.security.role.ResourceRoleRepository;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import io.jmix.security.role.RowLevelRoleRepository;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
Expand Down Expand Up @@ -62,8 +63,10 @@ public JmixOidcUserService oidcUserService(OidcUserMapper oidcUserMapper) {
@ConditionalOnBean(ResourceRoleRepository.class)
public ClaimsRolesMapper claimsRoleMapper(ResourceRoleRepository resourceRoleRepository,
RowLevelRoleRepository rowLevelRoleRepository,
OidcProperties oidcProperties) {
DefaultClaimsRolesMapper mapper = new DefaultClaimsRolesMapper(resourceRoleRepository, rowLevelRoleRepository);
OidcProperties oidcProperties,
RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
DefaultClaimsRolesMapper mapper = new DefaultClaimsRolesMapper(resourceRoleRepository,
rowLevelRoleRepository, roleGrantedAuthorityUtils);
mapper.setRolesClaimName(oidcProperties.getDefaultClaimsRolesMapper().getRolesClaimName());
mapper.setResourceRolePrefix(oidcProperties.getDefaultClaimsRolesMapper().getResourceRolePrefix());
mapper.setRowLevelRolePrefix(oidcProperties.getDefaultClaimsRolesMapper().getRowLevelRolePrefix());
Expand Down
@@ -1,17 +1,16 @@
package io.jmix.oidc.claimsmapper;

import io.jmix.oidc.OidcProperties;
import io.jmix.security.model.ResourceRole;
import io.jmix.security.model.RowLevelRole;
import io.jmix.security.role.ResourceRoleRepository;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import io.jmix.security.role.RowLevelRoleRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.*;
import java.util.stream.Stream;

/**
* {@link ClaimsRolesMapper} that can be used as super-class for your own {@link ClaimsRolesMapper} implementations. The
Expand All @@ -27,11 +26,15 @@ public class BaseClaimsRolesMapper implements ClaimsRolesMapper {

protected ResourceRoleRepository resourceRoleRepository;

protected RoleGrantedAuthorityUtils roleGrantedAuthorityUtils;

//todo setter injection?
public BaseClaimsRolesMapper(ResourceRoleRepository resourceRoleRepository,
RowLevelRoleRepository rowLevelRoleRepository) {
RowLevelRoleRepository rowLevelRoleRepository,
RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
this.rowLevelRoleRepository = rowLevelRoleRepository;
this.resourceRoleRepository = resourceRoleRepository;
this.roleGrantedAuthorityUtils = roleGrantedAuthorityUtils;
}

@Override
Expand All @@ -58,7 +61,7 @@ public Collection<RowLevelRole> toRowLevelRoles(Map<String, Object> claims) {
if (jmixRole != null) {
roles.add(jmixRole);
} else {
log.warn("Resource role {} not found", jmixRoleCode);
log.warn("Row-level role {} not found", jmixRoleCode);
}
}
return roles;
Expand All @@ -71,4 +74,21 @@ protected Collection<String> getResourceRolesCodes(Map<String, Object> claims) {
protected Collection<String> getRowLevelRoleCodes(Map<String, Object> claims) {
return Collections.emptySet();
}

/**
* Transforms collection of roles returned by {@link #toResourceRoles(Map)} and {@link #toRowLevelRoles(Map)} into a
* collection of {@link GrantedAuthority}.
*
* @param claims pieces of information about the user returned by the OpenID Provider
* @return a collection of granted authorities that contain information about resource and row-level roles available
* to the user
*/
@Override
public Collection<? extends GrantedAuthority> toGrantedAuthorities(Map<String, Object> claims) {
Stream<GrantedAuthority> resourceRoleAuthoritiesStream = toResourceRoles(claims).stream()
.map(roleGrantedAuthorityUtils::createResourceRoleGrantedAuthority);
Stream<GrantedAuthority> rowLevelRoleAuthoritiesStream = toRowLevelRoles(claims).stream()
.map(roleGrantedAuthorityUtils::createRowLevelRoleGrantedAuthority);
return Stream.concat(resourceRoleAuthoritiesStream, rowLevelRoleAuthoritiesStream).toList();
}
}
@@ -1,15 +1,11 @@
package io.jmix.oidc.claimsmapper;

import io.jmix.security.authentication.RoleGrantedAuthority;
import io.jmix.security.model.ResourceRole;
import io.jmix.security.model.RowLevelRole;
import org.springframework.security.core.GrantedAuthority;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Mapper of claims received from the OpenID Provider into Jmix resource roles and row-level roles. Some {@link io.jmix.oidc.usermapper.OidcUserMapper}
Expand All @@ -24,24 +20,5 @@ public interface ClaimsRolesMapper {

Collection<RowLevelRole> toRowLevelRoles(Map<String, Object> claims);

/**
* Transforms collection of roles returned by {@link #toResourceRoles(Map)} and {@link #toRowLevelRoles(Map)} into a
* collection of {@link GrantedAuthority}.
*
* @param claims pieces of information about the user returned by the OpenID Provider
* @return a collection of granted authorities that contain information about resource and row-level roles available
* to the user
*/
default Collection<? extends GrantedAuthority> toGrantedAuthorities(Map<String, Object> claims) {
List<RoleGrantedAuthority> resourceRoleAuthorities = toResourceRoles(claims).stream()
.map(RoleGrantedAuthority::ofResourceRole)
.collect(Collectors.toList());
List<RoleGrantedAuthority> rowLevelRoleAuthorities = toRowLevelRoles(claims).stream()
.map(RoleGrantedAuthority::ofRowLevelRole)
.collect(Collectors.toList());
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.addAll(resourceRoleAuthorities);
grantedAuthorities.addAll(rowLevelRoleAuthorities);
return grantedAuthorities;
}
Collection<? extends GrantedAuthority> toGrantedAuthorities(Map<String, Object> claims);
}
Expand Up @@ -2,6 +2,7 @@

import io.jmix.oidc.OidcProperties;
import io.jmix.security.role.ResourceRoleRepository;
import io.jmix.security.role.RoleGrantedAuthorityUtils;
import io.jmix.security.role.RowLevelRoleRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -35,8 +36,9 @@ public class DefaultClaimsRolesMapper extends BaseClaimsRolesMapper {
protected String rowLevelRolePrefix = "";

public DefaultClaimsRolesMapper(ResourceRoleRepository resourceRoleRepository,
RowLevelRoleRepository rowLevelRoleRepository) {
super(resourceRoleRepository, rowLevelRoleRepository);
RowLevelRoleRepository rowLevelRoleRepository,
RoleGrantedAuthorityUtils roleGrantedAuthorityUtils) {
super(resourceRoleRepository, rowLevelRoleRepository, roleGrantedAuthorityUtils);
}

@Override
Expand Down

0 comments on commit 44f5b2b

Please sign in to comment.