Seam Security Gets An Upgrade

by Shane Bryzak

Article permalink: http://shane.bryzak.com/articles/seam_security_gets_an_upgrade

The upcoming 2.1.0.GA release of JBoss Seam will contain a number of new and improved security features, including Identity Management, ACL-based permissions and Permission Management, plus strongly-typed security annotations.  In this article I'll be explaining some of these features as well as showing how they are used in the SeamSpace example application (included in the /examples/seamspace directory within the Seam distribution).

If you'd like to play with the SeamSpace example yourself, here are the steps you need to follow to download the necessary software and configure it to run:

1) Ensure you have JDK 1.5 installed, and a recent version of Apache Ant
2) Download and install JBoss AS 4.2.2, from http://www.jboss.org/jbossas/downloads/
3) Download and extract the latest 2.1 nightly build of Seam, from http://www.seamframework.org/Download (or if you're feeling adventurous, check out the latest version from our anonymous SVN repository, at http://anonsvn.jboss.org/repos/seam/trunk/)
4) Edit the build.properties file in the Seam distribution to point to your JBoss AS installation
5) Change into the examples/seamspace directory in the Seam distribution, and run 'ant'
6) Start JBoss AS by using the run script in its /bin directory
7) After JBoss AS has started, browse to http://localhost:8080/seam-space

Let's start off by taking a look at the new Identity Management features.

Identity Management

So what is Identity Management?  Up until now, Seam has only provided the built-in components used to facilitate user authentication (the Identity component).  What it didn't provide was a formal API for the creation and management of the actual user accounts that you authenticate with, leaving this pretty much up to the developer.  Identity Management fills this gap by providing such an API, which endeavours to provide a consistent way of managing users and roles, no matter how they are stored in the backend.  Whether they are persisted as records in a relational database, or stored as entries in an LDAP directory, Identity Management offers a standard API for creating, updating and deleting users and roles within a Seam application. 

At the heart of the Identity Management API is the IdentityManager component.  It is this component that exposes the bulk of the identity management operations.  To give you more of an idea of the kind of features that it provides, here is a list of some of its methods:

  • createUser(String username, String password)
  • deleteUser(String name)
  • enableUser(String name)
  • disableUser(String name)
  • changePassword(String name, String password)
  • isUserEnabled()
  • grantRole(String name, String role)
  • revokeRole(String name, String role)
  • createRole(String role)
  • deleteRole(String role)
  • userExists(String name)
  • roleExists(String name)
  • listUsers()
  • authenticate(String username, String password)


As you can see from the above list, many of the methods are synonymous with the familiar operating system commands that deal with security (particularly if you are familiar with unix/linux), such as adduser, deluser, etc.

Configuring IdentityManager

The configuration for IdentityManager is quite simple - it must be configured with either one, or two identity stores (more on these in a moment).  One identity store is used for all the user-related operations, and the other is used for all the role-related operations.  If both your users and roles are contained within the same persistent storage (such as a database) then only one identity store needs to be configured, which will then be used for both user and role operations.  This may sound a little strange, however it allows for greater flexibility when addressing complex security requirements.  For example, it makes possible scenarios such as authenticating users using LDAP, but loading their roles from a relational database (from application-specific tables).

Each IdentityStore implementation knows how to work with a particular type of persistent security storage.  Seam provides two IdentityStore implementations out of the box, JpaIdentityStore and LdapIdentityStore, for working with either database or LDAP-based security (respectively).  If no identity stores are configured for the IdentityManager, then it defaults to using the JpaIdentityStore.  We'll explore the LdapIdentityStore and how we can set up Seam to authenticate against an LDAP directory another day - for now we'll concentrate on JpaIdentityStore.

JpaIdentityStore

This IdentityStore implementation allows you to use a relational database to store user account details.  By annotating the entity beans that represent your users and roles with a special set of annotations, JpaIdentityStore is able to use these entities to manage the user accounts that are persisted as records in the database.


