CustomMenu

Friday, April 18, 2014

Spring Security 3.2 - Users, Roles, Permissions - Part 5

This post builds on the code from part 4 of this series,Spring Security 3.2 - Users, Roles, Permissions - Part 4.  In this post, we will create a new package and move the exception/exception handling classes to this package in order to more easily accommodate unit testing with JUnit.  The revised directory structure and refactored class names are shown in the image below.

1. We will first create a new package, com.dtr.oas.exception.  We will then move all of the exception classes from com.dtr.oas.dao to this new package as shown in the image above.


2. Next, we will move the AccessDeniedExceptionHandler from com.dtr.oas.controller to com.dtr.oas.exception as shown in the image above.


3. The import statement in the SecurityConfig file should be updated to refer to the AccessDeniedExceptionHandler in the new package.


4. Now we will rename com.dtr.oas.DatabaseConfig to com.dtr.oas.RootConfig.


5. The import statement in the Initializer file should be updated to refer to the RootConfig file rather than the DatabaseConfig file.


6. Lastly, the component scan entry in the RootConfig file should be updated to the following.
...
import com.dtr.oas.exception.AccessDeniedExceptionHandler;

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.dtr.oas.dao", "com.dtr.oas.model", "com.dtr.oas.service", "com.dtr.oas.util"})
@PropertySource("classpath:database.properties")
public class RootConfig {

private static final String PROPERTY_NAME_DATABASE_DRIVER   = "db.driver";
...
In the WebAppConfig.java file, all packages under "com.dtr.oas" are scanned, while in the RootConfig.java file, all packages except the "com.dtr.oas.controller" package are scanned.  The RootConfig file is used to prepare the environment for JUnit and we are not running any controller tests at this time (so we need to exclude controllers from this environment prep).  Much of this package and config reorg was to enable unit testing of the service and DTO layers.


7. At this time we should now be able to run the application and test the security configuration.  The HttpSecurity line in the SecurityConfig will keep all users not in the ADMIN role from being able to access the Strategy pages.  To test that the method level authorization is functional on the Strategy pages, we can check the database to review the pemissionname and associated id.
In the screen shot above, we can see that ID number 3 corresponds to the permissionname "CTRL_STRATEGY_EDIT_GET".  This is the permission that we want to reassign.

Now we can modify the ROLE_ID field in role_permissions table as shown in the screenshot below.
Rather than having the permission id number 3 associated with the ADMIN role, we will reassign this permission to the TRADER role.

To confirm this authorization change, log in as the admin user, go to the strategy list page and try to edit a strategy.  If the authorization update is working, the admin user should receive our custom 403 page when they try to edit a strategy.  All other strategy functionality should be working for the admin user.  After the confirmation, change the role id value back to ADMIN for our edit permission.

In the next posts, we will add CRUD functionality to our User, Role, and Permission entities.

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

Spring Security 3.2 - Users, Roles, Permissions - Part 4

This post builds on the code from part 3 of this series,Spring Security 3.2 - Users, Roles, Permissions - Part 3.  In this post, we will add/update the controller layer to utilize the new authorization scheme. There are only two controller classes to update.

1. We will start by updating the link controller in the areas highlighted below.  Rather than getting the single role from the User object, we now need to get all of the authorities (roles and permissions) from the User object.  The method getAuthorities() highlighted at the bottom of the controller performs this function.  We then iterate through the authorities to determine which role is associated with this user.  As before, the uesr's role dictates the home-page for that user.
package com.dtr.oas.controller;
import java.util.Collection;
import java.util.HashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.dtr.oas.model.User;

@Controller
public class LinkController {
    static Logger logger = LoggerFactory.getLogger(LinkController.class);

    @RequestMapping(value = "/")
    public String mainPage() {
        Collection<GrantedAuthority> authorities = getAuthorities();
        String rolename;
        
        for (GrantedAuthority authority : authorities) {
            rolename = authority.getAuthority();
            
            if (rolename.equals("ROLE_ADMIN")) {
                logger.debug("Directing to home page for: [" + rolename + "]");
                return "home-admin";
            }
            if (rolename.equals("ROLE_TRADER")) {
                logger.debug("Directing to home page for: [" + rolename + "]");
                return "home-trader";
            }
            if (rolename.equals("ROLE_USER")) {
                logger.debug("Directing to home page for: [" + rolename + "]");
                return "home-user";
            }
        }
        
        logger.error("Role not found - directing to home page for ROLE_USER");
        return "home-user";
    }

