Thursday, April 17, 2014

Spring Security 3.2 Authorization - Part 3

This post builds on the code from part 2 of this series,Spring Security 3.2 Authorization - Part 2.  We will add method level role based security to the strategy controller.  We only need to make two simple changes to enable this security.

1. First, we will add one annotation to enable global method security (highlighted below) to the security config file.
package com.dtr.oas.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.dtr.oas.controller.AccessDeniedExceptionHandler;

@Configuration
@EnableWebMvcSecurity
@ComponentScan(basePackageClasses=com.dtr.oas.service.UserServiceImpl.class)
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(UserDetailsService userDetailsService, AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService);
    }
   
    @Autowired
    AccessDeniedExceptionHandler accessDeniedExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/error/**").permitAll()
                .antMatchers("/strategy/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedExceptionHandler);
    }
}

2. Second, we will add a pre-authorize annotation (highlighted below) to to the strategy controller.
package com.dtr.oas.controller;

import java.util.List;
import java.util.Locale;

import javax.validation.Valid;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.dtr.oas.model.Strategy;
import com.dtr.oas.service.StrategyService;

@Controller
@RequestMapping(value = "/strategy")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class StrategyController {
    static Logger logger = LoggerFactory.getLogger(StrategyController.class);

    @Autowired
    private StrategyService strategyService;

    @Autowired
    private MessageSource messageSource;

    @RequestMapping(value = {"/", "/list"}, method = RequestMethod.GET)
    public String listOfStrategies(Model model) {
        logger.info("IN: Strategy/list-GET");

        List<Strategy> strategies = strategyService.getStrategies();
        model.addAttribute("strategies", strategies);

        // if there was an error in /add, we do not want to overwrite
        // the existing strategy object containing the errors.
        if (!model.containsAttribute("strategy")) {
            logger.info("Adding Strategy object to model");
            Strategy strategy = new Strategy();
            model.addAttribute("strategy", strategy);
        }
        return "strategy-list";
    }              
    
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String addingStrategy(@Valid @ModelAttribute Strategy strategy,
            BindingResult result, RedirectAttributes redirectAttrs) {

        logger.info("IN: Strategy/add-POST");

        if (result.hasErrors()) {
            logger.info("Strategy-add error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.strategy", result);
            redirectAttrs.addFlashAttribute("strategy", strategy);
            return "redirect:/strategy/list";
        } else {
            strategyService.addStrategy(strategy);
            String message = "Strategy " + strategy.getId() + " was successfully added";
            redirectAttrs.addFlashAttribute("message", message);
            return "redirect:/strategy/list";
        }
    }

    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String editStrategyPage(@RequestParam(value = "id", required = true) 
            Integer id, Model model) {
        
        logger.info("IN: Strategy/edit-GET:  ID to query = " + id);

        if (!model.containsAttribute("strategy")) {
            logger.info("Adding Strategy object to model");
            Strategy strategy = strategyService.getStrategy(id);
            logger.info("Strategy/edit-GET:  " + strategy.toString());
            model.addAttribute("strategy", strategy);
        }

        return "strategy-edit";
    }
        
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String editingStrategy(@Valid @ModelAttribute Strategy strategy,
            BindingResult result, RedirectAttributes redirectAttrs,
            @RequestParam(value = "action", required = true) String action) {

        logger.info("IN: Strategy/edit-POST: " + action);

        if (action.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            String message = "Strategy " + strategy.getId() + " edit cancelled";
            redirectAttrs.addFlashAttribute("message", message);
        } else if (result.hasErrors()) {
            logger.info("Strategy-edit error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.strategy", result);
            redirectAttrs.addFlashAttribute("strategy", strategy);
            return "redirect:/strategy/edit?id=" + strategy.getId();
        } else if (action.equals(messageSource.getMessage("button.action.save",  null, Locale.US))) {
            logger.info("Strategy/edit-POST:  " + strategy.toString());
            strategyService.updateStrategy(strategy);
            String message = "Strategy " + strategy.getId() + " was successfully edited";
            redirectAttrs.addFlashAttribute("message", message);
        }

        return "redirect:/strategy/list";
    }

    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String deleteStrategyPage(
            @RequestParam(value = "id", required = true) Integer id,
            @RequestParam(value = "phase", required = true) String phase,
            Model model) {

        Strategy strategy = strategyService.getStrategy(id);
        logger.info("IN: Strategy/delete-GET | id = " + id + " | phase = " + phase + " | " + strategy.toString());

        if (phase.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            String message = "Strategy delete was cancelled.";
            model.addAttribute("message", message);
            return "redirect:/strategy/list";
        } else if (phase.equals(messageSource.getMessage("button.action.stage", null, Locale.US))) {
            String message = "Strategy " + strategy.getId() + " queued for display.";
            model.addAttribute("strategy", strategy);
            model.addAttribute("message", message);
            return "strategy-delete";
        } else if (phase.equals(messageSource.getMessage("button.action.delete", null, Locale.US))) {
            strategyService.deleteStrategy(id);
            String message = "Strategy " + strategy.getId() + " was successfully deleted";
            model.addAttribute("message", message);
            return "redirect:/strategy/list";
        }

        return "redirect:/strategy/list";
    }
}

3. To test that the method level security is active, we can comment out the line in our security configuration that only allows the ADMIN role to access the strategy pages. The line to comment out is:

.antMatchers("/strategy/**").hasRole("ADMIN")

With this line commented out, the only way we could receive an access denied exception is if our method level security intercepts our request for the strategy pages.


4. When we log in as the user "user", and attempt to access a strategy page, we should see our custom 403 error page. We are now receiving this message because of the method level security rather than the HttpSecurity check.


The approach outlined above works well, but what about the case where we need to add a new role, or the case where we want to expand access to functionality for an existing role. For these two cases, using the approach outlined above, we would need to modify our code to change method level authorization. Another approach would be to have unique authorization specified at the method level, with these unique method level permissions entered into a permissions table. We could then create a link table to loosely couple permissions to roles. In the next post, we will add this structure to the example application.

Code at GitHub: https://github.com/dtr-trading/spring-ex13-auth

No comments:

Post a Comment