Methods to shift your enterprise.TM  

Solution Pattern

Role

Allows security roles to be established and enforced based on various user, group, time, value, and custom qualifying criteria. Support for dynamic qualification is implied.

 

 

 

[image]

 

 

Background

Traditionally data objects have been secured by defining the permissions that users must have to access them. Administrators have to manipulate each object to change the access permissions that apply to it. Customarily permissions are defined in an access control list (ACL), which is attached to the object whose access control is being defined. To give a large number of users the same kind of access to the object, all the users are placed in a group and the group is given access by adding it to the ACL. Alternatively each user could be added to the ACL individually, rather than as part of a group.

This approach does not work well in an enterprise business environment. The variety of resources that must be secured, the vast mixture of users inside and outside the organization that need to access the resources, and the range of ways in which the resources must be accessed, make the conventional security models impractical. It would be helpful to change from the traditional model to support access to the line-of-business systems that are common in today’s enterprise.

 

Value and Benefits

One of the best means to provide security access to a line-of-business enterprise is through roles. Roles allow secure resource access to be managed by the kind of job that each user performs, or the department they work in.

When you can narrow down access to the scope of job types, it makes managing the access to various enterprise resources much easier. In effect the security realm protects resources by relatively few symbolic types rather than a potentially overwhelming number of individual users and access control lists.

Besides, in a B2C enterprise, users may show up minute by minute. Trying to provide access permissions for each of those individuals could be impossible unless a well-designed approach is used. Roles provide just such an approach. For example, imagine that a Shopper role is associated with all new users of a B2C e-commerce site. All users that are associated with the Shopper role have a limited set of permissions for accessing system resources. They are permitted to access all public, e-commerce Web site URLs, including the store catalog and shopping cart components. On the other hand, they do not have permission to access the URL or enterprise components used by, say, the Accounting Department. Users must have the Accountant role to do that:

 

 

 

[image]

 

 

 

You can quickly see the benefits of roles as they rapidly simplified how access management is implemented. You will see this point further as we put the Role pattern to work.

 

Putting It to Work

