SUPPORT-9363: permissions

This commit is contained in:
adel.ka 2025-08-29 17:20:12 +03:00
parent 44681d41d3
commit 3fef27e79e
28 changed files with 227 additions and 115 deletions

View file

@ -18,8 +18,7 @@ import ru.micord.ervu.account_applications.component.model.dto.SearchRequest;
import ru.micord.ervu.account_applications.component.model.dto.SearchResponse;
import ru.micord.ervu.account_applications.dao.RecruitmentDao;
import ru.micord.ervu.account_applications.security.context.SecurityContext;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.role.ErvuRoleAuthority;
import ru.micord.ervu.account_applications.security.model.role.ErvuPermissionAuthority;
import ru.micord.ervu.account_applications.service.AccountFetchService;
import ru.micord.ervu.account_applications.util.StringUtils;
import service.GridService;
@ -110,10 +109,9 @@ public class AccountGridLoadService extends Behavior implements GridService {
Filter[] filters) {
int page = (offset / limit) + 1;
Map<String, Object> filterMap = new HashMap<>();
UserSession userSession = securityContext.getUserSession();
Set<ErvuRoleAuthority> roles = userSession.roles();
Set<ErvuPermissionAuthority> permissions = securityContext.getPermissions();
if (ervuSecurityRole == null
|| roles.stream().noneMatch(role -> role.getAuthority().equals(ervuSecurityRole))) {
|| permissions.stream().noneMatch(permission -> permission.getAuthority().equals(ervuSecurityRole))) {
List<String> domainIds = recruitmentDao.getRecruitmentIdsWithParentByDomainId(securityContext.getDomainId());
filterMap.put(DOMAIN_IDS, domainIds.toArray());
}

View file

@ -11,7 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.micord.ervu.account_applications.component.dao.AuditDao;
import ru.micord.ervu.account_applications.security.context.SecurityContext;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.UserSession;
import utils.DateTimeUtil;
import ru.cg.webbpm.modules.database.api.bean.TableFieldData;

View file

@ -13,8 +13,7 @@ import model.grid.SortInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import ru.micord.ervu.account_applications.dao.RecruitmentDao;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.role.ErvuRoleAuthority;
import ru.micord.ervu.account_applications.security.model.role.ErvuPermissionAuthority;
import service.GridV2ServiceImpl;
import ru.cg.webbpm.modules.database.api.dao.option.LoadOptions;
@ -49,12 +48,11 @@ public class RecruitmentGridService extends GridV2ServiceImpl {
@Override
public GridRows loadData(Integer offset, Integer limit, Filter[] filters, SortInfo[] sortInfos) {
UserSession userSession = securityContext.getUserSession();
Set<ErvuRoleAuthority> roles = userSession.roles();
Set<ErvuPermissionAuthority> permissions = securityContext.getPermissions();
List<String> recruitmentIds;
List<Filter> updatedFilters = new ArrayList<>(Arrays.asList(filters));
Optional<Filter> recruitmentFilterOpt = findRecruitmentFilter(updatedFilters);
if (ervuSecurityRole != null && roles.stream().anyMatch(role -> role.getAuthority().equals(
if (ervuSecurityRole != null && permissions.stream().anyMatch(permission -> permission.getAuthority().equals(
ervuSecurityRole))) {
recruitmentIds = recruitmentFilterOpt.map(
filter -> getChildRecruitmentIds(updatedFilters, filter))

View file

@ -14,8 +14,7 @@ import org.springframework.stereotype.Service;
import ru.micord.ervu.account_applications.component.model.TreeItemDto;
import ru.micord.ervu.account_applications.component.rpc.TreeItemRpcService;
import ru.micord.ervu.account_applications.security.context.SecurityContext;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.role.ErvuRoleAuthority;
import ru.micord.ervu.account_applications.security.model.role.ErvuPermissionAuthority;
import ru.cg.webbpm.modules.database.api.bean.TableRow;
import ru.cg.webbpm.modules.database.api.dao.option.LoadOptions;
@ -56,10 +55,10 @@ public class TreeItemService {
public List<TreeItemDto> loadTreeData() {
String domainId = securityContext.getDomainId();
UserSession userSession = securityContext.getUserSession();
Set<ErvuRoleAuthority> roles = userSession.roles();
Set<ErvuPermissionAuthority> permissions = securityContext.getPermissions();
List<TreeItemDto> filteredTreeItems = loadTreeItems();
if (ervuSecurityRole == null || roles.stream().noneMatch(role -> role.getAuthority().equals(
if (ervuSecurityRole == null || permissions.stream().noneMatch(permission -> permission.getAuthority().equals(
ervuSecurityRole))) {
filteredTreeItems = filteredTreeItems.stream()
.filter(item -> item.domainId.equalsIgnoreCase(domainId))

View file

@ -1,7 +1,6 @@
package ru.micord.ervu.account_applications.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
@ -26,21 +25,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ErvuJwtAuthenticationProvider jwtAuthenticationProvider;
@Value("${security.roles.allowed:#{null}}")
private String[] allowedRoles;
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> {
auth.antMatchers("/version").permitAll();
auth.antMatchers("/session").authenticated();
if (allowedRoles != null && allowedRoles.length > 0) {
auth.anyRequest().hasAnyAuthority(allowedRoles);
}
else {
auth.anyRequest().authenticated();
}
auth.anyRequest().authenticated();
})
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

View file

@ -1,11 +1,16 @@
package ru.micord.ervu.account_applications.security.context;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import java.util.Set;
import ru.micord.ervu.account_applications.security.model.UserSession;
import ru.micord.ervu.account_applications.security.model.role.ErvuPermissionAuthority;
public interface SecurityContext {
String getDomainId();
Set<ErvuPermissionAuthority> getPermissions();
String getUserId();
String getToken();

View file

@ -1,9 +1,12 @@
package ru.micord.ervu.account_applications.security.context;
import java.util.Set;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.UserSession;
import ru.micord.ervu.account_applications.security.model.jwt.authentication.JwtTokenAuthentication;
import ru.micord.ervu.account_applications.security.model.role.ErvuPermissionAuthority;
@Component
@ -15,6 +18,12 @@ public class SecurityContextImpl
return auth != null ? auth.getUserSession().domainId() : null;
}
@Override
public Set<ErvuPermissionAuthority> getPermissions() {
JwtTokenAuthentication auth = (JwtTokenAuthentication) SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getUserSession().permissions() : null;
}
@Override
public String getUserId() {
JwtTokenAuthentication auth = (JwtTokenAuthentication) SecurityContextHolder.getContext().getAuthentication();

View file

@ -6,8 +6,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.micord.ervu.account_applications.security.context.SecurityContext;
import ru.micord.ervu.account_applications.security.dto.SessionDto;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.role.ErvuRoleAuthority;
import ru.micord.ervu.account_applications.security.model.UserSession;
import ru.micord.ervu.account_applications.security.model.role.ErvuPermissionAuthority;
@RestController
@ -26,9 +26,9 @@ public class AuthorizationController {
userSession.userId(),
userSession.name(),
userSession.realm(),
userSession.roles()
userSession.permissions()
.stream()
.map(ErvuRoleAuthority::getAuthority)
.map(ErvuPermissionAuthority::getAuthority)
.collect(Collectors.toSet()),
userSession.domainId()
);

View file

@ -6,6 +6,6 @@ public record SessionDto(
String userId,
String name,
String realm,
Set<String> roles,
Set<String> permissions,
String domainId
) {}

View file

@ -56,9 +56,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (token == null) {
return null;
}
Authentication authentication = new JwtTokenDummy(token);
authentication = authenticationManager.authenticate(authentication);
return authentication;
String permissionsHeader = extractPermissionsHeaderFromRequest(request);
Authentication authentication = new JwtTokenDummy(token, permissionsHeader);
return authenticationManager.authenticate(authentication);
}
private String extractAuthTokenFromRequest(HttpServletRequest request) {
@ -68,4 +69,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
.map(String::trim)
.orElse(null);
}
private String extractPermissionsHeaderFromRequest(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader("X-User-Permissions"))
.orElse("");
}
}

View file

@ -1,14 +1,14 @@
package ru.micord.ervu.account_applications.security.model.jwt;
package ru.micord.ervu.account_applications.security.model;
import java.util.Set;
import ru.micord.ervu.account_applications.security.model.role.ErvuRoleAuthority;
import ru.micord.ervu.account_applications.security.model.role.ErvuPermissionAuthority;
public record UserSession(
String userId,
String name,
String realm,
Set<ErvuRoleAuthority> roles,
Set<ErvuPermissionAuthority> permissions,
String domainId
) {
@ -20,7 +20,7 @@ public record UserSession(
private String userId;
private String name;
private String realm;
private Set<ErvuRoleAuthority> roles;
private Set<ErvuPermissionAuthority> permissions;
private String domainId;
private UserSessionBuilder() {
@ -41,8 +41,8 @@ public record UserSession(
return this;
}
public UserSessionBuilder withRoles(Set<ErvuRoleAuthority> roles) {
this.roles = roles;
public UserSessionBuilder withPermissions(Set<ErvuPermissionAuthority> permissions) {
this.permissions = permissions;
return this;
}
@ -52,7 +52,7 @@ public record UserSession(
}
public UserSession build() {
return new UserSession(userId, name, realm, roles, domainId);
return new UserSession(userId, name, realm, permissions, domainId);
}
}
}

View file

@ -0,0 +1,47 @@
package ru.micord.ervu.account_applications.security.model.jwt;
public record UserClaims(
String userId,
String name,
String realm,
String domainId
) {
public static UserClaimsBuilder builder() {
return new UserClaimsBuilder();
}
public static final class UserClaimsBuilder {
private String userId;
private String name;
private String realm;
private String domainId;
private UserClaimsBuilder() {
}
public UserClaimsBuilder withUserId(String userId) {
this.userId = userId;
return this;
}
public UserClaimsBuilder withName(String name) {
this.name = name;
return this;
}
public UserClaimsBuilder withRealm(String realm) {
this.realm = realm;
return this;
}
public UserClaimsBuilder withDomainId(String domainId) {
this.domainId = domainId;
return this;
}
public UserClaims build() {
return new UserClaims(userId, name, realm, domainId);
}
}
}

View file

@ -4,7 +4,7 @@ import java.util.Collection;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.UserSession;
public class JwtTokenAuthentication implements Authentication {
@ -22,7 +22,7 @@ public class JwtTokenAuthentication implements Authentication {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return userSession.roles();
return userSession.permissions();
}
@Override

View file

@ -9,15 +9,21 @@ import org.springframework.security.core.GrantedAuthority;
public class JwtTokenDummy implements Authentication {
private final String token;
private final String permissionsHeader;
public JwtTokenDummy(String token) {
public JwtTokenDummy(String token, String permissionsHeader) {
this.token = token;
this.permissionsHeader = permissionsHeader;
}
public String getToken() {
return token;
}
public String getPermissionsHeader() {
return permissionsHeader;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
throw new NotImplementedException("Not implemented for dummy");

View file

@ -0,0 +1,16 @@
package ru.micord.ervu.account_applications.security.model.role;
import org.springframework.security.core.GrantedAuthority;
public class ErvuPermissionAuthority implements GrantedAuthority {
private final String permission;
public ErvuPermissionAuthority(String permission) {
this.permission = permission;
}
@Override
public String getAuthority() {
return permission;
}
}

View file

@ -1,17 +0,0 @@
package ru.micord.ervu.account_applications.security.model.role;
import org.springframework.security.core.GrantedAuthority;
public class ErvuRoleAuthority implements GrantedAuthority {
private final String role;
public ErvuRoleAuthority(String role) {
this.role = role;
}
@Override
public String getAuthority() {
return role;
}
}

View file

@ -1,12 +1,20 @@
package ru.micord.ervu.account_applications.security.provider;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import org.springframework.util.StringUtils;
import ru.micord.ervu.account_applications.security.model.jwt.UserClaims;
import ru.micord.ervu.account_applications.security.model.UserSession;
import ru.micord.ervu.account_applications.security.model.jwt.authentication.JwtTokenAuthentication;
import ru.micord.ervu.account_applications.security.model.jwt.authentication.JwtTokenDummy;
import ru.micord.ervu.account_applications.security.model.role.ErvuPermissionAuthority;
import ru.micord.ervu.account_applications.security.service.JwtTokenService;
@Component
@ -22,10 +30,32 @@ public class ErvuJwtAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtTokenDummy jwtTokenDummy = (JwtTokenDummy) authentication;
String jwtToken = jwtTokenDummy.getToken();
UserSession userSession = jwtTokenService.getUserSession(jwtToken);
String permissionHeader = jwtTokenDummy.getPermissionsHeader();
UserClaims userClaims = jwtTokenService.getUserClaims(jwtToken);
Set<ErvuPermissionAuthority> permissions = extractPermissionsFromHeader(permissionHeader);
UserSession userSession = UserSession.builder()
.withDomainId(userClaims.domainId())
.withUserId(userClaims.userId())
.withPermissions(permissions)
.withName(userClaims.name())
.withRealm(userClaims.realm())
.build();
return new JwtTokenAuthentication(userSession, jwtToken);
}
private Set<ErvuPermissionAuthority> extractPermissionsFromHeader(String permissionsHeader) {
if (!StringUtils.hasText(permissionsHeader)) {
return Collections.emptySet();
}
return Arrays.stream(permissionsHeader.split(","))
.map(String::trim)
.filter(StringUtils::hasText)
.map(String::toLowerCase)
.map(ErvuPermissionAuthority::new)
.collect(Collectors.toSet());
}
@Override
public boolean supports(Class<?> authentication) {
return JwtTokenDummy.class.isAssignableFrom(authentication);

View file

@ -3,11 +3,7 @@ package ru.micord.ervu.account_applications.security.service;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
@ -16,8 +12,7 @@ import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import ru.micord.ervu.account_applications.security.exception.JwtProcessingException;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.role.ErvuRoleAuthority;
import ru.micord.ervu.account_applications.security.model.jwt.UserClaims;
@Service
public class ErvuJwtTokenService implements JwtTokenService {
@ -37,24 +32,21 @@ public class ErvuJwtTokenService implements JwtTokenService {
}
@Override
public UserSession getUserSession(String token) {
public UserClaims getUserClaims(String token) {
return enableJwtValidation ? parseToken(token) : unsafeParseToken(token);
}
protected UserSession unsafeParseToken(String token) {
protected UserClaims unsafeParseToken(String token) {
try {
token = token.substring(0, token.lastIndexOf(".") + 1);
JwtParser parser = Jwts.parser();
Claims claims = parser.parseClaimsJwt(token).getBody();
List<String> lowerCaseRoles = getLowerCaseRoles(claims);
return UserSession.builder()
return UserClaims.builder()
.withUserId(claims.getSubject())
.withName(claims.get("name", String.class))
.withRealm(claims.get("realm", String.class))
.withDomainId(claims.get("domain_id", String.class))
.withRoles(getRoles(lowerCaseRoles))
.build();
}
catch (JwtException e) {
@ -62,7 +54,7 @@ public class ErvuJwtTokenService implements JwtTokenService {
}
}
protected UserSession parseToken(String token) {
protected UserClaims parseToken(String token) {
JwtParser parser = Jwts.parser();
try {
if (issuer != null && !issuer.isEmpty()) {
@ -74,14 +66,12 @@ public class ErvuJwtTokenService implements JwtTokenService {
}
Claims claims = parser.parseClaimsJws(token).getBody();
List<String> lowerCaseRoles = getLowerCaseRoles(claims);
return UserSession.builder()
return UserClaims.builder()
.withUserId(claims.getSubject())
.withName(claims.get("name", String.class))
.withRealm(claims.get("realm", String.class))
.withDomainId(claims.get("domain_id", String.class))
.withRoles(getRoles(lowerCaseRoles))
.build();
}
catch (JwtException e) {
@ -89,22 +79,6 @@ public class ErvuJwtTokenService implements JwtTokenService {
}
}
private List<String> getLowerCaseRoles(Claims claims) {
List<String> roleList = claims.get("roles", ArrayList.class);
return roleList != null ?
roleList.stream()
.map(String::toLowerCase)
.collect(Collectors.toList()) :
new ArrayList<>();
}
protected Set<ErvuRoleAuthority> getRoles(List<String> roles) {
return roles
.stream()
.map(ErvuRoleAuthority::new)
.collect(Collectors.toSet());
}
private PublicKey extractPublicKey(String publicKeyStr) {
try {
byte[] decodedPublicKey = Base64.getDecoder().decode(publicKeyStr);

View file

@ -1,8 +1,7 @@
package ru.micord.ervu.account_applications.security.service;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.jwt.UserClaims;
public interface JwtTokenService {
UserSession getUserSession(String token);
UserClaims getUserClaims(String token);
}

View file

@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import ru.micord.ervu.account_applications.component.dao.AuditDao;
import ru.micord.ervu.account_applications.dao.UserApplicationListDao;
import ru.micord.ervu.account_applications.security.context.SecurityContext;
import ru.micord.ervu.account_applications.security.model.jwt.UserSession;
import ru.micord.ervu.account_applications.security.model.UserSession;
import utils.DateTimeUtil;
import static ru.micord.ervu.account_applications.enums.ApplicationStatus.ACCEPTED;