, , , , , ,

Spring Security: Simple ACL using Expression-Based Access Control (Part 2)

In this tutorial we will continue what we had left off from the Spring Security: Simple ACL using Expression-Based Access Control (Part 1). Last time we set-up a simple ACL configuration. We also implemented the PermissionsEvaluator interface and provided our own custom Permissions list. In this part we will start building the MVC section of the application.

Spring MVC Section

We now move to the MVC section of the application.

Domain Objects

First, let's declare our domain objects:
AdminPost.java
PersonalPost.java
PublicPost.java
Here are the class declarations:

AdminPost.java
package org.krams.tutorial.domain;

import java.util.Date;

public class AdminPost {
private String id;
private Date date;
private String message;

public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

PersonalPost.java
package org.krams.tutorial.domain;

import java.util.Date;

public class PersonalPost {
private String id;
private Date date;
private String message;

public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

PublicPost.java
package org.krams.tutorial.domain;

import java.util.Date;

public class PublicPost {
private String id;
private Date date;
private String message;

public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

The Controllers

Next, we declare the controllers:
AdminController.java
PersonalController.java
PublicController.java

Here are the class declarations:

AdminController.java
package org.krams.tutorial.controller;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.AdminPost;
import org.krams.tutorial.service.AdminService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.annotation.Resource;

/**
* Handles Admin-related requests
*/
@Controller
@RequestMapping("/admin")
public class AdminController {

protected static Logger logger = Logger.getLogger("controller");

@Resource(name="adminService")
private AdminService adminService;

/**
* Retrieves the Edit page
*/
@RequestMapping(value = "/edit", method = RequestMethod.GET)
public String getEditPage(Model model) {
logger.debug("Received request to view edit page");

// Call service. If true, we have appropriate authority
if (adminService.edit(new AdminPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been edited successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Admin >> Edit");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}

/**
* Retrieves the Add page
*/
@RequestMapping(value = "/add", method = RequestMethod.GET)
public String getAddPage(Model model) {
logger.debug("Received request to view add page");

// Call service. If true, we have appropriate authority
if (adminService.add(new AdminPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been added successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Admin >> Add");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}

/**
* Retrieves the Delete page
*/
@RequestMapping(value = "/delete", method = RequestMethod.GET)
public String getDeletePage(Model model) {
logger.debug("Received request to view delete page");

// Call service. If true, we have appropriate authority
if (adminService.delete(new AdminPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been deleted successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Admin >> Delete");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}
}

PersonalController.java
package org.krams.tutorial.controller;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.PersonalPost;
import org.krams.tutorial.service.PersonalService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.annotation.Resource;

/**
* Handles Personal-related requests
*/
@Controller
@RequestMapping("/personal")
public class PersonalController {

protected static Logger logger = Logger.getLogger("controller");

@Resource(name="personalService")
private PersonalService personalService;

/**
* Retrieves the Edit page
*/
@RequestMapping(value = "/edit", method = RequestMethod.GET)
public String getEditPage(Model model) {
logger.debug("Received request to view edit page");

// Call service. If true, we have appropriate authority
if (personalService.edit(new PersonalPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been edited successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Personal >> Edit");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}

/**
* Retrieves the Add page
*/
@RequestMapping(value = "/add", method = RequestMethod.GET)
public String getAddPage(Model model) {
logger.debug("Received request to view add page");

// Call service. If true, we have appropriate authority
if (personalService.add(new PersonalPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been added successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Personal >> Add");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}

/**
* Retrieves the Delete page
*/
@RequestMapping(value = "/delete", method = RequestMethod.GET)
public String getDeletePage(Model model) {
logger.debug("Received request to view delete page");

// Call service. If true, we have appropriate authority
if (personalService.delete(new PersonalPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been deleted successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Personal >> Delete");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}
}

PublicController.java
package org.krams.tutorial.controller;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.PublicPost;
import org.krams.tutorial.service.PublicService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.annotation.Resource;

/**
* Handles Public-related requests
*/
@Controller
@RequestMapping("/public")
public class PublicController {

protected static Logger logger = Logger.getLogger("controller");

@Resource(name="publicService")
private PublicService publicService;

/**
* Retrieves the Edit page
*/
@RequestMapping(value = "/edit", method = RequestMethod.GET)
public String getEditPage(Model model) {
logger.debug("Received request to view edit page");

// Call service. If true, we have appropriate authority
if (publicService.edit(new PublicPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been edited successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Public >> Edit");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}

/**
* Retrieves the Add page
*/
@RequestMapping(value = "/add", method = RequestMethod.GET)
public String getAddPage(Model model) {
logger.debug("Received request to view add page");

// Call service. If true, we have appropriate authority
if (publicService.add(new PublicPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been added successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Public >> Add");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}

/**
* Retrieves the Delete page
*/
@RequestMapping(value = "/delete", method = RequestMethod.GET)
public String getDeletePage(Model model) {
logger.debug("Received request to view delete page");

// Call service. If true, we have appropriate authority
if (publicService.delete(new PublicPost()) == true) {
// Add result to model
model.addAttribute("result", "Entry has been deleted successfully!");
} else {
// Add result to model
model.addAttribute("result", "You're not allowed to perform that action!");
}

// Add source to model to help us determine the source of the JSP page
model.addAttribute("source", "Public >> Delete");

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/resultpage.jsp
return "resultpage";
}
}

We also declare a fourth controller that displays all records:

AllController.java
/**
*
*/
package org.krams.tutorial.controller;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.krams.tutorial.service.AdminService;
import org.krams.tutorial.service.PersonalService;
import org.krams.tutorial.service.PublicService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
* Handles authentication related requests
*/
@Controller
@RequestMapping("/all")
public class AllController {

protected static Logger logger = Logger.getLogger("controller");

@Resource(name="adminService")
private AdminService adminService;

@Resource(name="personalService")
private PersonalService personalService;

@Resource(name="publicService")
private PublicService publicService;

/**
* Retrieves the View page.
* <p>
* This loads all authorized posts.
*/
@RequestMapping(value = "/view", method = RequestMethod.GET)
public String getViewAllPage(Model model) {
logger.debug("Received request to view all page");

// Retrieve items from service and add to model
model.addAttribute("adminposts", adminService.getAll());
model.addAttribute("personalposts", personalService.getAll());
model.addAttribute("publicposts", publicService.getAll());

// Add our current role and username
model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());

// This will resolve to /WEB-INF/jsp/bulletinpage.jsp
return "bulletinpage";
}
}

The Services

Next, we declare the corresponding services:
AdminService.java
PersonService.java
PublicService.java
Here are the class declarations:

AdminService.java
package org.krams.tutorial.service;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.krams.tutorial.domain.AdminPost;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service("adminService")
public class AdminService {

private Map<String, AdminPost> adminPosts = new HashMap<String, AdminPost>();

public AdminService() {
// Initiliaze our in-memory HashMap list
init();
}

// filterObject refers to the current object in the collection
@PostFilter("hasPermission(filterObject, 'READ')")
public List<adminpost> getAll() {
// Iterate our HashMap list and convert it to an ArrayList
List<adminpost> adminList = new ArrayList<adminpost>();
for (String key: adminPosts.keySet()) {
adminList.add(adminPosts.get(key));
}
// Return our new list
return adminList;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean add(AdminPost post) {
// This will return true if it's accessible
return true;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean edit(AdminPost post) {
// This will return true if it's accessible
return true;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean delete(AdminPost post) {
// This will return true if it's accessible
return true;
}

// Initiliazes an in-memory HashMap list
private void init() {
// Create new post
AdminPost post1 = new AdminPost();
post1.setId(UUID.randomUUID().toString());
post1.setDate(new Date());
post1.setMessage("This is admin's post #1");

// Create new post
AdminPost post2 = new AdminPost();
post2.setId(UUID.randomUUID().toString());
post2.setDate(new Date());
post2.setMessage("This is admin's post #2");

// Create new post
AdminPost post3 = new AdminPost();
post3.setId(UUID.randomUUID().toString());
post3.setDate(new Date());
post3.setMessage("This is admin's post #3");

// Add to adminPosts
adminPosts.put(post1.getId(), post1);
adminPosts.put(post2.getId(), post2);
adminPosts.put(post3.getId(), post3);
}
}

PersonalService.java
package org.krams.tutorial.service;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.krams.tutorial.domain.PersonalPost;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service("personalService")
public class PersonalService {

private Map<String, PersonalPost> personalPosts = new HashMap<String, PersonalPost>();

public PersonalService() {
// Initiliaze our in-memory HashMap list
init();
}

// filterObject refers to the current object in the collection
@PostFilter("hasPermission(filterObject, 'READ')")
public List<personalpost> getAll() {
// Iterate our HashMap list and convert it to an ArrayList
List<personalpost> personalList = new ArrayList<personalpost>();
for (String key: personalPosts.keySet()) {
personalList.add(personalPosts.get(key));
}
// Return our new list
return personalList;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean add(PersonalPost post) {
// This will return true if it's accessible
return true;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean edit(PersonalPost post) {
// This will return true if it's accessible
return true;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean delete(PersonalPost post) {
// This will return true if it's accessible
return true;
}

// Initiliazes an in-memory HashMap list
private void init() {
// Create new post
PersonalPost post1 = new PersonalPost();
post1.setId(UUID.randomUUID().toString());
post1.setDate(new Date());
post1.setMessage("This is personal's post #1");

// Create new post
PersonalPost post2 = new PersonalPost();
post2.setId(UUID.randomUUID().toString());
post2.setDate(new Date());
post2.setMessage("This is personal's post #2");

// Create new post
PersonalPost post3 = new PersonalPost();
post3.setId(UUID.randomUUID().toString());
post3.setDate(new Date());
post3.setMessage("This is personal's post #3");

// Add to personalPosts
personalPosts.put(post1.getId(), post1);
personalPosts.put(post2.getId(), post2);
personalPosts.put(post3.getId(), post3);
}
}

PublicService.java
package org.krams.tutorial.service;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.krams.tutorial.domain.PublicPost;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service("publicService")
public class PublicService {

private Map<String, PublicPost> publicPosts = new HashMap<String, PublicPost>();

public PublicService() {
// Initiliaze our in-memory HashMap list
init();
}

// filterObject refers to the current object in the collection
@PostFilter("hasPermission(filterObject, 'READ')")
public List<publicpost> getAll() {
// Iterate our HashMap list and convert it to an ArrayList
List<publicpost> publicList = new ArrayList<publicpost>();
for (String key: publicPosts.keySet()) {
publicList.add(publicPosts.get(key));
}
// Return our new list
return publicList;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean add(PublicPost post) {
// This will return true if it's accessible
return true;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean edit(PublicPost post) {
// This will return true if it's accessible
return true;
}

@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean delete(PublicPost post) {
// This will return true if it's accessible
return true;
}

// Initiliazes an in-memory HashMap list
private void init() {
// Create new post
PublicPost post1 = new PublicPost();
post1.setId(UUID.randomUUID().toString());
post1.setDate(new Date());
post1.setMessage("This is public's post #1");

// Create new post
PublicPost post2 = new PublicPost();
post2.setId(UUID.randomUUID().toString());
post2.setDate(new Date());
post2.setMessage("This is public's post #2");

// Create new post
PublicPost post3 = new PublicPost();
post3.setId(UUID.randomUUID().toString());
post3.setDate(new Date());
post3.setMessage("This is public's post #3");

// Add to publicPosts
publicPosts.put(post1.getId(), post1);
publicPosts.put(post2.getId(), post2);
publicPosts.put(post3.getId(), post3);
}
}

Observations

Notice the service methods have been annotated with @PreAuthorize and @PostFilter. Also, we're not actually performing any add, edit, or delete actions. Instead we're just returning a Boolean value to test whether the method is accessible.

The following declaration means check the domain object post and see if the current user has WRITE access to this object:
@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean add(PublicPost post) {
// This will return true if it's accessible
return true;
}

Whereas the following declaration means after returning, check the domain object post and see if the current user has READ access to this object:
@PostFilter("hasPermission(filterObject, 'READ')")
public List<publicpost> getAll() {
// Iterate our HashMap list and convert it to an ArrayList
List<publicpost> publicList = new ArrayList<publicpost>();
for (String key: publicPosts.keySet()) {
publicList.add(publicPosts.get(key));
}
// Return our new list
return publicList;
}

Run the Application

Let's run the application to see the results (To see the remaining XML configuration, please download the source code below):

We'll need to login first. Enter the following URL to login:
http://localhost:8080/spring-security-acl-expression/krams/auth/login

To access the Bulletin Page, enter the following URL:
http://localhost:8080/spring-security-acl-expression/krams/all/view

We will log-in first as an admin using john/admin as the username/password pair.


Next, we'll login as a user using jane/user as the username/password pair.


Then, we'll login as a visitor using mike/visitor as the username/password pair.


Notice the admin can see all posts while the user can see only the Personal and Public posts; whereas the visitor can only see the Public posts. We've also indicated at the top the name of the current user and the associated role.

If any of the three tries to access an unauthorized resource for their role, they will get the following:


This is the benefit of ACL. We're restricting access on the domain level, not just on the URL level. However if we use the normal intercept-url setup, we won't be able to display all three types of posts. It's either we see everything or we get denied. Try clicking on the remaining links, and verify if they are really protected. I'll bet you they are :)

Conclusion

That's it. We've finished our simple ACL application that leverages Spring Security's Expression-Based Access Control. It may look complicated at first but the concepts are really simple. Also, instead of going the heavyweight solution, we opted to use the lightweight solution using the hasPermission() expression and a custom PermissionsEvaluator implementation

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-security-acl-expression/

You can download the project as a Maven build. Look for the spring-security-acl-expression.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.

0 komentar:

Post a Comment