Let's take a look at the MemberAccount entity bean, used to store our user account records.  The following class has been truncated and reformatted to portray just its relevant bits:
 
  @Entity
  @Table(uniqueConstraints = @UniqueConstraint(columnNames = "username"))
  public class MemberAccount implements Serializable
  {
     // snip field and key declarations
    
     @NotNull @UserPrincipal
     public String getUsername() { return username; }  
     public void setUsername(String username) { this.username = username; }
    
     @UserPassword(hash = "MD5")
     public String getPasswordHash() { return passwordHash; }  
     public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }     
    
     @UserEnabled
     public boolean isEnabled() { return enabled; }
     public void setEnabled(boolean enabled) { this.enabled = enabled; }  
 
     @UserRoles
     @ManyToMany(targetEntity = MemberRole.class)
     @JoinTable(name = "AccountMembership",
           joinColumns = @JoinColumn(name = "AccountId"),
           inverseJoinColumns = @JoinColumn(name = "MemberOf"))
     public Set<MemberRole> getRoles() { return roles; }  
     public void setRoles(Set<MemberRole> roles) { this.roles = roles; }
  }

As we can see from the above code, there are a number of additional annotations on the bean's property accessors that tell JpaIdentityStore how it should interact with this particular entity bean.  Let's take a look at these annotations in some more detail:

  • @UserPrincipal - this annotation indicates the field that contains the user's principal (i.e. their username)
  • @UserPassword - this annotation indicates the field that contains the user's password.  It is usually not a good idea to store user passwords in plain text, so this annotation supports the specifying of a hash algorithm to use.  This algorithm will be used to generate a hash for the user's plain text password, and the hash will be persisted rather than the password itself.
  • @UserEnabled - this is optional, and indicates that the field reflects whether the user account is enabled or not.  If this annotation is not present, then it is assumed that all user accounts are enabled by default.
  • @UserRoles - this annotation indicates that the field returns the collection of role memberships for which the user is a member of.


There is also a similar set of annotations for configuring an entity to store roles.  Alternatively, it is possible to store role records in the same table as users (which then becomes self-referencing via a one-to-many relationship to itself) by specifying one of the columns as a discriminator (determining whether the record represents a user or a role).  This is described, along with various other options, in much more detail in the Seam reference documentation.

Using IdentityManager

Let's now take a look at the SeamSpace example, and see how Identity Management can be used.



From the screenshot of the SeamSpace home page above, we can see the 'SIGN UP' link that allows a new user to register.  The user can enter some basic details on the registration page to create a new account so that they can log in.  The registration procedure uses IdentityManager to create the new user account and then logs in the newly registered user.  Taking a look at RegisterAction.java, we can see that the Identity Manager is injected using the @In annotation:

   @In
   private IdentityManager identityManager;
  
Then a little further down, we can see how the new user is created in the uploadPicture() method (this method is called at the end of the registration procedure, and also ends the current conversation):  

   @End
   public void uploadPicture()
   { 
      (snip)
  
      new RunAsOperation() {
         public void execute() {
            identityManager.createUser(username, password);
            identityManager.grantRole(username, "user");           
         }        
      }.addRole("admin")
       .run();
      
     (snip)
    
      // Login the user
      identity.setUsername(username);
      identity.setPassword(password);
      identity.login();
   }
  
The RunAsOperation is used to execute specific operations using elevated privileges.  In this case, creating a new user account requires that the current user has admin rights, which are temporarily granted for the scope of this particular RunAsOperation, using the addRole() method.  At the end of the method, we can see that the user is logged in using the username and password that they provided during registration.

Authentication

Previously, Seam Security required an 'authenticator' component that would be invoked during the authentication process, and which was responsibly for populating a user's roles.  While this authentication model is still supported, it actually makes more sense to use the IdentityManager to authenticate users, as it provides the ability to authenticate without requiring any additional code.  With that in mind, to use IdentityManager to authenticate your users, simply remove the 'authenticate-method' attribute from the configuration for the identity component in components.xml:

    <!-- The old way of authenticating, using an authenticator component -->
    <!--security:identity authenticate-method="#{authenticator.authenticate}"/-->
   
    <!-- The new way to authenticate, using IdentityManager (you don't actually need to
         include this element, since it has no attributes now) -->
    <security:identity/>

