With #RequestMapping, request can be associated with different controller functions through header or request parameters. Is there a way to achieve this base on the user user role? The aim is avoid if statement in the controller.
As far as I am aware, there is not anything that comes out of the box, but if you wanted to you could probably create a custom mapping annotation to do this routing for you.
I have not actually tried any of this code, but something like:
Your new annoation, used like #UserRoleMapping("ROLE_ADMIN")
#Target( ElementType.TYPE )
#Retention(RetentionPolicy.RUNTIME)
public #interface UserRoleMapping {
String[] value();
}
Next, you can just extend the standard Spring RequestMappingHandlerMapping class (this is the class that handles the standard mapping of #RequestMapping annotations). You just need to tell the mapping handler to also take into account a custom condition:
public class UserRoleRequestCondition extends RequestMappingHandlerMapping {
#Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
UserRoleMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, UserRoleMapping.class);
return (typeAnnotation != null) ? new UserRoleRequestCondition( typeAnnotation.value() ) : null;
}
}
The above code just checks the controller for your new annotation created above and if it is found it returns a new condition class, constructed with the value you have set in the annotation (e.g. "ROLE_ADMIN"). This MappingHandler will need to be set in your Spring config (whereever you are currently setting the RequestMappingHandlerMapping, just replace it with one of these).
Next we need to create the custom condition - this is the guy that is going to be invoked on request to determine if a request matches the controller:
public class UserRoleRequestCondition implements RequestCondition<UserRoleRequestCondition> {
private final Set<String> roles;
public UserRoleRequestCondition( String... roles ) {
this( Arrays.asList(roles) );
}
public UserRoleRequestCondition( Collection<String> roles ) {
this.roles = Collections.unmodifiableSet(new HashSet<String>(roles));
}
#Override public UserRoleRequestCondition combine(UserRoleRequestCondition other) {
Set<String> allRoles = new LinkedHashSet<String>(this.roles);
allRoles.addAll(other.roles);
return new UserRoleRequestCondition(allRoles);
}
#Override public UserRoleRequestCondition getMatchingCondition( HttpServletRequest request ) {
UserRoleRequestCondition condition = null;
for (String r : roles){
if ( request.isUserInRole( r ) ){
condition = this;
}
}
return condition;
}
#Override public int compareTo(UserRoleRequestCondition other, HttpServletRequest request) {
return (other.roles - this.roles).size();
}
}
In the above, the method getMatchingCondition is where we match the request. (apologies if I have missed some semi-colons or return keywords etc - this is based on groovy, but hopefully if you are in java you can work out where those bits go!)
Props to Marek for his more detailed answer on the more fully-formed solution to custom routing based on the subdomain that I used when I had to implement something similar! How to implement #RequestMapping custom properties - That gives more details about what is going on, and how to have method level annotations (this example skips that and only defines class level annotations)
I have also written up some notes on this here: http://automateddeveloper.blogspot.co.uk/2014/12/spring-mvc-custom-routing-conditions.html
Implement AuthenticationSuccessHandler onAuthenticationSuccess redirect to specific controller based on the User Role.
Related
I tried to use these two ways to write a customfield and it is recording correctly, but it is keeping customfields between requests
public class LoggerAudit : ILoggerAudit
{
public void AddOnSavingAction(string key, object value)
{
Configuration.AddOnSavingAction(scope =>
{
scope.SetCustomField(key, value);
//scope.Event.CustomFields.Remove(key);
//scope.Event.CustomFields.Add(key, value);
});
}
}
For example:
In the first request my webapi recorded the customfield 'field-A', but in the second request my webapi, there was no need to write this customfield, but it was kept in scope and consequently in my json
I tried this setting, but it didn't work
.WithAction(action =>
{
action.OnEventSaved(scope => scope.Event.CustomFields = new Dictionary<string, object>());
});
The custom actions attached with AddOnSavingAction / OnEventSaved are globally attached and will execute for each and all the events before saving or after saving occurs (respectively), so you should attach each action just once.
But your use case looks like you don't have a way to derive the custom field value from the audit scope, so a custom action will not be useful.
Also I guess you are using Audit.WebApi extension. If that's the case, you won't need a custom action to add a custom field, since you can access the AuditScope with the provided extension methods directly on your controllers or in any place where you can get the current HttpContext, for example:
using Audit.WebApi;
[AuditApi]
public class UsersController : Controller
{
public IHttpActionResult Get(string id)
{
//...
var auditScope = this.GetCurrentAuditScope();
auditScope.SetCustomField("MyField", Guid.NewGuid());
//...
}
}
or just
private void SetCustomField(HttpContext context, string key, object value)
{
var auditScope = context.GetCurrentAuditScope();
auditScope.SetCustomField(key, value);
}
I am attempting to authorize against an external identity provider. Everything seems setup fine, but I keep getting a validation error with my identity provider because the state parameter automatically tacked onto my authorization request is not long enough:
For example:
&state=uYG5DC
The requirements of my IDP say that this state param must be at least 32-characters long. How can I programmatically increase the size of this auto-generated number?
Even if I could generate this number myself, it is not possible to override with other methods I have seen suggested. The following attempt fails because my manual setting of ?state=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz is superceded by the autogenerated param placed after it during the actual request:
#Bean
public OAuth2ProtectedResourceDetails loginGovOpenId() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails() {
#Override
public String getUserAuthorizationUri() {
return super.getUserAuthorizationUri() + "?state=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
}
};
details.setClientId(clientId);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid", "email"));
details.setPreEstablishedRedirectUri(redirectUri);
details.setUseCurrentUri(true);
return details;
}
The 6-character setting seems to be set here, is there a way to override this?
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/util/RandomValueStringGenerator.java
With the help of this post:
spring security StateKeyGenerator custom instance
I was able to come up with a working solution.
In my configuration class marked with these annotations:
#Configuration
#EnableOAuth2Client
I configured the following beans:
#Bean
public OAuth2ProtectedResourceDetails loginGovOpenId() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
AuthorizationCodeResourceDetails details = new
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid", "email"));
details.setPreEstablishedRedirectUri(redirectUri);
details.setUseCurrentUri(true);
return details;
}
#Bean
public StateKeyGenerator stateKeyGenerator() {
return new CustomStateKeyGenerator();
}
#Bean
public AccessTokenProvider accessTokenProvider() {
AuthorizationCodeAccessTokenProvider accessTokenProvider = new AuthorizationCodeAccessTokenProvider();
accessTokenProvider.setStateKeyGenerator(stateKeyGenerator());
return accessTokenProvider;
}
#Bean
public OAuth2RestTemplate loginGovOpenIdTemplate(final OAuth2ClientContext clientContext) {
final OAuth2RestTemplate template = new OAuth2RestTemplate(loginGovOpenId(), clientContext);
template.setAccessTokenProvider(accessTokenProvider());
return template;
}
Where my CustomStateKeyGenerator implementation class looks as follows:
public class CustomStateKeyGenerator implements StateKeyGenerator {
// login.gov requires state to be at least 32-characters long
private static int length = 32;
private RandomValueStringGenerator generator = new RandomValueStringGenerator(length);
#Override
public String generateKey(OAuth2ProtectedResourceDetails resource) {
return generator.generate();
}
}
Originally I just had MyRestController
#CrossOrigin(origins = "*")
#RestController
public class MyRestController {
#RequestMapping(value = "/v1/endpoint", method = {RequestMethod.GET})
public ResponseEntity<Object> endpoint(HttpServletRequest request,
HttpServletResponse response) {
// etc - duplicate code across controllers with the one
// difference of a single function call and its corresponding params
}
}
Then I realized that a lot of the functionality was reused across 6 other controllers so I consolidated them using an abstract BaseController
abstract class BaseController {
public ResponseEntity<Object> run(String path, String[] params) {
Object result = null;
switch (path.toLowerCase()) {
// case for each path
case MY_PATH:
result = someService.myPath(param[0]);
break;
case MY_OTHER_PATH:
result = someService.myOtherPath(param[0], param[1]);
break;
default:
System.out.println("No");
throw new Exception();
}
return new ResponseEntity<>(result, HttpStatus.OK);
}
}
and then I changed the class header for MyRestController to
public class MyRestController extends BaseController {
and this worked!
My questions are:
How come I couldn't move the CrossOrigin from MyRestController to BaseController ?
I was told to use an abstract class. Does this help at all in this use case?
I replaced the duplicate try / catch with a single function using the path in a switch statement to use the correct method with the correct params. This seems hackish... Is there a better way to do this?
Thanks
Looking at the documentation for the annotation CrossOrigin https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/CrossOrigin.html
This is why it didn't work is because your BaseController doesn't have any methods to add to the RequestMappingHandlerMapping so that is why it didn't work.
As far as the BaseController being abstract it is not necessary unless you have a method that you want your extending controllers to overwrite.
Update about Switch
That is really up to you depending on how many controller methods are going to fall into each case. If multiple methods fall into the MY_PATH case then I believe you are ok, but I can see where the cases could turn into a very lengthy switch statement. It's really up to you as the maintainer. In my opinion, I would break the switch statement cases into different methods and let the controller that extend call that method. But that is personal preference.
I have a method on an ApiController that looks like this:
public IEnumerable<Items> GetSlideSets() {
IServiceClass serviceClass = new ServiceClass();
//...
Yes, I am aware that this is not good design but I'm addressing this issue in a different iteration.
At a certain point in my application I need to call this functionality from within the project itself so I thought I could simply reuse the controller (and why not, I can pluck it out of my IoC container). The only problem is that in this case, I need to inject my own implementation of IServiceClass, easy enough:
public IEnumerable<Items> GetSlideSets(IServiceClass serviceClass = null) {
serviceClass = serviceClass ?? new ServiceClass();
//...
Except now I am getting errors when calling this via a regular Api call Optionalparameter 'serviceClass' is not supported by FormatterParameterBinding.
I know that there are various attributes that control bindings. Is there one that I can put on the parameter to say it shouldn't bind.
Like others have mentioned, it's probably a better idea to inject the dependency in the constructor.
But if you really must avoid binding an action parameter, there isn't a built-in attribute but you can create one pretty easily. Here's what it could look like:
public class DontBindAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
return new DontBindParameterBinding(parameter);
}
private class DontBindParameterBinding : HttpParameterBinding
{
public DontBindParameterBinding(HttpParameterDescriptor parameter) : base(parameter)
{
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
{
actionContext.ActionArguments.Add(Descriptor.ParameterName, Descriptor.DefaultValue);
var completedTaskSource = new TaskCompletionSource<object>();
completedTaskSource.SetResult(null);
return completedTaskSource.Task;
}
}
}
You just need to apply the attribute to the parameter afterwards:
public IEnumerable<Items> GetSlideSets([DontBind] IServiceClass serviceClass = null)
I am in the middle of converting my controllers to annotated style controllers in spring mvc.
Basically I do this in the old style controller simpleformcontroller.
protected Map referenceData(HttpServletRequest request) throws Exception
{
Map referenceData = new HashMap();
List<ItemVo> lstItem1 = eqrManager
.searchAllEqptCondQualItems("A1", "BOXES");
List<ItemVo> lstItem2 = eqrManager
.searchAllEqptFullQualItems("A2", "CANNED_GOODS");
referenceData.put("BOX_ITEMS", lstItem1);
referenceData.put("CANNED_ITEMS", lstItem2);
return referenceData;
}
In the annotated, I do something like this:
#ModelAttribute("BOX_ITEMS")
public List<ItemVo> populateCondEQRItems() {
List<ItemVo> lstCondQual = eqrManager
.searchAllEqptCondQualItems("A1", "BOXES");
return lstCondQual;
}
#ModelAttribute("CANNED_ITEMS")
public List<ItemVo> populateFullEQRItems() {
List<ItemVo> lstFullQual = eqrManager
.searchAllEqptFullQualItems("A2", "CANNED_GOODS");
return lstFullQual;
}
My question is, is there a way to return all attributes in just a single method and not
having to create multiple #ModelAttribute? In my case, I need to annotate 2 method? What if I need
3, should I create 3 annotated methods also?
The rule is clear
If you need more than one model attribute, take model as a input argument
#RequestMapping(method=RequestMethod.GET)
public void setUp(Model model) {
model.addAttribute("CANNED_ITEMS", eqrManager.searchAllEqptFullQualItems("A2", "CANNED_GOODS"))
.addAttribute("BOX_ITEMS", eqrManager.searchAllEqptCondQualItems("A1", "BOXES"));
}
Good lucky!
I cannot get it clearly
Ok! I was telling that #ModelAttribute can be put at Method level as well as Method Parameter level. And it behaves differently depends on where you've put it.
#ModelAttribute(user)
public void preRender(Model model) {
/* this method will be invoked before resolving #ModelAttribute Method Parameter i.e. before invoking render/processCreate method */
/* codes are available to CreateUser.jsp if render request comes */
/* codes are available to CreateUser.jsp if validation fails */
model.addAttribute("countryCodes", I18Nservice.getCountryISOCodes());
model.addAttribute("languageCodes", I18Nservice.getLanguageISOCodes());
}
public String renderCreate(#ModelAttribute(value="user") User user) {
return "/user/create";
}
#Override
public String processCreate(#ModelAttribute(value="user") User user, BindingResult result) {
if(result.hasErrors() {
return "/user/create";
}
securityService.createUser(user);
return "/user/detail/user.getId()";
}
If you are new in Spring MVC 3 arena:
read Web MVC framework
Check #RequestMapping JavaDoc
And play with Petcinic & mvc-showcase