When updating to Spring Security 6, the JSR250 annotation #RolesAllowed on my #RestController doesn't take the defined roleHierarchy into account.
Related to: AccessDecisionVoter Deprecated with Spring Security 6.x
Since Spring Security 6, the AccessDecisionVoter is deprecated and the suggested way, from the thread above, is to "simply expose a expressionHandler". This didn't work for me with JSR250 enabled.
#Bean
public DefaultMethodSecurityExpressionHandler expressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "a > b";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
It seems like the created AuthorityAuthorizationManager by Jsr250AuthorizationManagerRegistry.resolveManager for RolesAllowed doesn't take the expressionHandler nor DefaultMethodSecurityExpressionHandler into account.
The AuthorityAuthorizationManager does have a field for a roleHierarchy to be set, but I couldn't figure out how or when this is supposed to be called.
I would have commented on the related post above but unfortunately I don't have the required reputation.
Unfortunately there is no support to add role hierarchy to JSR250 manager. But there is a workaround that basically clones the library's implementation. At this point it makes more sense to drop JSR250 since you will be just replicating the logic from the libs to your code base, but if you want to do it anyway, just follow these instructions:
Create a custom manager that will deal with the JSR250 annotations:
public class Jsr250CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {
static final AuthorizationManager<MethodInvocation> NULL_MANAGER = (a, o) -> null;
private final RoleHierarchy roleHierarchy;
public Jsr250CustomAuthorizationManager(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
#Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation methodInvocation) {
var manager = resolveManager(methodInvocation);
return manager.check(authentication, methodInvocation);
}
public AuthorizationManager<MethodInvocation> resolveManager(MethodInvocation methodInvocation) {
if (hasDenyAll(methodInvocation)) {
return (a, o) -> new AuthorizationDecision(false);
}
if (hasPermitAll(methodInvocation)) {
return (a, o) -> new AuthorizationDecision(true);
}
if (hasRolesAllowed(methodInvocation)) {
var rolesAllowed = (RolesAllowed) methodInvocation.getMethod().getAnnotation(RolesAllowed.class);
var manager = AuthorityAuthorizationManager.<MethodInvocation>hasAnyRole(rolesAllowed.value());
manager.setRoleHierarchy(roleHierarchy);
return manager;
}
return NULL_MANAGER;
}
public boolean hasDenyAll(MethodInvocation methodInvocation) {
return methodInvocation.getMethod().getAnnotation(DenyAll.class) != null;
}
public boolean hasPermitAll(MethodInvocation methodInvocation) {
return methodInvocation.getMethod().getAnnotation(PermitAll.class) != null;
}
public boolean hasRolesAllowed(MethodInvocation methodInvocation) {
return methodInvocation.getMethod().getAnnotation(RolesAllowed.class) != null;
}
}
You can just copy and paste the code above into a new class in your code, this is a class that I made myself based on the one that exists in the spring security lib.
expose this new instance as a bean of type Advisor:
#EnableMethodSecurity(jsr250Enabled = true)
public class MethodSecuritytConfiguration {
#Bean
public Advisor jsr250AuthorizationMethodInterceptor() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
var manager = new Jsr250CustomAuthorizationManager(roleHierarchy);
return AuthorizationManagerBeforeMethodInterceptor.jsr250(manager);
}
}
In case you are using #EnableMethodSecurity(jsr250Enabled = true), you will also need to add this configuration to your application.properties:
spring.main.allow-bean-definition-overriding=true
This will allow to override a bean that is defined in the security lib that deals with JSR250 annotations. Because this bean's configuration is hardcoded in the lib wihtout exposing a way to change it, we have to override it altogheter to add the behavior we need. Note that the name of your bean needs to be jsr250AuthorizationMethodInterceptor to override the one (with same name) from the security lib. If you remove the jsr250Enabled configuration from EnableMethodSecurity, then you can name your bean anything you want and remove the configuration from application.properties.
Related
I'm developing application using Spring Boot, and I'm using Swagger to auto-generate API docs and also I use swagger-ui.html to interact with those APIs.
I have Spring Security enabled too, and I have Users with different roles. Different REST APIs are available to different roles.
Question: how do I configure Swagger to respect Spring's #Secured annotation and trim operations displayed by swagger-ui.html so that only operations available to current user are available?
I.e. imagine following controller
#RestController
#Secured(ROLE_USER)
public void SomeRestController {
#GetMapping
#Secured(ROLE_USER_TOP_MANAGER)
public String getInfoForTopManager() { /*...*/ }
#GetMapping
#Secured(ROLE_USER_MIDDLE_MANAGER)
public String getInfoForMiddleManager() { /*...*/ }
#GetMapping
public String getInfoForAnyUser() { /*...*/ }
}
Swagger will show both operations getInfoForTopManager and getInfoForMiddleManager regardless of current user role. In case currently authenticated user role is ROLE_USER_MIDDLE_MANAGER, I want only getInfoForMiddleManager and getInfoForAnyUser operations to be available in the Swagger.
Ok, I think found good solution to that question. Solution consists of 2 parts:
Extend controllers scanning logic through OperationBuilderPlugin to retain roles in the Swagger's vendor extensions
Override ServiceModelToSwagger2MapperImpl bean to filter out actions based on current security context
In your project this might look a bit different (i.e. most likely you don't have thing like securityContextResolver), but I believe you'll get the gist of this solution from following code:
Part 1: Extend controllers scanning logic to retain roles in the Swagger's vendor extensions
#Component
#Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1000)
public class OperationBuilderPluginSecuredAware implements OperationBuilderPlugin {
#Override
public void apply(OperationContext context) {
Set<String> roles = new HashSet<>();
Secured controllerAnnotation = context.findControllerAnnotation(Secured.class).orNull();
if (controllerAnnotation != null) {
roles.addAll(List.of(controllerAnnotation.value()));
}
Secured methodAnnotation = context.findAnnotation(Secured.class).orNull();
if (methodAnnotation != null) {
roles.addAll(List.of(methodAnnotation.value()));
}
if (!roles.isEmpty()) {
context.operationBuilder().extensions(List.of(new TrimToRoles(roles.toArray(new String[0]))));
}
}
#Override
public boolean supports(DocumentationType delimiter) {
return SwaggerPluginSupport.pluginDoesApply(delimiter);
}
}
Part 2: Filter out actions based on current security context
#Primary
#Component
public class ServiceModelToSwagger2MapperImplEx extends ServiceModelToSwagger2MapperImpl {
#Autowired
private SecurityContextResolver<User> securityContextResolver;
#Override
protected io.swagger.models.Operation mapOperation(Operation from) {
if (from == null) {
return null;
}
if (!isPermittedForCurrentUser(findTrimToRolesExtension(from.getVendorExtensions()))) {
return null;
}
return super.mapOperation(from);
}
private boolean isPermittedForCurrentUser(TrimToRoles trimToRoles) {
if (trimToRoles == null) {
return true;
}
if (securityContextResolver.hasAnyRole(trimToRoles.getValue())) {
return true;
}
return false;
}
private TrimToRoles findTrimToRolesExtension(#SuppressWarnings("rawtypes") List<VendorExtension> list) {
if (CollectionUtils.isEmpty(list)) {
return null;
}
return list.stream().filter(x -> x instanceof TrimToRoles).map(TrimToRoles.class::cast).findFirst()
.orElse(null);
}
#Override
protected Map<String, Path> mapApiListings(Multimap<String, ApiListing> apiListings) {
Map<String, Path> paths = super.mapApiListings(apiListings);
return paths.entrySet().stream().filter(x -> !x.getValue().isEmpty())
.collect(Collectors.toMap(x -> x.getKey(), v -> v.getValue()));
}
#Override
public Swagger mapDocumentation(Documentation from) {
Swagger ret = super.mapDocumentation(from);
Predicate<? super Tag> hasAtLeastOneOperation = tag -> ret.getPaths().values().stream()
.anyMatch(x -> x.getOperations().stream().anyMatch(y -> y.getTags().contains(tag.getName())));
ret.setTags(ret.getTags().stream().filter(hasAtLeastOneOperation).collect(Collectors.toList()));
return ret;
}
}
p.s. these impls are not efficient, but given their usage scenarios I preferred simple impl
I have an old code base that I need to refactor using Java 8, so I have an interface, which tells whether my current site supports the platform.
public interface PlatformSupportHandler {
public abstract boolean isPaltformSupported(String platform);
}
and I have multiple classes implementing it and each class supports a different platform.
A few of the implementing classes are:
#Component("bsafePlatformSupportHandler")
public class BsafePlatoformSupportHandler implements PlatformSupportHandler {
String[] supportedPlatform = {"iPad", "Android", "iPhone"};
Set<String> supportedPlatformSet = new HashSet<>(Arrays.asList(supportedPlatform));
#Override
public boolean isPaltformSupported(String platform) {
return supportedPlatformSet.contains(platform);
}
}
Another implementation:
#Component("discountPlatformSupportHandler")
public class DiscountPlatoformSupportHandler implements PlatformSupportHandler{
String[] supportedPlatform = {"Android", "iPhone"};
Set<String> supportedPlatformSet = new HashSet<>(Arrays.asList(supportedPlatform));
#Override
public boolean isPaltformSupported(String platform) {
return supportedPlatformSet.contains(platform);
}
}
At runtime in my filter, I get the required bean which I want:
platformSupportHandler = (PlatformSupportHandler) ApplicationContextUtil
.getBean(subProductType + Constants.PLATFORM_SUPPORT_HANDLER_APPEND);
and call isPlatformSupported to get whether my current site supports the following platform or not.
I am new to Java 8, so is there any way I can refactor this code without creating multiple classes? As the interface only contains one method, can I somehow use lambda to refactor it?
If you want to stick to the current design, you could do something like this:
public class MyGeneralPurposeSupportHandler implements PlatformSupportHandler {
private final Set<String> supportedPlatforms;
public MyGeneralPurposeSupportHandler(Set<String> supportedPlatforms) {
this.supportedPlatforms = supportedPlatforms;
}
public boolean isPlatformSupported(String platform) {
return supportedPlatforms.contains(platform);
}
}
// now in configuration:
#Configuration
class MySpringConfig {
#Bean
#Qualifier("discountPlatformSupportHandler")
public PlatformSupportHandler discountPlatformSupportHandler() {
return new MyGeneralPurposeSupportHandler(new HashSefOf({"Android", "iPhone"})); // yeah its not a java syntax, but you get the idea
}
#Bean
#Qualifier("bsafePlatformSupportHandler")
public PlatformSupportHandler bsafePlatformSupportHandler() {
return new MyGeneralPurposeSupportHandler(new HashSefOf({"Android", "iPhone", "iPad"}));
}
}
This method has an advantage of not creating class per type (discount, bsafe, etc), so this answers the question.
Going step further, what happens if there no type that was requested, currently it will fail because the bean does not exist in the application context - not a really good approach.
So you could create a map of type to the set of supported platforms, maintain the map in the configuration or something an let spring do its magic.
You'll end up with something like this:
public class SupportHandler {
private final Map<String, Set<String>> platformTypeToSuportedPlatforms;
public SupportHandler(Map<String, Set<String>> map) {
this.platformTypeToSupportedPlatforms = map;
}
public boolean isPaltformSupported(String type) {
Set<String> supportedPlatforms = platformTypeToSupportedPlatforms.get(type);
if(supportedPlatforms == null) {
return false; // or maybe throw an exception, the point is that you don't deal with spring here which is good since spring shouldn't interfere with your business code
}
return supportedPlatforms.contains(type);
}
}
#Configuration
public class MyConfiguration {
// Configuration conf is supposed to be your own way to read configurations in the project - so you'll have to implement it somehow
#Bean
public SupportHandler supportHandler(Configuration conf) {
return new SupportHandler(conf.getRequiredMap());
}
}
Now if you follow this approach, adding a new supported types becomes codeless at all, you only add a configuration, by far its the best method I can offer.
Both methods however lack the java 8 features though ;)
You can use the following in your config class where you can create beans:
#Configuration
public class AppConfiguration {
#Bean(name = "discountPlatformSupportHandler")
public PlatformSupportHandler discountPlatformSupportHandler() {
String[] supportedPlatforms = {"Android", "iPhone"};
return getPlatformSupportHandler(supportedPlatforms);
}
#Bean(name = "bsafePlatformSupportHandler")
public PlatformSupportHandler bsafePlatformSupportHandler() {
String[] supportedPlatforms = {"iPad", "Android", "iPhone"};
return getPlatformSupportHandler(supportedPlatforms);
}
private PlatformSupportHandler getPlatformSupportHandler(String[] supportedPlatforms) {
return platform -> Arrays.asList(supportedPlatforms).contains(platform);
}
}
Also, when you want to use the bean, it is again very easy:
#Component
class PlatformSupport {
// map of bean name vs bean, automatically created by Spring for you
private final Map<String, PlatformSupportHandler> platformSupportHandlers;
#Autowired // Constructor injection
public PlatformSupport(Map<String, PlatformSupportHandler> platformSupportHandlers) {
this.platformSupportHandlers = platformSupportHandlers;
}
public void method1(String subProductType) {
PlatformSupportHandler platformSupportHandler = platformSupportHandlers.get(subProductType + Constants.PLATFORM_SUPPORT_HANDLER_APPEND);
}
}
As it was written in Mark Bramnik's answer you can move this to configuration.
Suppose that it would be in yaml in that way:
platforms:
bsafePlatformSupportHandler: ["iPad", "Android", "iPhone"]
discountPlatformSupportHandler: ["Android", "iPhone"]
Then you can create config class to read this:
#Configuration
#EnableConfigurationProperties
#ConfigurationProperties
public class Config {
private Map<String, List<String>> platforms = new HashMap<String, List<String>>();
// getters and setters
You can than create handler with checking code.
Or place it in your filter like below:
#Autowired
private Config config;
...
public boolean isPlatformSupported(String subProductType, String platform) {
String key = subProductType + Constants.PLATFORM_SUPPORT_HANDLER_APPEND;
return config.getPlatforms()
.getOrDefault(key, Collections.emptyList())
.contains(platform);
}
I want implement strategy design pattern in spring boot application. I create BeanPostProcessor for construct strategy resolver:
#Component
public class HandlerInAnnotationBeanPostProcessor implements BeanPostProcessor {
private final UnpHandlersResolver unpHandlersResolver;
public HandlerInAnnotationBeanPostProcessor(UnpHandlersResolver unpHandlersResolver) {
this.unpHandlersResolver = unpHandlersResolver;
}
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Annotation[] annotations = bean.getClass().getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof HandlerIn) {
if (bean.getClass() != UnpHandler.class)
throw new RuntimeException("Not UnpHandler bean annotated by HandlerIn");
SmevMessageType[] type = ((HandlerIn) annotation).type();
for (SmevMessageType smevMessageType : type) {
unpHandlersResolver.setHandler(smevMessageType, (UnpHandler) bean);
}
}
}
return bean;
}
}
And I create resolver:
#Slf4j
#Component
public class UnpHandlersResolverImpl implements UnpHandlersResolver {
private Map<SmevMessageType, UnpHandler> map = new HashMap<>();
#Override
public void setHandler(SmevMessageType messageType, UnpHandler unpHandler) {
map.put(messageType, unpHandler);
}
#Override
public UnpHandler getUnpHandler(SmevMessageType type) {
UnpHandler sendRequestHandler = map.get(type);
if (sendRequestHandler == null)
throw new IllegalArgumentException("Invalid SendRequestHandler type: " + type);
return sendRequestHandler;
}
}
My BeanPostProcessor scan all beans with annotation HandlerIn and add to resolver's mup. I think it's wrong to do that:
unpHandlersResolver.setHandler(smevMessageType, (UnpHandler) bean);
But I not understand how can I add find beans to resolver. Before this implementation I faind beans in #Postconstruct method of resolver like:
context.getBeansWithAnnotation(HandlerIn.class);
But in this solution I have context in resolver and I think is bad.
Tell me how to properly implement what I want? In short, I want to have a set of classes that implement different behaviors. And the class that controls them. Give the class a parameter so that he chooses the right strategy and gives it to me. Like this:
Handler handler = handlersResolver.getHandler(messageType);
Result result = handler.somthing(param);
I'm going to try to make a simple example.
Interface Greeting {
void sayHello();
String getSupportedLanguage();
}
Then you have X number of implementations and you can loop through them in your "resolver"'s constructor to build the map. (I've seen this called a Proxy or a Decorator in code though, i.e. GreetingProxy or GreetingDecorator)
#Service
public GreetingResolver {
private Map<String, Greeting> languageToGreetingMap = new HashMap<>();
#Autowired
public GreetingResolver(List<Greeting> greetings) {
for (Greeting greeting : greetings) {
languageToGreetingMap.put(greeting.getSupportedLanguage(), greeting);
}
}
public void sayGreetingForLanguage(String language) {
languageToGreetingMap.get(language).sayHello();
}
}
This is a basic example of how one can do the strategy pattern in Spring. Every interface implementation of "Greeting" only knows about itself and what it can support. We then autowire all implementations in a list and loop through to create the map once and then during runtime only the relevant entry from the map in retrieved and used.
Note: this was typed "free hand" directly in the web page so please forgive any typos in the code.
I am converting my existing Spring Application to a Spring Boot Application. In my existing application, we have the need to connect to multiple databases and we had achieved this by having multiple data sources defined and fetching the corresponding bean based on the condition. The transaction manager were also selected using a custom implementation of TransactionInterceptor.
#Override
public TransactionAttributeSource getTransactionAttributeSource() {
final TransactionAttributeSource origTxAttrSource = super.getTransactionAttributeSource();
return new TransactionAttributeSource() {
#Override
public TransactionAttribute getTransactionAttribute(final Method method, final Class<?> targetClass) {
TransactionAttribute txAttr = origTxAttrSource.getTransactionAttribute(method, targetClass);
String database = (String) ThreadContext.get("database");
if (database != null && StringUtils.isNotBlank(database)) {
if (txAttr instanceof DefaultTransactionAttribute) {
((DefaultTransactionAttribute) txAttr).setQualifier("txManager" + database);
}
}
return txAttr;
}
};
}
Through a BeanFactoryPostProcessor we were including this interceptor
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
String[] names = beanFactory.getBeanNamesForType(TransactionInterceptor.class);
for (String name : names) {
BeanDefinition bd = beanFactory.getBeanDefinition(name);
bd.setBeanClassName(MyTransactionInterceptor.class.getName());
}
}
This worked perfectly fine in Spring 4.X.
Now that we are moving towards Spring Boot, I am trying to convert the same approach. I can see that the bean factory is getting called but I don't find calls happening to the Custom Interceptor class. This results in my #Transactional to fail as there are more than one qualifying bean.
Am I missing something with regards to the Spring Boot Configuration?
(This approach of dynamic transaction management was through a reference blog http://blog.tirasa.net/dynamic-springs--at-transactional.html)
The final answer turned out to be setting the factory classes and the factory bean name to null which resulted in the transaction interceptor being invoked. I am yet to figure out how this affects the interceptor call as with the values in these fields (they point to the ProxyTransaction classes as transactionInterceptor bean is created by it).
The final code was of the form -
TransactionInterceptor Class
#Component
public class TransactionInterceptorReplacer implements BeanFactoryPostProcessor {
#Override
public void postProcessBeanFactory(final ConfigurableListableBeanFactory factory) throws BeansException {
String[] names = factory.getBeanNamesForType(TransactionInterceptor.class);
for (String name : names) {
BeanDefinition bd = factory.getBeanDefinition(name);
bd.setBeanClassName(MyTransactionInterceptor.class.getName());
bd.setFactoryBeanName(null);
bd.setFactoryMethodName(null);
}
}
}
First of all, according to Spring doc
, if i want to map user roles to scopes, i should use setCheckUserScopes(true) to DefaultOAuth2RequestFactory. So one way to do this, is injecting my own DefaultOAuth2RequestFactory bean, as doc says:
The AuthorizationServerEndpointsConfigurer allows you to inject a custom OAuth2RequestFactory so you can use that feature to set up a factory if you use #EnableAuthorizationServer.
Then i do
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends
AuthorizationServerConfigurerAdapter {
...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
.tokenServices(tokenServices());
endpoints
.getOAuth2RequestFactory(); // this doesn't return me my own DefaultOAuth2RequestFactory
}
#Bean
#Primary
public OAuth2RequestFactory defaultOAuth2RequestFactory() {
DefaultOAuth2RequestFactory defaultOAuth2RequestFactory = new DefaultOAuth2RequestFactory(
clientDetailsService);
defaultOAuth2RequestFactory.setCheckUserScopes(true);
return defaultOAuth2RequestFactory;
}
}
EDIT
I've overlooked the method requestFactory() from AuthorizationServerEndpointsConfigurer. That was the correct way to pass it to Spring Security. Setting OAuth2RequestFactory bean as primary didn't work. I've deleted some things to focus on the real problem:
After this observation, the actual problem:
as i understand, if the user has authorities A and B, and the app has scope A, then he gets just 'A' scope. But this is not happening. What is really happening is that if app has scope A, and APP (not user) has authorities A and B, then user gets A. But this doesn't make any sense.
This is DefaultOAuth2RequestFactory method that resolve user's scopes:
private Set<String> extractScopes(Map<String, String> requestParameters, String clientId) {
... // I avoid some unimportant lines to not make this post so long
if ((scopes == null || scopes.isEmpty())) {
scopes = clientDetails.getScope();
}
if (checkUserScopes) {
scopes = checkUserScopes(scopes, clientDetails);
}
return scopes;
}
private Set<String> checkUserScopes(Set<String> scopes, ClientDetails clientDetails) {
if (!securityContextAccessor.isUser()) {
return scopes;
}
Set<String> result = new LinkedHashSet<String>();
Set<String> authorities = AuthorityUtils.authorityListToSet(securityContextAccessor.getAuthorities());
for (String scope : scopes) {
if (authorities.contains(scope) || authorities.contains(scope.toUpperCase())
|| authorities.contains("ROLE_" + scope.toUpperCase())) {
result.add(scope);
}
}
return result;
}
Is this a bug? Please tell me if i am wrong. Regards
You need to wire your OAuth2RequestFactory by code something like here.
If the authorities are set by ClientDetailsService then you should be good. If you are looking to map logged-in user authorities I don't have luck there either.