User Management Views

If you log into SeamSpace using an admin account (username/password: demo/demo), you will now see a new 'Security' link at the top of the page.  Clicking this link will give you the option of managing either users or roles:



Clicking the first link, 'Manage Users' will take you to the user manager screen:



From here, you can create new users and edit or delete existing users.  Clicking on the 'new user' button displays the user details screen, where you can enter the details for a new user, such as their first and last name, username, password and roles that they are to be assigned:



Clicking save will create the new user, and return you to the user manager screen where we can now see our newly created user in the list:



Let's now take a look at role management - clicking on the 'Manage Roles' link (from the Security link at the top of the page) will take us to the role manager screen:



Once again, we get a list of all the defined application roles.  Clicking the 'new role' link will again take us to a detail screen where we can create a new role:



This screen is a little less complex than the user detail screen, allowing you to enter a name for the new role and assign any group memberships (roles can be members of other roles).  Clicking save takes us back to the role manager screen where we can see our new role:



This pretty much concludes our high level overview of the new identity management features, so let's now move on to permission management.

Permission Management

While Identity Management provides a consistent API for managing user accounts, we still need a way to manage user permissions.  The authorization features in Seam 2.1.0 as a whole have received a significant overhaul since the previous version of Seam.  Instead of requiring the developer to extend the built-in Identity component to implement custom permission checks, Seam 2.1.0 now provides a pluggable system that allows you to register your own permission resolver without having to override any built-in behaviour.  The following diagram shows how it all fits together:



Following the above diagram, Identity now uses PermissionMapper to map permission checks to a specific ResolverChain, which in turn can be configured with one or more PermissionResolvers.  Seam provides both RuleBasedPermissionResolver (for resolving rule-based permission checks) and PersistentPermissionResolver (for performing checks based on permissions stored in persistent storage, such as a database).  It is also really easy to implement your own PermissionResolver if your application has custom security requirements.

I guess before we go any further we should actually define what a permission actually is.  In the Seam universe, a permission has three aspects:

  • A target, which is an object that is to be acted upon in some way
  • An action, the intended action to be performed on the target
  • A recipient, the user or role entity that is given the right to perform the specified action on the target




The target of a permission check is what the PermissionMapper uses to determine which ResolverChain should be used to perform the check.  This makes it possible to configure different PermissionResolvers for different types of objects.  For example, you may wish to perform permission checks for Customer objects only using the RuleBasedPermissionResolver, and likewise you may wish to perform permission checks for Invoice objects only using PersistentPermissionResolver.  The PermissionMapper easily supports this level of flexibility.

Let's take a look at this stuff in action.

Persistent Permissions

The SeamSpace example allows users to upload images to their profile.  Other users can then browse to that user's profile to view their images:





Now let's say that some of the pictures you wish to keep private, and some other pictures you wish to only show to your friends.  Clicking on the padlock icon underneath your picture will take you to the permission management screen for that picture:



From this screen, we can see which users or roles currently have permission to view this image.  In this particular example, we can see that only our friends have permission to view the image.  Now let's say that we decide that any member of the site may view this particular image.  We can grant this permission by clicking on the 'new permission' button, taking us to the permission details screen:



From here we can grant specific permission actions to selected roles or individual users (in this case only our friends are listed).  We wish to allow all users to view this image, so we select 'user' from the list of available roles, and check the 'view' checkbox.  Clicking save will then take us back to the permission manager screen, where we can see our new permission:



So what actually happens when we grant a new permission?  Let's take a look at the component that is being used behind the scenes, ImagePermission.  Here's the relevant bits of code:

@Name("imagePermission")
@Scope(CONVERSATION)
public class ImagePermission implements Serializable
{
   // (snip)
   @In PermissionManager permissionManager; 
   @In PermissionSearch permissionSearch;  
  
