Sketching Out the Auction SecurityAs you saw in the previous section, the security features provided by the EJB and servlet containers are sufficient for many types of EJB applications. However, as we pointed out in the beginning of this chapter, some things are not covered by the specifications. For example, what if your Web application wanted to cache the user 's security context in the Web tier to prevent redundant network calls to the security realm, which is typically located in the application tier ? Suppose that you had a set of requirements to not show certain buttons , hyperlinks , or tabs depending on the user's roles and permissions. If you had to make several network calls while dynamically spitting out a JSP page, your performance would definitely suffer. To understand this a little better, let's take a look at what happens in a typical Web-enabled EJB application. Our auction application consists of a set of JSP pages located on the Web tier that will dynamically allow certain features depending on who you are and what role you are playing for a particular session. Two scenarios will emphasize the problem.
When Bob posts an auction for others to bid on, he's acting in the role of Auctioneer. This role has certain permissions when it comes to managing this particular auction. Bob would be an Auctioneer only for auctions that he created. He would be given the ability to cancel the auction, assign an early winner to the auction, and respond to questions about the auction. However, Bob must not have this ability when viewing other auctions. Therefore, the security framework should be capable of distinguishing the two roles based on the dynamic data for the user. Although the EJB security framework would prevent Bob from making invocations on particular servlets or methods within an enterprise bean, there's no real local way to get at the permissions that an auction user has been granted without going back and forth to the application tier. Another problem is that servlets and enterprise beans are role-based. This means that you either are in a role or are not. If you are in the role, you have permissions for everything the role has been granted. If you don't belong to a particular role, you are restricted from all permissions granted to that role. There's no way to assign or remove a single permission without putting the user in a role or taking them out of a role. It would be nice and much more flexible if we could not only assign permissions to a group , but also assign them directly to the user. The EJB security architecture doesn't allow for this directly, so we'll have to design our own way of handling this set of requirements if we truly need this behavior. For our auction application, we need to provide a way for the Web tier to get a set of groups and permissions that the client has been granted and then cache this information on the Web tier for performance. Because we are caching these on the Web tier, changes made to the security realm itself will not be reflected to the user during a user's live session. However, after the user logs out and then comes back in later, the changes will be reflected in the security context information that is marshaled to the Web tier. So, the plan for our auction example is going to include a session bean called SecurityManager that will be called only by a special login servlet on the Web server. We also will create an AdminSecurityManager session bean that will be responsible for creating, updating, and deleting users and groups. This session bean could be used from an admin application within the Web tier or, maybe to add more security, it might be called only by an application installed within the intranet. This separation of responsibility helps with security and also provides a more cohesive interface for each component because the responsibilities are arranged in a logical manner. The following code fragments show the steps for a login method inside the SecurityManagerBean class: public SecurityContext login( String userName, String password ) throws InvalidLoginException { SecurityContext secCtx = null; // Get a database connection from the datasource and look for the user // and make sure the account is still active // If the user doesn't exist or is inactive, throw an exception // If the user does exist, build the security context information // Get the user's permissions and groups and build the collections // return the context back to the caller return secCtx; } Creating the Auction Security Realm SchemaFor our example, we are going to be storing users, groups, and permissions in a relational database. We will need to create the database schema for these three tables. Listing 14.2 shows the DDL for our security schema. Listing 14.2 The Sample Auction Security Realm Schema#======================== # Table SecGroup # Represents a Security Group for the Auction Application #======================== CREATE TABLE SecGroup ( Id int NOT NULL, Name varchar (255) NOT NULL, Description varchar (255) NOT NULL ); ALTER TABLE SecGroup ADD CONSTRAINT PK_SecGroup PRIMARY KEY (Id); #======================== # Table SecUser # Represents a Security User for the Auction Application #======================== CREATE TABLE SecUser ( Id int NOT NULL, FirstName varchar (255) NOT NULL, LastName varchar (255) NOT NULL, EmailAddress varchar (255) NULL, UserName varchar (255) NOT NULL, Password varchar (255) NOT NULL, AccountCreatedDate date NOT NULL, LastLoginDate date NULL, IsAccountActive varchar (1) NOT NULL ); ALTER TABLE SecUser ADD CONSTRAINT PK_SecUser PRIMARY KEY (Id); #======================== # Table SecUserSecGroup # Represents a link table between User and Group #======================== CREATE TABLE SecUserSecGroup ( SecUserId int NOT NULL, SecGroupId int NOT NULL, IsGroupActive varchar (1) NOT NULL ); ALTER TABLE SecUserSecGroup ADD CONSTRAINT PK_SecUserSecGroup PRIMARY KEY (SecUserId, SecGroupId); ALTER TABLE SecUserSecGroup ADD CONSTRAINT FK_SecUserSecGroup_User FOREIGN KEY (SecUserId) REFERENCES SecUser (Id); ALTER TABLE SecUserSecGroup ADD CONSTRAINT FK_SecUserSecGroup_Group FOREIGN KEY (SecGroupId) REFERENCES SecGroup (Id); #======================== # Table Permission # Represents a permission that a user or group can perform #======================== CREATE TABLE Permission ( Id int NOT NULL, Name varchar (255) NOT NULL, Description varchar (255) NOT NULL ); ALTER TABLE Permission ADD CONSTRAINT PK_Permission PRIMARY KEY (Id); #======================== # Table SecUserPermission # Represents a link table between User and Permission #======================== CREATE TABLE SecUserPermission ( SecUserId int NOT NULL, PermissionId int NOT NULL ); ALTER TABLE SecUserPermission ADD CONSTRAINT PK_SecUserPermission PRIMARY KEY (SecUserId, PermissionId); ALTER TABLE SecUserPermission ADD CONSTRAINT FK_SecUserPermission_User FOREIGN KEY (SecUserId) REFERENCES SecUser (Id); ALTER TABLE SecUserPermission ADD CONSTRAINT FK_SecUserPerm_Permission FOREIGN KEY (PermissionId) REFERENCES Permission (Id); #======================== # Table SecGroupPermission # Represents a link table between Group and Permission #======================== CREATE TABLE SecGroupPermission ( SecGroupId int NOT NULL, PermissionId int NOT NULL ); ALTER TABLE SecGroupPermission ADD CONSTRAINT PK_SecGroupPermission PRIMARY KEY (SecGroupId, PermissionId); ALTER TABLE SecGroupPermission ADD CONSTRAINT FK_SecGroupPermission_Group FOREIGN KEY (SecGroupId) REFERENCES SecGroup (Id); ALTER TABLE SecGroupPermission ADD CONSTRAINT FK_SecGroupPerm_Permission FOREIGN KEY (PermissionId) REFERENCES Permission (Id); In the schema in Listing 14.2, we've included only the necessary attributes to understand the design. You might need more attributes, depending on your requirements. This schema was tested on Oracle 8i. If you want to test this on other database vendors, you might have to make a few modifications to the schema to support these other vendors . Don't worry too much about the exact definition of this security schema. There could be some normalization or denormalization on it, depending on how much you like normalized databases. The schema isn't presented to show a good database design, but rather to give you an idea of what types of table and attributes must be supported for the auction security realm. Designing Access to the Security RealmThe security objects are pretty lightweight objects, which means they don't contain many attributes or even a great deal of business logic. Choosing whether the security objects are entity beans or not depends on several factors, one of which is your particular strategy for making things entity beans or not. Making concepts in your logical model an entity bean can solve many of the transactional and concurrency headaches associated with persistent objects. You also can gain much more scalability because the container handles the life cycle for the enterprise bean and is able to shuffle resources as needed. All these things are true; however, you still don't want everything from your logical model translating into an entity bean. For one thing, if no client needs to access the data remotely, this can be one argument for not being an entity bean. Of course, there are others. For more information on what types of objects should be entity beans, see "Entity Beans," p. 105 . If you don't want to use entity beans and you are using bean-managed persistence, an alternative solution is to access the data in the relational database directly from the session beans. The session beans could return immutable view classes back to the client by using JDBC directly from within the session bean methods. There are some benefits to using this approach; however, there are some transactional and concurrency problems that you must deal with. If the administrator is updating the data and the client is reading it, concurrency must be dealt with to ensure that no transactional problems occur. There are several Object to Relational Mapping (ORM) frameworks that can provide help in this area. One such ORM is TOPLink from WebGain. TOPLink provides both a CMP and a BMP solution for EJB persistence and also deals with more complicated issues, such as data caching and transactional issues. We are not going to provide the entire solution for the data-accessing problem here, but the recommendation for the auction example would be to use session beans to access the data and return immutable view classes to the client. This solution is not the most elegant, but it will definitely work for this situation. Using Security Attributes in the Web TierWhen the Web tier calls the SecurityManager session bean and attempts to log in, an object called SecurityContext will be returned if the login is successful. Each user will have its own SecurityContext instance cached in the HttpSession . The SecurityContext object will be used to validate the user's permission to perform actions within the auction Web site. The SecurityContext object will contain a collection of roles or groups of which the user is a member, as well as a collection of permissions. The permission collection is a union of all the permissions from the groups to which the user belongs, as well as any extra permissions assigned directly to the user. This type of security design could also support negative permissions as well, rather than just additive. For example, if a user belongs to an "auctioneer" group that has a cancelAuction permission, we could easily add a column to the permission table called Additive that determines whether the permission should be added to the list of permissions or subtracted from the list. This gives the administrator more flexibility to determine how permissions are assigned or removed. Listing 14.3 shows the SecurityContext class that will be built by the security session bean and returned to the Web tier. Listing 14.3 The SecurityContext Source Representing a User's Security Context Information/** * Title: SecurityContext<p> * Description: The user's security context information.<p> */ package com.que.ejb20.entity.businessobjects; import java.security.Principal; import java.util.Collection; public class SecurityContext implements java.io.Serializable { private java.security.Principal principal; private java.util.Collection groups; private java.util.Collection permissions; public SecurityContext() { super(); } public Principal getPrincipal() { return principal; } public void setPrincipal( Principal newPrincipal ) { principal = newPrincipal; } public void setGroups( Collection newGroups ) { groups = newGroups; } public Collection getGroups() { return groups; } public void setPermissions( Collection newPermissions ) { permissions = newPermissions; } public Collection getPermissions() { return permissions; } public boolean isUserInRole( String role ) { return this.groups.contains(role); } public boolean checkPermission( String permission ) { return this.permissions.contains( permission ); } } The two most important methods in the SecurityContext class are isUserInRole and checkPermission . The client uses these two methods to determine to which security roles the user belongs and which security permissions have been granted to the user. Here's a code fragment that illustrates how a client can use these methods to hide or show a Close Auction button: // Assume a SecurityContext has already been obtained // Verify that the user is acting as the role auctioneer for this session if ( secCtx.isUserInRole( "auctioneer" )) { // Check to see if they have the closeAuction permission if ( secCtx.checkPermission( "closeAuction" )) { // Show a close auction button here } } The Principal reference in the SecurityContext class is an interface from the Java 2 security package that represents the user. We are going to provide a UserView class that implements this interface and acts as the user in the system. Listing 14.4 shows the UserView class that is built by the SecurityManager and returned to the client. Listing 14.4 This Class Represents a User Within the System/** * Title: UserView<p> * Description: A view of the user in the system<p> */ package com.que.ejb20.entity.businessobjects; import java.io.Serializable; import java.security.Principal; public class UserView implements Principal, Serializable { private String firstName; private String lastName; private String emailAddress; private String userName; private String password; private String accountCreatedDate; private String lastLoginDate; private String active; public UserView() { super(); } public String getFirstName() { return firstName; } public void setFirstName(String newFirstName) { firstName = newFirstName; } public void setLastName(String newLastName) { lastName = newLastName; } public String getLastName() { return lastName; } public void setEmailAddress(String newEmailAddress) { emailAddress = newEmailAddress; } public String getEmailAddress() { return emailAddress; } public void setUserName(String newUserName) { userName = newUserName; } public String getUserName() { return userName; } public void setPassword(String newPassword) { password = newPassword; } public String getPassword() { return password; } public void setAccountCreatedDate(String newAccountCreatedDate) { accountCreatedDate = newAccountCreatedDate; } public String getAccountCreatedDate() { return accountCreatedDate; } public void setLastLoginDate(String newLastLoginDate) { lastLoginDate = newLastLoginDate; } public String getLastLoginDate() { return lastLoginDate; } public void setActive(String newActive) { active = newActive; } public String getActive() { return active; } // Method implementation needed because this class implements the // java.security public String getName() { return this.userName; } } If you were using JSP pages on the client, it might be a good idea to wrap the security checks inside a JSP Custom Tag library. This might make the JSP pages a little cleaner because they wouldn't have to access the SecurityContext object directly. If an instance of a SecurityContext class were stored in the session for each user, the JSP Tag handler would have direct access to it and could do all the checks for the JSP Page. The JSP page would just include the tag library information within it. You can find more information on JSP custom tags at http://java.sun.com/products/jsp/taglibraries.html Propagating the PrincipalThere's one final note on implementing security in this manner. When a client invokes an operation on an enterprise bean, the principal is propagated to the EJB object from the client. This propagation is taken care of by the container or the stub classes, depending on the vendor's implementation. With the security design that we have discussed here, the Principal is not being associated with the current thread by our implementation, and it might not be propagated to the enterprise bean correctly. This would present some problems if the container has security attributes set up for the beans. It might be a good idea to associate the Principal that is returned in the SecurityContext object with the current thread; this sometimes is referred to as Thread-Specific Storage (TSS) . Some EJB servers will associate the JNDI principal with the current thread when a client creates a remote interface and uses this principal to invoke calls on enterprise beans. In theory, the JNDI principal and credential are supposed to be used only to authenticate and authorize access to the naming and directory service. Several vendors use this security information for calls to the enterprise beans. Just be careful when taking advantage of this because chances are it will not be portable. |