There are a few different approaches to designing roles. I focus on the two approaches here. The exact science of how roles are used to allow or deny system resource access to given users is discussed in the Access Authorization (page #) and Security Policy (page #) pattern.

 

Static Role

Roles can be very simple. In fact a role can be designed as uncomplicated as an entity with a unique name. The name identifies the role and is used as the only means of identifying a user’s access permissions. There need not be any further qualifying criteria other than being in the named role. You are considered to be in the role if you are assigned to the role either as an individual user or as a member of a group. This basic role is a static role, because you may only qualify to be in the role if you are directly or indirectly assigned to the role.

Using the above example of a Shopper, attempting to access the Store Catalog and Shopping Cart components would be permitted because the user can play the role of a Shopper. Here’s how the Store Catalog and Shopping Cart components might implement their access control:

 

 

 

[code]

 

 

On the other hand, the Accounting Department’s General Ledger component might implement access control in this way:

 

 

 

 

[code]

 

 

A user would be in the role if the user where defined as being assigned to the role. The user might be assigned directly, or indirectly as a member of a group that is assigned to the role. The role interface would therefore include two methods of assigning entities to it:

 

 

 

 

[code]

 

 

 

Likely an administrative GUI would represent the API. An administrator or group manager would be responsible for assigning users to the group and assigning groups to the role. At runtime if it is determined that the user is in the required role, access is granted to all system resources protected by the role. So the basic role looks something like this internally while it gathers qualified users and protects sensitive resources:

 

 

 

[image]

 

 

Note that the Accountant role can be played for a total of four users. There are three users in the group Accounting Department, Toni, Cathy, and Mark. One additional user that may play the Accountant role is the CommerceSystem, which is a software component that needs to post e-commerce sales activity to the GeneralLedger component.

While the basic role is very simple, it is also limited. The only way to be in the role is to be statically assigned to the role by an administrator or group manager. As you know, business is often more complex than that. Businesses want to grant access to certain system resources not only if a user is a member of a group, but also at fixed times of day, or if they have spent a minimum amount of money during a given time period, and so forth. In such cases you will want to use an advanced form of role.

 

Dynamic Role

The advanced role strategy combines the static user and group assignment capabilities with dynamic qualifications. Dynamic qualifications are those that must be determined at runtime, not using administration alone.

One of the business-rule-based qualifications for access might be that no Accounting Department personnel may close the books unless it is one of the first five days of the month following the month just ended. So we introduce a new component named BalanceSheet, and provide some business methods such as addEntry() and closeBooks(). While the addEntry() method may be used most any time (except for when the books have been closed for a specific month), the closeBooks() method would be protected by a dynamic role qualification. The role Accounting End of Month seems fitting. [NOTE: Check this example. I am far from being an accountant, so I am not certain that my terminology and example are correct.] The role would check, not only that a user is a member of the Accounting Department group, but also that the day of the month is one of the first five days of the month following the one for which the books will be closed (the month just ended).

Of course you could put additional business logic directly into the closeBooks() method to make the same access check, denying further execution if the day is not valid. But what if the qualifying time window needs change. For example, executive management wants their financial reports one day earlier than has been the case in the past. To limit the access to the first four days of the next month you’d have to go in and edit source code, build, test, and redeploy the component to the production system. On the other hand, placing this particular business rule—the one dealing with security concerns—into a dynamic role would make changing the Accounting End of Month dynamic qualification much easier.

Here’s how dynamic roles work. In essence a role becomes a container for one or more qualification definitions. Each definition is known as a qualifier condition. In object-oriented terms each of the concrete qualifiers would adhere to a common interface that the access management component understands (see introductory diagram). All qualifier conditions taken together are known as a qualifier expression. The entire qualifier expression must evaluate to true in order for the named user to be play the role.

Qualifier conditions are arranged within the container in the order that they should be evaluated. Qualifier conditions are chained together by logical AND or OR operators, or both. Qualifier conditions that are chained by logical AND operators must all be true for the user to qualify to be in the role. For qualifier conditions chained together by logical OR operators, at least one condition must be true. In fact qualifying the user as being in the role upon encountering the first true qualifier is most efficient.

Both logical AND and OR operators may be used in the same qualifier expression, but this approach adds complexity to the strategy. You will have to determine how qualifiers are chained and how the entire role qualifier expression is evaluated. In a programming language parenthesis may be used, but this is likely not an option for an administrative GUI. One method of simplifying the use of both AND and OR in the same qualifier expression is to put all OR conditions first, followed by all AND conditions. If no OR conditions prove true, then the qualifier expression fails before any AND expressions are evaluated. If at least one OR expression proves true and all subsequent AND expressions prove true, the entire qualifier expression is succeeds and the user is qualified as being in the role.

These are some common qualifier conditions listed in alphabetical order. The last four conditions are predefined, while the first condition allows you to provide your own custom conditions:

 

 

 

 

[table]

 

 

 

If the last four predefined common qualifier conditions do not satisfy your needs this pattern also facilities the definition of custom qualifier conditions. You use the same interfaces that the four predefined conditions use, but you provide the custom implementations of the interfaces to supply the new and unique behavior needed to qualify a requester as being in the given role. A very sophisticated custom qualifier condition could provide a scripting language that allows business analysts or developers to code complex business rules that ultimately return a true or false value.

Note that if the Time and/or Value (and perhaps Custom, too) qualifier conditions are used without a User or Group qualifier, then all requesters will qualify to be in the role as long as the Time and/or Value (and/or Custom) conditions are met. To be certain that only specific users are in the role within a timeframe and/or a value range, make sure that you use the User and/or Group qualifier conditions along with the other ones. Let’s now discuss each of the qualifier conditions in more detail to see how they work.

Custom. The idea behind this qualifier condition is to allow any arbitrary condition to be tested. The administrator or manager that configures a Custom qualifier condition supplies two data items, a discriminator and the name of a custom class that implements the access check. In addition to the discriminator, the username of the requester is passed to the custom class at runtime. The two data items provide proper context for the custom class instance to perform any necessary business logic to perform the access validation check. The discriminator may be used to differentiate one kind of qualification from another; that is, a single access check class may implement various different kinds of checks, which are differentiated by the passed discriminator

Group. This qualifier condition has a container with one or more group names assigned to it by an administrator or a manager. At runtime the qualifier condition implementation probes each contained group to see if the requesting user is a member of one of them. If it is a member of at least one group, then the requester passes this qualifying condition. There is no reason to check beyond the first group in which the user may be found.

Time. This qualifier condition contains two date/time values, which are set by an administrator or a manager. One is a start time and the other is an end time. The two times may be within the same 24-hour period, or they may span days. When the request is received to access the role-protected resource, then the current system time is read. If the current time is between the start time and the end time, then the requester passes this access check.

User. This qualifier condition is the most basic one. An administrator or a manager assigns the users to a role. In essence the role acts as a group when individual users are assigned to it. A container, such as a Set, is used to hold a collection of unique usernames. The usernames may be persistence in the same way that the Identity Grouping (page #) pattern persists them. If at runtime the requesting user is found to be in the collection of named users, then the requester passes this qualifying condition. Thus, if User qualifier condition contains the names Anita, Misty, Tim, and Zach, then a requester named Anita will pass this qualification while a user named Gene will not.

Value. This qualifier condition contains two numeric values, a minimum (or low) value and a maximum (or high) value. Both values would be the same if equality were to be tested. An administrator or a manager is responsible for providing the two values to the qualifier condition configuration. The values may be integers or decimals, but both must be of the same type. This is a slightly more complex qualifier condition to implement. When a role is checked the only required data passed to each qualifier condition is the username of the requesting user. So how does the condition obtain a value with which to compare to the minimum and maximum values? The answer is, the requester does not supply the value. Rather, the username of the requester along with the minimum and maximum values as well as a discriminator are given to a special validation class instance. Hence, there are actually four items that must be set in this qualifier condition. The administrator or manger must supply the two values as well as the discriminator string and the name of a class that implements the access check. This class may perform any necessary business logic to obtain the value that is to be compared to the configured minimum and maximum values. The discriminator may be used to differentiate one kind of qualification from another; that is, one access check class may implement various different kinds of checks, which are differentiated by the discriminator. Consequently, this is like the Custom qualifier condition, but with the additional minimum and maximum values and a special validation interface.

 

Security Considerations

Even within security mechanisms themselves, there are security concerns. When specifying the class names used by the Custom and Value qualifier conditions, how does the administrator ensure that the classes are safe and secure executables?

First, the class names provided may be aliases rather than fully qualified class names. The aliases would be maintained by the administrative staff and, thus, could not be replaced by rouge implementations. The actual classes (deployment jars, etc.) are obtained from trusted resources and are deployed in a way that only the secure access control subsystem can access them. Thus, when an administrator or manager specifies the alias for a class, only pre-registered aliases may be used. If the user configuring the qualifier condition enters a name that is not a pre-registered alias, then the configuration is considered to be invalid. In order to use a Custom qualifier condition that is not registered the user will have to contact an administrator or manager to arrange for the unregistered class to be registered.

 

Examples

My Role implementation starts with a standard interface that accommodates the behavior necessary to demonstrate the pattern:

 

 

 

[code]

 

 

 

The core behavior of the interface deals with maintaining a set of qualifier conditions by adding and removing them. Each role has a unique name that must be set. Finally, you may ask each role if a given user/requester is in the role. There are two varieties of the isUserInRole() method, one that takes a UserIF and one that takes a username String.

The two constants at the top of the class definition are used to facilitate a simple AND-OR approach to qualification checking. They are used in conjunction with the setScope() method. If the scope is set to ALL_CONDITIONS, all qualifier conditions must be true for the user to be in the role. This is the AND condition. If the scope is set to ANY_CONDITIONS, one or more conditions may be met for the requester to qualify to be in the role. This is the OR condition. If there is only one qualifier condition in the role, these two scopes are equivalent. I have chosen ALL_CONDITIONS as the default scope (see below). I do not support mixing AND with OR in this implementation.

In all honesty, my observation is that without mixing AND with OR the implementation is incomplete, and a bit frustrating to use. While running simple tests I put both a Group and User qualifier condition into a single role with the default scope (ALL_CONDITIONS). Naturally, most users are either part of a group or placed into the individual User qualifier condition. In that case no user will ever qualify to be in the role unless they are in both containers—something you don’t want to have to support. Switching to ANY_CONDITIONS works fine until you add a third condition, such as Value. You probably want the user to be in either the Group or User condition, and to meet the Value condition. This is an obvious OR with AND role. But in an effort to complete the codification of this pattern without succumbing to scope creep, I’ll have to leave the mixing of AND with OR to your devices. The obvious work-around around for using this implementation’s “problem” is to makes sure that you use either the Group or User condition along with a Time, Value, or other Custom condition, but not both.

Here is my implementation of RoleIF, which is class Role:

 

 

 

[code]

 

 

 

There are no surprises here. The class is implemented as expected. The most interesting method is the detailed isUserInRole(). The method iterates through all qualifier conditions in the order that Set provides them. If the specific condition is qualified, it affects the overall outcome of the test. For simplicity I always iterate over all conditions and test the final outcome at the end of the method. However, I have already admitted that I don’t like this either AND or OR approach.

The fact that my Role class implements interface DirectoryDomainObjectIF indicates that I manage the persistence of roles in my OpenLDAP directory via JNDI. I spare you the details, but assume that the implementation is a lot like that found in Identity Grouping (page #). I have chosen to store all roles under the top-level context ou=roles,dc=jubatus,dc=com.

It’s now appropriate to look at the qualifier conditions, starting with the base interface, QualifierConditionIF:

 

 

 

 

[code]

 

 

 

Every qualifier condition has a unique name and a type. This combination allows the Set to store conditions correctly. The type is the basic type of condition, such as Group, User, Value, etc. The method isUserAccessQualified() allows the implementer to determine whether or not the user/requester passes condition qualification. I implement the basic behavior in an abstract base class, BasicQualifierCondition:

 

 

 

[code]

 

 

 

There is one more abstract base class, which is a subclass of the above. This one is used to qualifier conditions that need to maintain a number of items. It is the BasicQualifierConditionContainer, which is used by the Group and User qualifier conditions:

 

 

 

[code]

 

 

 

The GroupQualifierCondition extends the above abstract base class and maintains all contained group names in its Set. Likewise, the UserQualifierCondition extends the same abstract base class and maintains all contained usernames in its backing Set. Here are the concrete implementations, starting with GroupQualifierCondition:

 

 

 

[code]

 

 

Next is the simpler UserQualifierCondition:

 

 

 

[code]

 

 

 

 

Next are the more interesting custom-based qualifier conditions. Here is the custom condition base class, CustomQualifierCondition:

 

 

 

 

[code]

 

 

 

The classIdentifier and discriminator are as discussed above. I have chosen the more secure approach the custom qualification check classes. I use a registry, where an identifier maps to the actual implementer. I list the registry implementation below. The first responsibility of the method isUserAccessQualified() is to get the custom checker class. In this implementation I arrange for the instance of the class to be created in the context of the current thread’s class loader.

The method isUserAccessQualified() is responsible for actually checking access qualifications for a given user. But the check is actually delegated to the implementer of CustomQualifierConditionCheckerIF:, the class that actually gets registered with the secure class registry:

 

 

 

[code]

 

 

 

As you can see, the CustomQualifierConditionCheckerIF has a single method that requires three arguments, the username being checked, the discriminator, and the special data argument. The special data argument, aQualifierDataArg, is any kind of Object and is specified by the requirements of the custom implementation. But before diving deeper into the checker implementation, let’s first look at a sample outer custom qualifier condition, the ValueQualifierCondition:

 

 

 

 

[code]

 

 

 

This custom qualifier condition supports the testing of all kinds of value ranges, from simple integers all the way to big decimals. It requires the use of a custom qualifier data type that gets passed to the checker implementation. In the case of class ValueQualifierCondition, I supply class ValueQualifierData, which holds the values that the user must qualify for. This simply provides a minimum and maximum value range:

 

 

 

[code]

 

 

 

Now let’s look at an implementation of a value qualifier condition checker:

 

 

 

[code]

 

 

 

This is a mocked up implementation that exists solely for the sake of testing values between 100 and 200. I use a random number generator and take the remainder of dividing by 250. This seems to produce a value that is sometimes within the range and sometimes not. Your implementation would naturally be based on some real, sound business logic. But my example class demonstrates the technique without clouding the approach.

The CustomConditionClassRegistry, a singleton, is my provision for securing custom checker implementations. This implementation does not provide a means for persisting the mappings:

 

 

 

[code]

 

 

 

 

 

Finally, here are a series of tests that use existing groups and users to run the role qualification checks, using both standard and custom qualifier conditions:

 

 

 

 

[code]

 

 

 

Here’s the output from some random testing:

 

 

 

[code]

 

 

 

 

User tristan is part of group admin and the random number generation algorithm produced a value between 100 and 200 when his qualification was checked. User gabrielle is also a member of the admin group, but didn’t get a valid random number generated during her test. Neither bogus nor mogli are part of the admin group, so the check is bound to fail for them.

 

Consequences

Consider the completing forces related to this pattern:

  • Keep It Simple: Your enterprise may not need the use of custom qualifier conditions. In many cases it may be enough to support only the User and Group types. If not, don’t bother supporting them. However, as my implementation demonstrates it is not terribly difficult to support customization.

  • Support AND As Well As OR: To squeeze the most power out of a Role implementation you will want your qualifier conditions to test with both logical AND as well as logical OR. This is more complex, but many commercial offerings support this. It is also worth the trouble of implementing on your own if you need it in a custom security implementation.

 

Related Patterns

The following are patterns that use the Role pattern.

  • Access Authorization (page #): Uses Role to enforce the access security of the system resource that the requester is attempting to access.

  • Security Policy (page #): Maps a given system resource to the access constraints provided by Role.

 

Frameworks and Tools

  • J2EE: See the J2EE specification, in particular the web.xml and ejb-jar.xml document types, for examples of this pattern.

  • Microsoft Access Manager: This Microsoft product provides an implementation of Security Policy.

 

[EOF]