   private MemberImage target;   
   private Principal recipient;
  
   @SuppressWarnings("unchecked")
   @Begin(nested = true)
   public void createPermission() {
      target = (MemberImage) permissionSearch.getTarget();     
      // (snip)
   }
  
   public void applyPermissions() {
      // (snip)
   
      List<Permission> permissions = new ArrayList<Permission>();
     
      for (String role : selectedRoles)
      {
         Principal r = new Role(role);
         for (String action : selectedActions)
         {           
            permissions.add(new Permission(target, action, r));
         }
      }
      
      for (Member friend : selectedFriends)
      {
         MemberAccount acct = (MemberAccount) entityManager.createQuery(
               "select a from MemberAccount a where a.member = :member")
               .setParameter("member", friend)
               .getSingleResult();
        
         Principal p = new SimplePrincipal(acct.getUsername());
        
         for (String action : selectedActions)
         {
            permissions.add(new Permission(target, action, p));
         }
      }
      
      permissionManager.grantPermissions(permissions);

      Conversation.instance().endBeforeRedirect();
   }  
  
   // (snip)
}

We can see from the above code that ImagePermission is a conversation-scoped component.  In fact, its function is implemented within the scope of a nested conversation, allowing multiple 'new permission' windows to be open at the same time for the same target object.  We can also see that the PermissionManager component is injected into our component using the @In annotation.

So, the createPermission() method begins our nested conversation (thanks to the @Begin(nested = true) annotation) and stores a reference to our target object.  Then when the user then hits the save button, after assigning the desired permissions, applyPermissions() is invoked which builds a list of Permission objects to be granted.  It is within this method that PermissionManager.grant() is invoked with the list of permissions to be granted.  Let's take some time to examine PermissionManager (the heart of the Permission Management API) in more detail.

The PermissionManager Component

Much the same way that IdentityManager is designed to carry out operations for users and roles, PermissionManager is designed to carry out operations for permissions.  It provides an API that allows permissions to be granted or revoked, or listed for a specific target object.  Let's take a look at some of the methods it exposes:

  • listPermissions(String target, String action)
  • listPermissions(Object target)
  • grantPermission(Permission permission)
  • grantPermissions(List<Permission> permissions)
  • revokePermission(Permission permission)
  • revokePermissions(List<Permission> permissions)

Also, in the same way that IdentityManager requires an IdentityStore to interface with a persistent security store, PermissionManager also requires a PermissionStore to talk to the persistent permission storage.  Seam only provides one PermissionStore implementation - JpaPermissionStore, which can be used to work with permissions stored in a database using JPA.  Theoretically it is possible to store permissions in an LDAP directory, or even a flat file, but the most practical solution really is to just store them in a database.

So, let's take a look at the source code for the AccountPermission entity bean in the SeamSpace example.  The following code has been truncated and reformatted for brevity:

@Entity
public class AccountPermission implements Serializable
{
   // snip field declarations, etc
     
   @PermissionUser @PermissionRole
   public String getRecipient() {  return recipient; }  
   public void setRecipient(String recipient) { this.recipient = recipient; }
  
   @PermissionTarget public String getTarget() { return target; }  
   public void setTarget(String target) { this.target = target; }
  
   @PermissionAction
   public String getAction() { return action; }  
   public void setAction(String action) { this.action = action; }
  
   @PermissionDiscriminator
   public String getDiscriminator() { return discriminator; }  
   public void setDiscriminator(String discriminator) { this.discriminator = discriminator; }
}

Once again, we can notice some special annotations being used.  To sidetrack for a moment, it should be pointed out that permissions can be assigned to either users or roles.  So while it is by all means possible to store these permissions in separate tables, it makes more sense from a performance perspective to store them both in a single table, and use a discriminator column to tell them apart.  Hence in the above code listing, we see that the getDiscriminator() property is annotated with @PermissionDiscriminator, indicating that this column determines whether the permission it represents is intended for a user or a role.