    @RequestMapping(value = "/index")
    public String indexPage() {
        return "redirect:/";
    }
    
    private Collection<GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof User) {
            authorities = ((User) principal).getAuthorities();
        } else {
            logger.error("Principal is not an instance of com.dtr.oas.model.User");
        }
        return authorities;
    }

}

2. Next we will update the strategy controller to user our new authentication scheme. The changed lines are highlighted in blue. The first modification is to deny access to all methods in the class using the "denyAll" declaration.  Then, we selectively turn on access to each of the methods using the permission strings that we loaded into the permissions table in the database.  For example, the addingStrategy() method can only be invoked if the user has a role that has the permission "CTRL_STRATEGY_ADD_POST".
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("denyAll")
public class StrategyController {
    static Logger logger = LoggerFactory.getLogger(StrategyController.class);

    @Autowired
    private StrategyService strategyService;

    @Autowired
    private MessageSource messageSource;

    @RequestMapping(value = {"/", "/list"}, method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_STRATEGY_LIST_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)
    @PreAuthorize("hasRole('CTRL_STRATEGY_ADD_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)
    @PreAuthorize("hasRole('CTRL_STRATEGY_EDIT_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)
    @PreAuthorize("hasRole('CTRL_STRATEGY_EDIT_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)
    @PreAuthorize("hasRole('CTRL_STRATEGY_DELETE_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";
    }
}

In the next post, we will make some configuration changes to accommodate unit testing, and then show the results.

Spring Security 3.2 - Users, Roles, Permissions - Part 3

This post builds on the code from part 2 of this series,Spring Security 3.2 - Users, Roles, Permissions - Part 2.  In this post, we will add/update the service layer to accommodate the addition of the Permission class. There are only two classes to create for Permission service.

1. We will start by creating the interface needed in this layer. This interface is nearly identical in functionality to the other service layer interfaces.
package com.dtr.oas.service;
import java.util.List;
import com.dtr.oas.dao.DuplicatePermissionException;
import com.dtr.oas.dao.PermissionNotFoundException;
import com.dtr.oas.model.Permission;

public interface PermissionService {

    public void addPermission(Permission permission) throws DuplicatePermissionException;

    public Permission getPermission(int id) throws PermissionNotFoundException;
    
    public Permission getPermission(String permissionname) throws PermissionNotFoundException;

    public void updatePermission(Permission permission) throws PermissionNotFoundException;

    public void deletePermission(int id) throws PermissionNotFoundException;

    public List<Permission> getPermissions();

}
2. Next we will create the implementation of the Permission service. This service layer implementation should look pretty familiar at this point.
package com.dtr.oas.service;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.dtr.oas.dao.DuplicatePermissionException;
import com.dtr.oas.dao.PermissionDAO;
import com.dtr.oas.dao.PermissionNotFoundException;
import com.dtr.oas.model.Permission;

@Service
@Transactional
public class PermissionServiceImpl implements PermissionService {
    static Logger logger = LoggerFactory.getLogger(PermissionServiceImpl.class);
    
    @Autowired
    private PermissionDAO permissionDAO;

    @Override
    public void addPermission(Permission permission) throws DuplicatePermissionException {
        permissionDAO.addPermission(permission);
    }

    @Override
    public Permission getPermission(int id) throws PermissionNotFoundException {
        return permissionDAO.getPermission(id);
    }

    @Override
    public Permission getPermission(String permissionname) throws PermissionNotFoundException {
        return permissionDAO.getPermission(permissionname);
    }

    @Override
    public void updatePermission(Permission permission) throws PermissionNotFoundException {
        permissionDAO.updatePermission(permission);
    }

    @Override
    public void deletePermission(int id) throws PermissionNotFoundException {
        permissionDAO.deletePermission(id);
    }

    @Override
    public List<Permission> getPermissions() {
        return permissionDAO.getPermissions();
    }
}

In the next post, we will add/update the Strategy controller to user the new authentication functionality.