Moving on, the special annotations used to configure an entity for storing permission are:

  • @PermissionUser - designates the field that contains the name of the recipient of the permission (for user-assigned permissions)
  • @PermissionRole - same as above, but for role-assigned permissions
  • @PermissionTarget - contains a unique identifier string, identifying a single instance of an object.  Alternatively, can contain a class name or any arbitrary string for the designation of more generalised permissions.
  • @PermissionAction - contains a list of the actions that the recipient may perform on the target object.
  • @PermissionDiscriminator - see paragraph above


Just to point out - the permission management features are for the managing of persistent permissions only.  There most certainly will be situations in which you wish to grant permissions based on business logic, for example users should always have rights to view and manage their own images.  These types of permission are handled by Seam's rule-based security, and discussed in more detail next.

Rule-based Permissions

Seam has long supported rule-based security based on Drools, so this isn't really a new feature.  Let's revisit it though to see how it is used within the scope of our example application, SeamSpace.

Continuing on with the subject of image security, it is relatively obvious that we need to have some basic security rules in place when dealing with user images, besides what was already discussed above in regards to persistent permissions.  By default, viewing all user images are restricted by security constraints, which means that in SeamSpace, if you want to view a user's image you must be explicitly granted permission to do so one way or another (either via a persistent permission grant, or a security rule).  With that in mind, we specifically need to allow for the following:

  • Users should be allowed to grant and revoke permissions for their own images
  • Users should be allowed to delete their own images
  • User profile images (a user's main image, i.e. their 'avatar') should always be viewable by anyone
  • Users should always be allowed to view their own images (of course)
  • User images with 'friend' permissions should be viewable by the user's friends (more on this in a bit)

Let's examine the rules for these in more detail.  First of all, users should be allowed to grant and revoke permissions for their images.  This is a pretty straight forward one, and is implemented as two separate rules.  When working with object permissions, Seam will insert a PermissionCheck object into the Drools working memory containing both the target of the permission, and an action either being 'seam.grant-permission' or 'seam.revoke-permission' depending on what the user is trying to do (based on whether PermissionManager.grantPermission() or PermissionManager.revokePermission() is called).  The MemberAccount instance for the currently authenticated user is always present in the working memory, so effectively the following rules are saying, 'if the MemberImage for which we're performing the permission check is owned by the current user, then grant the permission':

  rule GrantImagePermissions
    no-loop
    activation-group "permissions"
  when
    acct: MemberAccount()
    image: MemberImage(mbr : member -> (mbr.memberId.equals(acct.member.memberId)))
    check: PermissionCheck(target == image, action == "seam.grant-permission", granted == false)
  then
    check.grant();
  end
 
  rule RevokeImagePermissions
    no-loop
    activation-group "permissions"
  when
    acct: MemberAccount()
    image: MemberImage(mbr : member -> (mbr.memberId.equals(acct.member.memberId)))
    check: PermissionCheck(target == image, action == "seam.revoke-permission", granted == false)
  then
    check.grant();
  end
 
Moving on, we also need a rule to allow users to delete their own images.  Similar to our first two rules, we also check that the currently authenticated user is the owner of the image that is to be deleted, and if so grant the permission:

  rule DeleteImage
    no-loop
    activation-group "permissions"
  when
    acct: MemberAccount()
    image: MemberImage(mbr : member -> (mbr.memberId.equals(acct.member.memberId)))
    check: PermissionCheck(target == image, action == "delete", granted == false)
  then
    check.grant();
  end 

Our rule for viewing profile images is a little different.  Here we simply test that the image being viewed is the profile image for the owning member (i.e. image.getMember().getPicture() == image):

  rule ViewProfileImage
    no-loop
    activation-group "permissions"
  when
    image: MemberImage()
    check: PermissionCheck(target == image, action == "view", granted == false)
    eval( image.getMember().getPicture() == image )
  then
    check.grant();
  end
 
And also, users should be able to always view their own images.  This permission check is very similar to our first few rules, in which we simply check if the owner of the image is the currently authenticated user:
 
  rule ViewMyImages
    no-loop
    activation-group "permissions"
  when
    acct: MemberAccount()
    image: MemberImage(mbr : member -> (mbr.memberId.equals(acct.member.memberId)))
    check: PermissionCheck(target == image, action == "view")
  then
    check.grant();
  end

Conditional Roles 

Finally, for allowing our friends to view our images we need to have a special rule.  Earlier we saw how it was possible to grant permissions to the role 'friends', however depending on the context 'friend' can have different meanings.  Arguably, every single user in the system can be a 'friend' to someone else, so how do we determine who is a 'friend' when it comes to viewing images?  This is where conditional roles step in - these roles are are special, and cannot be explicitly granted to users. 

When a permission check is performed by the security API for an object and the permission manager informs the security API that a conditional role has been granted the permission, a special rule-based check needs to be performed to evaluate whether the current user actually has that role, but only within the context of the permission check.  To achieve this, as per usual a PermissionCheck object is inserted into the working memory containing the target and action, however in addition a RoleCheck object is also inserted, containing the name of the conditional role that is to be evaluated.  Using this we can write a security rule to determine whether or not to grant the conditional role:

  rule FriendViewImage
    no-loop
    activation-group "permissions"
  when
    acct: MemberAccount()
    image: MemberImage(mbr : member -> (mbr.isFriend(acct.member)))
    PermissionCheck(target == image, action == "view")
    role: RoleCheck(name == "friends")
  then
    role.grant();
  end
 
This rule checks whether the currently authenticated user is in the list of friends for the target image's owner.  If it is, then the role is temporarily granted for this particular permission check.  This allows great flexibility in assigning complex security rules to dynamic groups of users (in this case, a user's friends list) that don't necessarily warrant having their own role/group, due to either impracticality or design restrictions.

Strongly-typed Security Annotations

Finally, to wrap up let's take a look at some of the new security annotations.  In order to make Seam Security more 'Web Beansy' we've introduced a number of typesafe annotations for restricting component methods.  By using meta-annotations, we can provide a set of security annotations that apply security restrictions to either the method or its parameters.  Out of the box, Seam provides a standard set of CRUD annotations (@Insert, @Read, @Update, @Delete) and it is a piece of cake to add your own.  Take the following example:

  @Begin @Insert(Customer.class)
  public void createCustomer() {

This annotation prevents the user from invoking the createCustomer() method unless they have permission to insert new customer objects.  Similarly, we can annotate the parameters of a method also:

  public void updateCustomer(@Update Customer customer) {

Creating your own security annotations is as simple as meta-annotating your annotation with @PermissionCheck.  Say for example, you wish to create a new permission called 'Promote'.  The annotation can be easily defined like so:

  @Target({METHOD, PARAMETER})
  @Documented
  @Retention(RUNTIME)
  @Inherited
  @PermissionCheck
  public @interface Promote {
     Class value() default void.class;
  }

After defining the annotation, it can be used straight away:

  public void promoteStaff(@Promote Staff person) {
 
If writing a rule-based permission, the rule might look something like this:

  rule PromoteStaffMember
    no-loop
    activation-group "permissions"
  when
    acct: MemberAccount()
    Role(name == 'admin')
    staff: Staff()
    check: PermissionCheck(target == staff, action == "promote")
  then
    check.grant();
  end

The permission action becomes the lower case version of the annotation name.  It's as simple as that!  While we still support the legacy @Restrict annotation for expression-based security checks, I recommend that everyone gives the new typesafe annotations a try, at least for the compile-time safety it provides for simple restrictions.

Conclusion

That wraps up this overview of the new security features in JBoss Seam.  Here are some links for reference:

JBoss Seam Community Site (downloads, documentation, forums) - http://www.seamframework.org/

JBoss Home Page - http://www.jboss.org

Comments

Please direct any comments to the following page:

http://in.relation.to/Bloggers/SeamSecurityGetsAnUpgrade