SeamFramework.orgCommunity Documentation
The Seam Security API provides a multitude of security-related features for your Seam-based application, covering such areas as:
Authentication - an extensible, JAAS-based authentication layer that allows users to authenticate against any security provider.
Identity Management - an API for managing a Seam application's users and roles at runtime.
Authorization - an extremely comprehensive authorization framework, supporting user roles, persistent and rule-based permissions, and a pluggable permission resolver for easily implementing customised security logic.
Permission Management - a set of built-in Seam components to allow easy management of an application's security policy.
CAPTCHA support - to assist in the prevention of automated software/scripts abusing your Seam-based site.
And much more
This chapter will cover each of these features in detail.
In some situations it may be necessary to disable Seam Security, for instances during unit tests or because you
are using a different approach to security, such as native JAAS. Simply call the static method
Identity.setSecurityEnabled(false)
to disable the security infrastructure. Of course, it's not
very convenient to have to call a static method when you want to configure the application, so as an alternative
you can control this setting in components.xml:
Entity Security
Hibernate Security Interceptor
Seam Security Interceptor
Page restrictions
Servlet API security integration
Assuming you are planning to take advantage of what Seam Security has to offer, the rest of this chapter documents the plethora of options you have for giving your user an identity in the eyes of the security model (authentication) and locking down the application by establishing constraints (authorization). Let's begin with the task of authentication since that's the foundation of any security model.
The authentication features provided by Seam Security are built upon JAAS (Java Authentication and Authorization Service), and as such provide a robust and highly configurable API for handling user authentication. However, for less complex authentication requirements Seam offers a much more simplified method of authentication that hides the complexity of JAAS.
If you use Seam's Identity Management features (discussed later in this chapter) then it is not necessary to create an authenticator component (and you can skip this section).
The simplified authentication method provided by Seam uses a built-in JAAS login module, SeamLoginModule
, which
delegates authentication to one of your own Seam components. This login module is already configured inside Seam as
part of a default application policy and as such does not require any additional configuration files. It allows you to
write an authentication method using the entity classes that are provided by your own application, or alternatively to
authenticate with some other third party provider. Configuring this simplified form of authentication requires the
identity
component to be configured in components.xml
:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.2.xsd">
<security:identity authenticate-method="#{authenticator.authenticate}"/>
</components>
The EL expression #{authenticator.authenticate}
is a method binding that indicates
the authenticate
method of the authenticator
component will be used
to authenticate the user.
The authenticate-method
property specified for identity
in
components.xml
specifies which method will be used by SeamLoginModule
to authenticate users. This method takes no parameters, and is expected to return a boolean, which indicates
whether authentication is successful or not. The user's username and password can be obtained from
Credentials.getUsername()
and Credentials.getPassword()
,
respectively (you can get a reference to the credentials
component via
Identity.instance().getCredentials()
). Any roles that the user is a member of
should be assigned using Identity.addRole()
. Here's a complete example of an
authentication method inside a POJO component:
@Name("authenticator")
public class Authenticator {
@In EntityManager entityManager;
@In Credentials credentials;
@In Identity identity;
public boolean authenticate() {
try {
User user = (User) entityManager.createQuery(
"from User where username = :username and password = :password")
.setParameter("username", credentials.getUsername())
.setParameter("password", credentials.getPassword())
.getSingleResult();
if (user.getRoles() != null) {
for (UserRole mr : user.getRoles())
identity.addRole(mr.getName());
}
return true;
}
catch (NoResultException ex) {
return false;
}
}
}
In the above example, both User
and UserRole
are application-specific
entity beans. The roles
parameter is populated with the roles that the user is a member
of, which should be added to the Set
as literal string values, e.g. "admin", "user".
In this case, if the user record is not found and a NoResultException
thrown, the
authentication method returns false
to indicate the authentication failed.
When writing an authenticator method, it is important that it is kept minimal and free from any side-effects. This is because there is no guarantee as to how many times the authenticator method will be called by the security API, and as such it may be invoked multiple times during a single request. Because of this, any special code that should execute upon a successful or failed authentication should be written by implementing an event observer. See the section on Security Events further down in this chapter for more information about which events are raised by Seam Security.
The Identity.addRole()
method behaves differently depending on whether the current
session is authenticated or not. If the session is not authenticated, then addRole()
should only be called during the authentication process. When called here, the
role name is placed into a temporary list of pre-authenticated roles. Once authentication is successful,
the pre-authenticated roles then become "real" roles, and calling Identity.hasRole()
for those roles will then return true. The following sequence diagram represents the list of pre-authenticated
roles as a first class object to show more clearly how it fits in to the authentication process.
If the current session is already authenticated, then calling Identity.addRole()
will
have the expected effect of immediately granting the specified role to the current user.
Say for example, that upon a successful login that some user statistics must be
updated. This would be done by writing an event observer for the
org.jboss.seam.security.loginSuccessful
event, like this:
@In UserStats userStats;
@Observer("org.jboss.seam.security.loginSuccessful")
public void updateUserStats()
{
userStats.setLastLoginDate(new Date());
userStats.incrementLoginCount();
}
This observer method can be placed anywhere, even in the Authenticator component itself. You can find more information about security-related events later in this chapter.
The credentials
component provides both username
and password
properties, catering for the most common authentication scenario. These properties can be bound directly to the
username and password fields on a login form. Once these properties are set, calling
identity.login()
will authenticate the user using the provided credentials.
Here's an example of a simple login form:
<div>
<h:outputLabel for="name" value="Username"/>
<h:inputText id="name" value="#{credentials.username}"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret id="password" value="#{credentials.password}"/>
</div>
<div>
<h:commandButton value="Login" action="#{identity.login}"/>
</div>
Similarly, logging out the user is done by calling #{identity.logout}
. Calling this
action will clear the security state of the currently authenticated user, and invalidate the user's session.
So to sum up, there are the three easy steps to configure authentication:
Configure an authentication method in components.xml
.
Write an authentication method.
Write a login form so that the user can authenticate.
Seam Security supports the same kind of "Remember Me" functionality that is commonly encountered in many online web-based applications. It is actually supported in two different "flavours", or modes - the first mode allows the username to be stored in the user's browser as a cookie, and leaves the entering of the password up to the browser (many modern browsers are capable of remembering passwords).
The second mode supports the storing of a unique token in a cookie, and allows a user to authenticate automatically upon returning to the site, without having to provide a password.
Automatic client authentication with a persistent cookie stored on the client machine is dangerous. While convenient for users, any cross-site scripting security hole in your website would have dramatically more serious effects than usual. Without the authentication cookie, the only cookie to steal for an attacker with XSS is the cookie of the current session of a user. This means the attack only works when the user has an open session - which should be a short timespan. However, it is much more attractive and dangerous if an attacker has the possibility to steal a persistent Remember Me cookie that allows him to login without authentication, at any time. Note that this all depends on how well you protect your website against XSS attacks - it's up to you to make sure that your website is 100% XSS safe - a non-trival achievement for any website that allows user input to be rendered on a page.
Browser vendors recognized this issue and introduced a "Remember Passwords" feature - today almost all browsers support this. Here, the browser remembers the login username and password for a particular website and domain, and fills out the login form automatically when you don't have an active session with the website. If you as a website designer then offer a convenient login keyboard shortcut, this approach is almost as convenient as a "Remember Me" cookie and much safer. Some browsers (e.g. Safari on OS X) even store the login form data in the encrypted global operation system keychain. Or, in a networked environment, the keychain can be transported with the user (between laptop and desktop for example), while browser cookies are usually not synchronized.
To summarize: While everyone is doing it, persistent "Remember Me" cookies with automatic authentication are a bad practice and should not be used. Cookies that "remember" only the users login name, and fill out the login form with that username as a convenience, are not an issue.
To enable the remember me feature for the default (safe, username only) mode, no special configuration is required.
In your login form, simply bind the remember me checkbox to rememberMe.enabled
, like in the following
example:
<div>
<h:outputLabel for="name" value="User name"/>
<h:inputText id="name" value="#{credentials.username}"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret id="password" value="#{credentials.password}" redisplay="true"/>
</div>
<div class="loginRow">
<h:outputLabel for="rememberMe" value="Remember me"/>
<h:selectBooleanCheckbox id="rememberMe" value="#{rememberMe.enabled}"/>
</div>
To use the automatic, token-based mode of the remember me feature, you must first configure a token store. The
most common scenario is to store these authentication tokens within a database (which Seam supports), however it
is possible to implement your own token store by implementing the org.jboss.seam.security.TokenStore
interface. This section will assume you will be using the provided JpaTokenStore
implementation
to store authentication tokens inside a database table.
The first step is to create a new Entity which will contain the tokens. The following example shows a possible structure that you may use:
@Entity
public class AuthenticationToken implements Serializable {
private Integer tokenId;
private String username;
private String value;
@Id @GeneratedValue
public Integer getTokenId() {
return tokenId;
}
public void setTokenId(Integer tokenId) {
this.tokenId = tokenId;
}
@TokenUsername
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@TokenValue
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
As you can see from this listing, a couple of special annotations, @TokenUsername
and
@TokenValue
are used to configure the username and token properties of the entity. These
annotations are required for the entity that will contain the authentication tokens.
The next step is to configure JpaTokenStore
to use this entity bean to store and retrieve
authentication tokens. This is done in components.xml
by specifying the token-class
attribute:
<security:jpa-token-store token-class="org.jboss.seam.example.seamspace.AuthenticationToken"/>
Once this is done, the last thing to do is to configure the RememberMe
component in
components.xml
also. Its mode
should be set to autoLogin
:
<security:remember-me mode="autoLogin"/>
That is all that is required - automatic authentication will now occur for users revisiting your site (as long as they check the "remember me" checkbox).
To ensure that users are automatically authenticated when returning to the site, the following section should be placed in components.xml:
<event type="org.jboss.seam.security.notLoggedIn">
<action execute="#{redirect.captureCurrentView}"/>
<action execute="#{identity.tryLogin()}"/>
</event>
<event type="org.jboss.seam.security.loginSuccessful">
<action execute="#{redirect.returnToCapturedView}"/>
</event>
To prevent users from receiving the default error page in response to a security error, it's recommended that
pages.xml
is configured to redirect security errors to a more "pretty" page. The two
main types of exceptions thrown by the security API are:
NotLoggedInException
- This exception is thrown if the user attempts to access a
restricted action or page when they are not logged in.
AuthorizationException
- This exception is only thrown if the user is already logged in,
and they have attempted to access a restricted action or page for which they do not have the necessary
privileges.
In the case of a NotLoggedInException
, it is recommended that the user is redirected to
either a login or registration page so that they can log in. For an AuthorizationException
,
it may be useful to redirect the user to an error page. Here's an example of a pages.xml
file that redirects both of these security exceptions:
<pages>
...
<exception class="org.jboss.seam.security.NotLoggedInException">
<redirect view-id="/login.xhtml">
<message>You must be logged in to perform this action</message>
</redirect>
</exception>
<exception class="org.jboss.seam.security.AuthorizationException">
<end-conversation/>
<redirect view-id="/security_error.xhtml">
<message>You do not have the necessary security privileges to perform this action.</message>
</redirect>
</exception>
</pages>
Most web applications require even more sophisticated handling of login redirection, so Seam includes some special functionality for handling this problem.
You can ask Seam to redirect the user to a login screen when an unauthenticated user tries to access a particular view (or wildcarded view id) as follows:
<pages login-view-id="/login.xhtml">
<page view-id="/members/*" login-required="true"/>
...
</pages>
This is less of a blunt instrument than the exception handler shown above, but should probably be used in conjunction with it.
After the user logs in, we want to automatically send them back where they came from, so
they can retry the action that required logging in. If you add the following event listeners
to components.xml
, attempts to access a restricted view while not logged
in will be remembered, so that upon the user successfully logging in they will be redirected
to the originally requested view, with any page parameters that existed in the original
request.
<event type="org.jboss.seam.security.notLoggedIn">
<action execute="#{redirect.captureCurrentView}"/>
</event>
<event type="org.jboss.seam.security.postAuthenticate">
<action execute="#{redirect.returnToCapturedView}"/>
</event>
Note that login redirection is implemented as a conversation-scoped mechanism, so don't end
the conversation in your authenticate()
method.
Although not recommended for use unless absolutely necessary, Seam provides means for authenticating
using either HTTP Basic or HTTP Digest (RFC 2617) methods. To use either form of authentication,
the authentication-filter
component must be enabled in components.xml:
<web:authentication-filter url-pattern="*.seam" auth-type="basic"/>
To enable the filter for basic authentication, set auth-type
to basic
,
or for digest authentication, set it to digest
. If using digest authentication, the
key
and realm
must also be set:
<web:authentication-filter url-pattern="*.seam" auth-type="digest" key="AA3JK34aSDlkj" realm="My App"/>
The key
can be any String value. The realm
is the name of the
authentication realm that is presented to the user when they authenticate.
If using digest authentication, your authenticator class should extend the abstract class
org.jboss.seam.security.digest.DigestAuthenticator
, and use the
validatePassword()
method to validate the user's plain text password
against the digest request. Here is an example:
public boolean authenticate()
{
try
{
User user = (User) entityManager.createQuery(
"from User where username = :username")
.setParameter("username", identity.getUsername())
.getSingleResult();
return validatePassword(user.getPassword());
}
catch (NoResultException ex)
{
return false;
}
}
This section explores some of the advanced features provided by the security API for addressing more complex security requirements.
If you would rather not use the simplified JAAS configuration provided by the Seam Security API, you may
instead delegate to the default system JAAS configuration by providing a jaas-config-name
property in components.xml
. For example, if you are using JBoss AS and wish to use
the other
policy (which uses the UsersRolesLoginModule
login module
provided by JBoss AS), then the entry in components.xml
would look like this:
<security:identity jaas-config-name="other"/>
Please keep in mind that doing this does not mean that your user will be authenticated in whichever container your Seam application is deployed in. It merely instructs Seam Security to authenticate itself using the configured JAAS security policy.
Identity Management provides a standard API for the management of a Seam application's users and roles,
regardless of which identity store (database, LDAP, etc) is used on the backend. At the center
of the Identity Management API is the identityManager
component, which provides
all the methods for creating, modifying and deleting users, granting and revoking roles, changing passwords,
enabling and disabling user accounts, authenticating users and listing users and roles.
Before it may be used, the identityManager
must first be configured with one or more
IdentityStore
s. These components do the actual work of interacting with the backend
security provider, whether it be a database, LDAP server, or something else.
The identityManager
component allows for separate identity stores to be configured
for authentication and authorization operations. This means that it is possible for users to
be authenticated against one identity store, for example an LDAP directory, yet have their roles
loaded from another identity store, such as a relational database.
Seam provides two IdentityStore
implementations out of the box;
JpaIdentityStore
uses a relational database to store user and role information,
and is the default identity store that is used if nothing is explicitly configured in the
identityManager
component. The other implementation that is provided is
LdapIdentityStore
, which uses an LDAP directory to store users and roles.
There are two configurable properties for the identityManager
component -
identityStore
and roleIdentityStore
. The value for these
properties must be an EL expression referring to a Seam component implementing the
IdentityStore
interface. As already mentioned,
if left unconfigured then JpaIdentityStore
will be assumed by default. If
only the identityStore
property is configured, then the same value will be used for
roleIdentityStore
also. For example, the following entry in
components.xml
will configure identityManager
to use
an LdapIdentityStore
for both user-related and role-related operations:
<security:identity-manager identity-store="#{ldapIdentityStore}"/>
The following example configures identityManager
to use an LdapIdentityStore
for user-related operations, and JpaIdentityStore
for role-related operations:
<security:identity-manager
identity-store="#{ldapIdentityStore}"
role-identity-store="#{jpaIdentityStore}"/>
The following sections explain both of these identity store implementations in greater detail.
This identity store allows for users and roles to be stored inside a relational database. It is designed to be as unrestrictive as possible in regards to database schema design, allowing a great deal of flexibility in the underlying table structure. This is achieved through the use of a set of special annotations, allowing entity beans to be configured to store user and role records.
JpaIdentityStore
requires that both the user-class
and
role-class
properties are configured. These properties should refer to the
entity classes that are to be used to store both user and role records, respectively. The following
example shows the configuration from components.xml
in the SeamSpace example:
<security:jpa-identity-store
user-class="org.jboss.seam.example.seamspace.MemberAccount"
role-class="org.jboss.seam.example.seamspace.MemberRole"/>
As already mentioned, a set of special annotations are used to configure entity beans for storing users and roles. The following table lists each of the annotations, and their descriptions.
Table 15.1. User Entity Annotations
Annotation |
Status |
Description |
---|---|---|
|
Required |
This annotation marks the field or method containing the user's username. |
|
Required |
This annotation marks the field or method containing the user's password. It allows a @UserPassword(hash = "md5")
If an application requires a hash algorithm that isn't supported natively by Seam, it
is possible to extend the |
|
Optional |
This annotation marks the field or method containing the user's first name. |
|
Optional |
This annotation marks the field or method containing the user's last name. |
|
Optional |
This annotation marks the field or method containing the enabled status of the user. This should be a boolean property, and if not present then all user accounts are assumed to be enabled. |
|
Required |
This annotation marks the field or method containing the roles of the user. This property will be described in more detail further down. |
Table 15.2. Role Entity Annotations
Annotation |
Status |
Description |
---|---|---|
|
Required |
This annotation marks the field or method containing the name of the role. |
|
Optional |
This annotation marks the field or method containing the group memberships of the role. |
|
Optional |
This annotation marks the field or method indicating whether the role is conditional or not. Conditional roles are explained later in this chapter. |
As mentioned previously, JpaIdentityStore
is designed to be as flexible as possible when
it comes to the database schema design of your user and role tables. This section looks at a number of
possible database schemas that can be used to store user and role records.
In this bare minimal example, a simple user and role table are linked via a
many-to-many relationship using a cross-reference table named UserRoles
.
@Entity
public class User {
private Integer userId;
private String username;
private String passwordHash;
private Set<Role> roles;
@Id @GeneratedValue
public Integer getUserId() { return userId; }
public void setUserId(Integer userId) { this.userId = userId; }
@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; }
@UserRoles
@ManyToMany(targetEntity = Role.class)
@JoinTable(name = "UserRoles",
joinColumns = @JoinColumn(name = "UserId"),
inverseJoinColumns = @JoinColumn(name = "RoleId"))
public Set<Role> getRoles() { return roles; }
public void setRoles(Set<Role> roles) { this.roles = roles; }
}
@Entity public class Role { private Integer roleId; private String rolename; @Id @Generated public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } @RoleName public String getRolename() { return rolename; } public void setRolename(String rolename) { this.rolename = rolename; } }
This example builds on the above minimal example by including all of the optional fields, and allowing group memberships for roles.
@Entity
public class User {
private Integer userId;
private String username;
private String passwordHash;
private Set<Role> roles;
private String firstname;
private String lastname;
private boolean enabled;
@Id @GeneratedValue
public Integer getUserId() { return userId; }
public void setUserId(Integer userId) { this.userId = userId; }
@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; }
@UserFirstName
public String getFirstname() { return firstname; }
public void setFirstname(String firstname) { this.firstname = firstname; }
@UserLastName
public String getLastname() { return lastname; }
public void setLastname(String lastname) { this.lastname = lastname; }
@UserEnabled
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
@UserRoles
@ManyToMany(targetEntity = Role.class)
@JoinTable(name = "UserRoles",
joinColumns = @JoinColumn(name = "UserId"),
inverseJoinColumns = @JoinColumn(name = "RoleId"))
public Set<Role> getRoles() { return roles; }
public void setRoles(Set<Role> roles) { this.roles = roles; }
}
@Entity public class Role { private Integer roleId; private String rolename; private boolean conditional; @Id @Generated public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } @RoleName public String getRolename() { return rolename; } public void setRolename(String rolename) { this.rolename = rolename; } @RoleConditional public boolean isConditional() { return conditional; } public void setConditional(boolean conditional) { this.conditional = conditional; } @RoleGroups @ManyToMany(targetEntity = Role.class) @JoinTable(name = "RoleGroups", joinColumns = @JoinColumn(name = "RoleId"), inverseJoinColumns = @JoinColumn(name = "GroupId")) public Set<Role> getGroups() { return groups; } public void setGroups(Set<Role> groups) { this.groups = groups; } }
When using JpaIdentityStore
as the identity store implementation with IdentityManager
,
a few events are raised as a result of invoking certain IdentityManager
methods.
This event is raised in response to calling IdentityManager.createUser()
. Just before the user
entity is persisted to the database, this event will be raised passing the entity instance as an event parameter.
The entity will be an instance of the user-class
configured for JpaIdentityStore
.
Writing an observer for this event may be useful for setting additional field values on the entity, which aren't set
as part of the standard createUser()
functionality.
This event is also raised in response to calling IdentityManager.createUser()
. However, it is
raised after the user entity has already been persisted to the database. Like the EVENT_PRE_PERSIST_USER
event, it also passes the entity instance as an event parameter. It may be useful to observe this event if you also
need to persist other entities that reference the user entity, for example contact detail records or other user-specific
data.
This identity store implementation is designed for working with user records stored in an LDAP directory. It is very highly configurable, allowing great flexibility in how both users and roles are stored in the directory. The following sections describe the configuration options for this identity store, and provide some configuration examples.
The following table describes the available properties that can be configured in components.xml
for
LdapIdentityStore
.
Table 15.3. LdapIdentityStore Configuration Properties
Property |
Default Value |
Description |
---|---|---|
|
|
The address of the LDAP server. |
|
|
The port number that the LDAP server is listening on. |
|
|
The Distinguished Name (DN) of the context containing user records. |
|
|
This value is prefixed to the front of the username to locate the user's record. |
|
|
This value is appended to the end of the username to locate the user's record. |
|
|
The DN of the context containing role records. |
|
|
This value is prefixed to the front of the role name to form the DN for locating the role record. |
|
|
This value is appended to the role name to form the DN for locating the role record. |
|
|
This is the context used to bind to the LDAP server. |
|
|
These are the credentials (the password) used to bind to the LDAP server. |
|
|
This is the name of the attribute of the user record that contains the list of roles that the user is a member of. |
|
|
This boolean property indicates whether the role attribute of the user record is itself a distinguished name. |
|
|
Indicates which attribute of the user record contains the username. |
|
|
Indicates which attribute of the user record contains the user's password. |
|
|
Indicates which attribute of the user record contains the user's first name. |
|
|
Indicates which attribute of the user record contains the user's last name. |
|
|
Indicates which attribute of the user record contains the user's full (common) name. |
|
|
Indicates which attribute of the user record determines whether the user is enabled. |
|
|
Indicates which attribute of the role record contains the name of the role. |
|
|
Indicates which attribute determines the class of an object in the directory. |
|
|
An array of the object classes that new role records should be created as. |
|
|
An array of the object classes that new user records should be created as. |
The following configuration example shows how LdapIdentityStore
may be configured for
an LDAP directory running on fictional host directory.mycompany.com
. The users are stored
within this directory under the context ou=Person,dc=mycompany,dc=com
, and are identified
using the uid
attribute (which corresponds to their username). Roles are stored in their
own context, ou=Roles,dc=mycompany,dc=com
and referenced from the user's entry via the
roles
attribute. Role entries are identified by their common name (the cn
attribute)
, which corresponds to the role name. In this example, users may be disabled by setting the value of their
enabled
attribute to false.
<security:ldap-identity-store
server-address="directory.mycompany.com"
bind-DN="cn=Manager,dc=mycompany,dc=com"
bind-credentials="secret"
user-DN-prefix="uid="
user-DN-suffix=",ou=Person,dc=mycompany,dc=com"
role-DN-prefix="cn="
role-DN-suffix=",ou=Roles,dc=mycompany,dc=com"
user-context-DN="ou=Person,dc=mycompany,dc=com"
role-context-DN="ou=Roles,dc=mycompany,dc=com"
user-role-attribute="roles"
role-name-attribute="cn"
user-object-classes="person,uidObject"
enabled-attribute="enabled"
/>
Writing your own identity store implementation allows you to authenticate and perform identity management
operations against security providers that aren't supported out of the box by Seam. Only a single class is
required to achieve this, and it must implement the
org.jboss.seam.security.management.IdentityStore
interface.
Please refer to the JavaDoc for IdentityStore
for a description of the methods that
must be implemented.
If you are using the Identity Management features in your Seam application, then it is not required
to provide an authenticator component (see previous Authentication section) to enable authentication.
Simply omit the authenticator-method
from the identity
configuration
in components.xml
, and the SeamLoginModule
will by default
use IdentityManager
to authenticate your application's users, without any special
configuration required.
The IdentityManager
can be accessed either by injecting it into your Seam
component as follows:
@In IdentityManager identityManager;
or by accessing it through its static instance()
method:
IdentityManager identityManager = IdentityManager.instance();
The following table describes IdentityManager
's API methods:
Table 15.4. Identity Management API
Method |
Returns |
Description |
---|---|---|
|
|
Creates a new user account, with the specified name and password. Returns |
|
|
Deletes the user account with the specified name. Returns |
|
|
Creates a new role, with the specified name. Returns |
|
|
Deletes the role with the specified name. Returns |
|
|
Enables the user account with the specified name. Accounts that are not enabled are
not able to authenticate. Returns |
|
|
Disables the user account with the specified name. Returns |
|
|
Changes the password for the user account with the specified name. Returns
|
|
|
Returns |
|
|
Grants the specified role to the specified user or role. The role must already exist for it to
be granted. Returns |
|
|
Revokes the specified role from the specified user or role. Returns |
|
|
Returns |
|
|
Returns a list of all user names, sorted in alpha-numeric order. |
|
|
Returns a list of all user names filtered by the specified filter parameter, sorted in alpha-numeric order. |
|
|
Returns a list of all role names. |
|
|
Returns a list of the names of all the roles explicitly granted to the specified user name. |
|
|
Returns a list of the names of all the roles implicitly granted to the specified user name.
Implicitly granted roles include those that are not directly granted to a user, rather they are
granted to the roles that the user is a member of. For example, is the |
|
|
Authenticates the specified username and password using the configured Identity Store. Returns
|
|
|
Adds the specified role as a member of the specified group. Returns true if the operation is successful. |
|
|
Removes the specified role from the specified group. Returns true if the operation is successful. |
|
|
Lists the names of all roles. |
Using the Identity Management API requires that the calling user has the appropriate authorization to invoke
its methods. The following table describes the permission requirements for each of the methods in
IdentityManager
. The permission targets listed below are literal String values.
Table 15.5. Identity Management Security Permissions
Method |
Permission Target |
Permission Action |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The following code listing provides an example set of security rules that grants access to all
Identity Management-related methods to members of the admin
role:
rule ManageUsers no-loop activation-group "permissions" when check: PermissionCheck(name == "seam.user", granted == false) Role(name == "admin") then check.grant(); end rule ManageRoles no-loop activation-group "permissions" when check: PermissionCheck(name == "seam.role", granted == false) Role(name == "admin") then check.grant(); end
The security API produces a number of default faces messages for various security-related events.
The following table lists the message keys that can be used to override these messages by specifying
them in a message.properties
resource file. To suppress the message, just put the
key with an empty value in the resource file.
Table 15.6. Security Message Keys
Message Key |
Description |
---|---|
|
This message is produced when a user successfully logs in via the security API. |
|
This message is produced when the login process fails, either because the user provided an incorrect username or password, or because authentication failed in some other way. |
|
This message is produced when a user attempts to perform an action or access a page that requires a security check, and the user is not currently authenticated. |
|
This message is produced when a user that is already authenticated attempts to log in again. |
There are a number of authorization mechanisms provided by the Seam Security API for securing access to
components, component methods, and pages. This section describes each of these. An important thing to
note is that if you wish to use any of the advanced features (such as rule-based permissions) then
your components.xml
may need to be configured to support this - see the Configuration section
above.
Seam Security is built around the premise of users being granted roles and/or permissions, allowing them to perform operations that may not otherwise be permissible for users without the necessary security privileges. Each of the authorization mechanisms provided by the Seam Security API are built upon this core concept of roles and permissions, with an extensible framework providing multiple ways to secure application resources.
A role is a group, or type, of user that may have been granted certain privileges for performing one or more specific actions within an application. They are simple constructs, consisting of just a name such as "admin", "user", "customer", etc. They can be granted either to users (or in some cases to other roles), and are used to create logical groups of users for the convenient assignment of specific application privileges.
A permission is a privilege (sometimes once-off) for performing a single, specific action. It is entirely possible to build an application using nothing but permissions, however roles offer a higher level of convenience when granting privileges to groups of users. They are slightly more complex in structure than roles, essentially consisting of three "aspects"; a target, an action, and a recipient. The target of a permission is the object (or an arbitrary name or class) for which a particular action is allowed to be performed by a specific recipient (or user). For example, the user "Bob" may have permission to delete customer objects. In this case, the permission target may be "customer", the permission action would be "delete" and the recipient would be "Bob".
Within this documentation, permissions are generally represented in the form target:action
(omitting the recipient, although in reality one is always required).
Let's start by examining the simplest form of authorization, component security, starting with the
@Restrict
annotation.
While using the @Restrict
annotation provides a powerful and flexible method for security component methods
due to its ability to support EL expressions, it is recommended that the typesafe equivalent (described later) be
used, at least for the compile-time safety it provides.
Seam components may be secured either at the method or the class level, using the @Restrict
annotation. If both a method and it's declaring class are annotated with @Restrict
,
the method restriction will take precedence (and the class restriction will not apply). If a method
invocation fails a security check, then an exception will be thrown as per the contract for
Identity.checkRestriction()
(see Inline Restrictions). A @Restrict
on just the component class itself is equivalent to adding @Restrict
to each of its
methods.
An empty @Restrict
implies a permission check of componentName:methodName
.
Take for example the following component method:
@Name("account")
public class AccountAction {
@Restrict public void delete() {
...
}
}
In this example, the implied permission required to call the delete()
method is
account:delete
. The equivalent of this would be to write
@Restrict("#{s:hasPermission('account','delete')}")
. Now let's look at
another example:
@Restrict @Name("account")
public class AccountAction {
public void insert() {
...
}
@Restrict("#{s:hasRole('admin')}")
public void delete() {
...
}
}
This time, the component class itself is annotated with @Restrict
. This means that
any methods without an overriding @Restrict
annotation require an implicit permission check.
In the case of this example, the insert()
method requires a permission of
account:insert
, while the delete()
method requires that the user is a
member of the admin
role.
Before we go any further, let's address the #{s:hasRole()}
expression seen in the above
example. Both s:hasRole
and s:hasPermission
are EL functions, which
delegate to the correspondingly named methods of the Identity
class. These
functions can be used within any EL expression throughout the entirety of the security API.
Being an EL expression, the value of the @Restrict
annotation may reference any objects that
exist within a Seam context. This is extremely useful when performing permission checks for a specific
object instance. Look at this example:
@Name("account")
public class AccountAction {
@In Account selectedAccount;
@Restrict("#{s:hasPermission(selectedAccount,'modify')}")
public void modify() {
selectedAccount.modify();
}
}
The interesting thing to note from this example is the reference to selectedAccount
seen within the hasPermission()
function call. The value of this variable will be
looked up from within the Seam context, and passed to the hasPermission()
method
in Identity
, which in this case can then determine if the user has the required
permission for modifying the specified Account
object.
Sometimes it might be desirable to perform a security check in code, without using the
@Restrict
annotation. In this situation, simply use
Identity.checkRestriction()
to evaluate a security expression, like this:
public void deleteCustomer() {
Identity.instance().checkRestriction("#{s:hasPermission(selectedCustomer,'delete')}");
}
If the expression specified doesn't evaluate to true
, either
if the user is not logged in, a NotLoggedInException
exception is thrown or
if the user is logged in, an AuthorizationException
exception is thrown.
It is also possible to call the hasRole()
and hasPermission()
methods directly from Java code:
if (!Identity.instance().hasRole("admin"))
throw new AuthorizationException("Must be admin to perform this action");
if (!Identity.instance().hasPermission("customer", "create"))
throw new AuthorizationException("You may not create new customers");
One indication of a well designed user interface is that the user is not presented with options for which they don't have the necessary privileges to use. Seam Security allows conditional rendering of either 1) sections of a page or 2) individual controls, based upon the privileges of the user, using the very same EL expressions that are used for component security.
Let's take a look at some examples of interface security. First of all, let's pretend that we have a
login form that should only be rendered if the user is not already logged in. Using the
identity.isLoggedIn()
property, we can write this:
<h:form class="loginForm" rendered="#{not identity.loggedIn}">
If the user isn't logged in, then the login form will be rendered - very straight forward so far.
Now let's pretend there is a menu on the page that contains some actions which should only be accessible
to users in the manager
role. Here's one way that these could be written:
<h:outputLink action="#{reports.listManagerReports}" rendered="#{s:hasRole('manager')}">
Manager Reports
</h:outputLink>
This is also quite straight forward. If the user is not a member of the manager
role, then the outputLink will not be rendered. The rendered
attribute can
generally be used on the control itself, or on a surrounding <s:div>
or
<s:span>
control.
Now for something more complex. Let's say you have a h:dataTable
control on a
page listing records for which you may or may not wish to render action links depending on the
user's privileges. The s:hasPermission
EL function allows us to pass in an
object parameter which can be used to determine whether the user has the requested permission
for that object or not. Here's how a dataTable with secured links might look:
<h:dataTable value="#{clients}" var="cl">
<h:column>
<f:facet name="header">Name</f:facet>
#{cl.name}
</h:column>
<h:column>
<f:facet name="header">City</f:facet>
#{cl.city}
</h:column>
<h:column>
<f:facet name="header">Action</f:facet>
<s:link value="Modify Client" action="#{clientAction.modify}"
rendered="#{s:hasPermission(cl,'modify')"/>
<s:link value="Delete Client" action="#{clientAction.delete}"
rendered="#{s:hasPermission(cl,'delete')"/>
</h:column>
</h:dataTable>
Page security requires that the application is using a pages.xml
file, however is
extremely simple to configure. Simply include a <restrict/>
element within
the page
elements that you wish to secure. If no explicit restriction is specified
by the restrict
element, an implied permission of /viewId.xhtml:render
will be checked when the page is accessed via a non-faces (GET) request, and a permission of
/viewId.xhtml:restore
will be required when any JSF postback (form submission) originates
from the page. Otherwise, the specified restriction will be evaluated as a standard security expression.
Here's a couple of examples:
<page view-id="/settings.xhtml">
<restrict/>
</page>
This page has an implied permission of /settings.xhtml:render
required for non-faces
requests and an implied permission of /settings.xhtml:restore
for faces requests.
<page view-id="/reports.xhtml">
<restrict>#{s:hasRole('admin')}</restrict>
</page>
Both faces and non-faces requests to this page require that the user is a member of the
admin
role.
Seam security also makes it possible to apply security restrictions to read, insert, update and delete actions for entities.
To secure all actions for an entity class, add a @Restrict
annotation on the class
itself:
@Entity
@Name("customer")
@Restrict
public class Customer {
...
}
If no expression is specified in the @Restrict
annotation, the default security check
that is performed is a permission check of entity:action
, where the permission target
is the entity instance, and the action
is either read
, insert
,
update
or delete
.
It is also possible to only restrict certain actions, by placing a @Restrict
annotation
on the relevent entity lifecycle method (annotated as follows):
@PostLoad
- Called after an entity instance is loaded from the database. Use this
method to configure a read
permission.
@PrePersist
- Called before a new instance of the entity is inserted. Use this method
to configure an insert
permission.
@PreUpdate
- Called before an entity is updated. Use this method
to configure an update
permission.
@PreRemove
- Called before an entity is deleted. Use this method
to configure a delete
permission.
Here's an example of how an entity would be configured to perform a security check for any insert
operations. Please note that the method is not required to do anything, the only important thing in regard to
security is how it is annotated:
@PrePersist @Restrict
public void prePersist() {}
/META-INF/orm.xml
You can also specify the call back method in /META-INF/orm.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
version="1.0">
<entity class="Customer">
<pre-persist method-name="prePersist" />
</entity>
</entity-mappings>
Of course, you still need to annotate the prePersist()
method on Customer
with @Restrict
And here's an example of an entity permission rule that checks if the authenticated user is allowed to insert
a new MemberBlog
record (from the seamspace example). The entity for which the security
check is being made is automatically inserted into the working memory (in this case MemberBlog
):
rule InsertMemberBlog no-loop activation-group "permissions" when principal: Principal() memberBlog: MemberBlog(member : member -> (member.getUsername().equals(principal.getName()))) check: PermissionCheck(target == memberBlog, action == "insert", granted == false) then check.grant(); end;
This rule will grant the permission memberBlog:insert
if the currently authenticated
user (indicated by the Principal
fact) has the same name as the member for which the
blog entry is being created. The "principal: Principal()
" structure that can be seen in the
example code is a variable binding - it binds the instance of the Principal
object from the
working memory (placed there during authentication) and assigns it to a variable called principal
.
Variable bindings allow the value to be referred to in other places, such as the following line which compares the
member's username to the Principal
name. For more details, please refer to the JBoss Rules documentation.
Finally, we need to install a listener class that integrates Seam security with your JPA provider.
Security checks for EJB3 entity beans are performed with an EntityListener
.
You can install this listener by using the following META-INF/orm.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
version="1.0">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="org.jboss.seam.security.EntitySecurityListener"/>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
Seam provides a number of annotations that may be used as an alternative to @Restrict
, which have
the added advantage of providing compile-time safety as they don't support arbitrary EL expressions in the same way
that @Restrict
does.
Out of the box, Seam comes with annotations for standard CRUD-based permissions, however it is a simple matter to
add your own. The following annotations are provided in the org.jboss.seam.annotations.security
package:
@Insert
@Read
@Update
@Delete
To use these annotations, simply place them on the method or parameter for which you wish to perform a security check. If placed on a method, then they should specify a target class for which the permission will be checked. Take the following example:
@Insert(Customer.class) public void createCustomer() { ... }
In this example, a permission check will be performed for the user to ensure that they have the rights to create
new Customer
objects. The target of the permission check will be Customer.class
(the actual java.lang.Class
instance itself), and the action is the lower case representation of the
annotation name, which in this example is insert
.
It is also possible to annotate the parameters of a component method in the same way. If this is done, then it is not required to specify a permission target (as the parameter value itself will be the target of the permission check):
public void updateCustomer(@Update Customer customer) { ... }
To create your own security annotation, you simply need to annotate it with @PermissionCheck
, for example:
@Target({METHOD, PARAMETER})
@Documented
@Retention(RUNTIME)
@Inherited
@PermissionCheck
public @interface Promote {
Class value() default void.class;
}
If you wish to override the default permisison action name (which is the lower case version of the annotation name) with
another value, you can specify it within the @PermissionCheck
annotation:
@PermissionCheck("upgrade")
In addition to supporting typesafe permission annotation, Seam Security also provides typesafe role annotations that
allow you to restrict access to component methods based on the role memberships of the currently authenticated user.
Seam provides one such annotation out of the box, org.jboss.seam.annotations.security.Admin
, used
to restrict access to a method to users that are a member of the admin
role (so long as your
own application supports such a role). To create your own role annotations, simply meta-annotate them with
org.jboss.seam.annotations.security.RoleCheck
, like in the following example:
@Target({METHOD}) @Documented @Retention(RUNTIME) @Inherited @RoleCheck public @interface User { }
Any methods subsequently annotated with the @User
annotation as shown in the above example
will be automatically intercepted and the user checked for the membership of the corresponding role name
(which is the lower case version of the annotation name, in this case user
).
Seam Security provides an extensible framework for resolving application permissions. The following class diagram shows an overview of the main components of the permission framework:
The relevant classes are explained in more detail in the following sections.
This is actually an interface, which provides methods for resolving individual object permissions. Seam provides
the following built-in PermissionResolver
implementations, which are described in more detail later
in the chapter:
RuleBasedPermissionResolver
- This permission resolver uses Drools to resolve rule-based
permission checks.
PersistentPermissionResolver
- This permission resolver stores object permissions in a
persistent store, such as a relational database.
It is very simple to implement your own permission resolver. The PermissionResolver
interface defines only two methods that must be implemented, as shown by the following table. By deploying
your own PermissionResolver
implementation in your Seam project, it will be automatically
scanned during deployment and registered with the default ResolverChain
.
Table 15.7. PermissionResolver interface
Return type |
Method |
Description |
---|---|---|
|
|
This method must resolve whether the currently authenticated user (obtained via a call to
|
|
|
This method should remove any objects from the specified set, that would
return |
As they are cached in the user's session, any custom PermissionResolver
implementations must adhere to a couple of restrictions. Firstly, they may not contain any
state that is finer-grained than session scope (and the scope of the component itself should
either be application or session). Secondly, they must not use dependency
injection as they may be accessed from multiple threads simultaneously. In fact, for
performance reasons it is recommended that they are annotated with
@BypassInterceptors
to bypass Seam's interceptor stack altogether.
A ResolverChain
contains an ordered list of PermissionResolver
s, for the
purpose of resolving object permissions for a particular object class or permission target.
The default ResolverChain
consists of all permission resolvers discovered during
application deployment. The org.jboss.seam.security.defaultResolverChainCreated
event is raised (and the ResolverChain
instance passed as an event parameter)
when the default ResolverChain
is created. This allows additional resolvers that
for some reason were not discovered during deployment to be added, or for resolvers that are in the
chain to be re-ordered or removed.
The following sequence diagram shows the interaction between the components of the permission framework during a
permission check (explanation follows). A permission check can originate from a number of possible sources,
for example - the security interceptor, the s:hasPermission
EL function, or via an API
call to Identity.checkPermission
:
1. A permission check is initiated somewhere (either in code or via an EL
expression) resulting in a call to Identity.hasPermission()
.
1.1. Identity
invokes
PermissionMapper.resolvePermission()
, passing in the
permission to be resolved.
1.1.1. PermissionMapper
maintains a Map
of
ResolverChain
instances, keyed by class. It uses this map
to locate the correct ResolverChain
for the permission's
target object. Once it has the correct ResolverChain
, it
retrieves the list of PermissionResolver
s it contains via
a call to ResolverChain.getResolvers()
.
1.1.2. For each PermissionResolver
in the ResolverChain
,
the PermissionMapper
invokes its hasPermission()
method,
passing in the permission instance to be checked. If any of the PermissionResolver
s
return true
, then the permission check has succeeded and the
PermissionMapper
also returns true
to Identity
.
If none of the PermissionResolver
s return true, then the permission check
has failed.
One of the built-in permission resolvers provided by Seam, RuleBasedPermissionResolver
allows permissions to be evaluated based on a set of Drools (JBoss Rules) security rules. A couple of the
advantages of using a rule engine are 1) a centralized location for the business logic that is used to
evaluate user permissions, and 2) speed - Drools uses very efficient algorithms for evaluating large
numbers of complex rules involving multiple conditions.
If using the rule-based permission features provided by Seam Security, the following jar files are required by Drools to be distributed with your project:
drools-api.jar
drools-compiler.jar
drools-core.jar
drools-decisiontables.jar
drools-templates.jar
janino.jar
antlr-runtime.jar
mvel2.jar
The configuration for RuleBasedPermissionResolver
requires that a Drools rule base is first
configured in components.xml
. By default, it expects that the rule base is named
securityRules
, as per the following example:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:drools="http://jboss.com/products/seam/drools"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.2.xsd
http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd
http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.2.xsd
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.2.xsd">
<drools:rule-base name="securityRules">
<drools:rule-files>
<value>/META-INF/security.drl</value>
</drools:rule-files>
</drools:rule-base>
</components>
The default rule base name can be overridden by specifying the security-rules
property for RuleBasedPermissionResolver
:
<security:rule-based-permission-resolver security-rules="#{prodSecurityRules}"/>
Once the RuleBase
component is configured, it's time to write the security rules.
The first step to writing security rules is to create a new rule file in the /META-INF
directory of your application's jar file. Usually this file would be named something like
security.drl
, however you can name it whatever you like as long as it is configured
correspondingly in components.xml
.
So what should the security rules file contain? At this stage it might be a good idea to at least skim through the Drools documentation, however to get started here's an extremely simple example:
package MyApplicationPermissions; import org.jboss.seam.security.permission.PermissionCheck; import org.jboss.seam.security.Role; rule CanUserDeleteCustomers when c: PermissionCheck(target == "customer", action == "delete") Role(name == "admin") then c.grant(); end
Let's break this down step by step. The first thing we see is the package declaration. A package in Drools is essentially a collection of rules. The package name can be anything you want - it doesn't relate to anything else outside the scope of the rule base.
The next thing we can notice is a couple of import statements for the PermissionCheck
and Role
classes. These imports inform the rules engine that we'll be referencing
these classes within our rules.
Finally we have the code for the rule. Each rule within a package should be given a unique name (usually
describing the purpose of the rule). In this case our rule is called CanUserDeleteCustomers
and will be used to check whether a user is allowed to delete a customer record.
Looking at the body of the rule definition we can notice two distinct sections. Rules have what is known
as a left hand side (LHS) and a right hand side (RHS). The LHS consists of the conditional part of the
rule, i.e. a list of conditions which must be satisfied for the rule to fire. The LHS is represented by
the when
section. The RHS is the consequence, or action section of the rule that will
only be fired if all of the conditions in the LHS are met. The RHS is represented by the
then
section. The end of the rule is denoted by the end
line.
If we look at the LHS of the rule, we see two conditions listed there. Let's examine the first condition:
c: PermissionCheck(target == "customer", action == "delete")
In plain english, this condition is stating that there must exist a PermissionCheck
object
with a target
property equal to "customer", and an action
property equal
to "delete" within the working memory.
So what is the working memory? Also known as a "stateful session" in Drools terminology, the working memory
is a session-scoped object that contains the contextual information that is required by the rules engine to
make a decision about a permission check. Each time the hasPermission()
method is called,
a temporary PermissionCheck
object, or Fact, is inserted into the
working memory. This PermissionCheck
corresponds exactly to the permission that is being
checked, so for example if you call hasPermission("account", "create")
then a
PermissionCheck
object with a target
equal to "account" and
action
equal to "create" will be inserted into the working memory for the duration of the
permission check.
Besides the PermissionCheck
facts, there is also a org.jboss.seam.security.Role
fact for each of the roles that the authenticated user is a member of. These Role
facts
are synchronized with the user's authenticated roles at the beginning of every permission check. As a consequence,
any Role
object that is inserted into the working memory during the course of a permission
check will be removed before the next permission check occurs, if the authenticated user is not actually a member of
that role. Besides the PermissionCheck
and Role
facts, the working
memory also contains the java.security.Principal
object that was created as a result of
the authentication process.
It is also possible to insert additional long-lived facts into the working memory by calling
RuleBasedPermissionResolver.instance().getSecurityContext().insert()
,
passing the object as a parameter. The exception to this is Role
objects, which as
already discussed are synchronized at the start of each permission check.
Getting back to our simple example, we can also notice that the first line of our LHS is prefixed with
c:
. This is a variable binding, and is used to refer back to the object that is
matched by the condition (in this case, the PermissionCheck
). Moving on to the
second line of our LHS, we see this:
Role(name == "admin")
This condition simply states that there must be a Role
object with a
name
of "admin" within the working memory. As already mentioned, user roles are inserted into
the working memory at the beginning of each permission check. So, putting both conditions together, this
rule is essentially saying "I will fire if you are checking for the customer:delete
permission and the user is a member of the admin
role".
So what is the consequence of the rule firing? Let's take a look at the RHS of the rule:
c.grant()
The RHS consists of Java code, and in this case is invoking the grant()
method of the c
object, which as already mentioned is a variable binding
for the PermissionCheck
object. Besides the name
and
action
properties of the PermissionCheck
object, there
is also a granted
property which is initially set to false
.
Calling grant()
on a PermissionCheck
sets the
granted
property to true
, which means that the permission
check was successful, allowing the user to carry out whatever action the permission check was
intended for.
So far we have only seen permission checks for String-literal permission targets. It is of course also
possible to write security rules for permission targets of more complex types. For example, let's say that you wish
to write a security rule to allow your users to create blog comments. The following rule demonstrates
how this may be expressed, by requiring the target of the permission check to be an instance of
MemberBlog
, and also requiring that the currently authenticated user is a member of the
user
role:
rule CanCreateBlogComment no-loop activation-group "permissions" when blog: MemberBlog() check: PermissionCheck(target == blog, action == "create", granted == false) Role(name == "user") then check.grant(); end
It is possible to implement a wildcard permission check (which allows all actions for a given permission
target), by omitting the action
constraint for the PermissionCheck
in
your rule, like this:
rule CanDoAnythingToCustomersIfYouAreAnAdmin when c: PermissionCheck(target == "customer") Role(name == "admin") then c.grant(); end;
This rule allows users with the admin
role to perform any action for
any customer
permission check.
Another built-in permission resolver provided by Seam, PersistentPermissionResolver
allows permissions to be loaded from persistent storage, such as a relational database. This permission
resolver provides ACL style instance-based security, allowing for specific object permissions to be assigned
to individual users and roles. It also allows for persistent, arbitrarily-named permission targets (not
necessarily object/class based) to be assigned in the same way.
Before it can be used, PersistentPermissionResolver
must be configured with a
valid PermissionStore
in components.xml
. If not configured,
it will attempt to use the default permission store, JpaIdentityStore
(see section
further down for details). To use a permission store other than the default, configure the
permission-store
property as follows:
<security:persistent-permission-resolver permission-store="#{myCustomPermissionStore}"/>
A permission store is required for PersistentPermissionResolver
to connect to the
backend storage where permissions are persisted. Seam provides one PermissionStore
implementation out of the box, JpaPermissionStore
, which is used to store
permissions inside a relational database. It is possible to write your own permission store
by implementing the PermissionStore
interface, which defines the following
methods:
Table 15.8. PermissionStore interface
Return type |
Method |
Description |
---|---|---|
|
|
This method should return a |
|
|
This method should return a |
|
|
This method should return a |
|
|
This method should persist the specified |
|
|
This method should persist all of the |
|
|
This method should remove the specified |
|
|
This method should remove all of the |
|
|
This method should return a list of all the available actions (as Strings) for the class of the specified target object. It is used in conjunction with permission management to build the user interface for granting specific class permissions (see section further down). |
This is the default PermissionStore
implementation (and the only one provided by Seam), which
uses a relational database to store permissions. Before it can be used it must be configured with either one or
two entity classes for storing user and role permissions. These entity classes must be annotated with a special
set of security annotations to configure which properties of the entity correspond to various aspects of the
permissions being stored.
If you wish to use the same entity (i.e. a single database table) to store both user and role permissions, then
only the user-permission-class
property is required to be configured. If you wish to use
separate tables for storing user and role permissions, then in addition to the user-permission-class
property you must also configure the role-permission-class
property.
For example, to configure a single entity class to store both user and role permissions:
<security:jpa-permission-store user-permission-class="com.acme.model.AccountPermission"/>
To configure separate entity classes for storing user and role permissions:
<security:jpa-permission-store user-permission-class="com.acme.model.UserPermission"
role-permission-class="com.acme.model.RolePermission"/>
As mentioned, the entity classes that contain the user and role permissions must be configured with a
special set of annotations, contained within the org.jboss.seam.annotations.security.permission
package.
The following table lists each of these annotations along with a description of how they are used:
Table 15.9. Entity Permission annotations
Annotation |
Target |
Description |
---|---|---|
|
|
This annotation identifies the property of the entity that will contain the permission target. The property
should be of type |
|
|
This annotation identifies the property of the entity that will contain the permission action. The property
should be of type |
|
|
This annotation identifies the property of the entity that will contain the recipient user for the permission. It should
be of type |
|
|
This annotation identifies the property of the entity that will contain the recipient role for the permission. It should
be of type |
|
|
This annotation should be used when the same entity/table is used to store both user and role permissions. It identifies
the property of the entity that is used to discriminate between user and role permissions. By default, if the column
value contains the string literal @PermissionDiscriminator(userValue = "u", roleValue = "r") |
Here is an example of an entity class that is used to store both user and role permissions. The following class can be found inside the SeamSpace example:
@Entity
public class AccountPermission implements Serializable {
private Integer permissionId;
private String recipient;
private String target;
private String action;
private String discriminator;
@Id @GeneratedValue
public Integer getPermissionId() {
return permissionId;
}
public void setPermissionId(Integer permissionId) {
this.permissionId = permissionId;
}
@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;
}
}
As can be seen in the above example, the getDiscriminator()
method has been annotated
with the @PermissionDiscriminator
annotation, to allow JpaPermissionStore
to
determine which records represent user permissions and which represent role permissions. In addition, it
can also be seen that the getRecipient()
method is annotated with both
@PermissionUser
and @PermissionRole
annotations. This is perfectly valid,
and simply means that the recipient
property of the entity will either contain the name
of the user or the name of the role, depending on the value of the discriminator
property.
A further set of class-specific annotations can be used to configure a specific set of allowable permissions
for a target class. These permissions can be found in the org.jboss.seam.annotation.security.permission
package:
Table 15.10. Class Permission Annotations
Annotation |
Target |
Description |
---|---|---|
|
|
A container annotation, this annotation may contain an array of |
|
|
This annotation defines a single allowable permission action for the target class. Its |
Here's an example of the above annotations in action. The following class can also be found in the SeamSpace example:
@Permissions({
@Permission(action = "view"),
@Permission(action = "comment")
})
@Entity
public class MemberImage implements Serializable {
This example demonstrates how two allowable permission actions, view
and comment
can be declared for the entity class MemberImage
.
By default, multiple permissions for the same target object and recipient will be persisted as a single database record,
with the action
property/column containing a comma-separated list of the granted actions. To reduce
the amount of physical storage required to persist a large number of permissions, it is possible to use a bitmasked
integer value (instead of a comma-separated list) to store the list of permission actions.
For example, if recipient "Bob" is granted both the view
and comment
permissions
for a particular MemberImage
(an entity bean) instance, then by default the action
property of the
permission entity will contain "view,comment
", representing the two granted permission actions.
Alternatively, if using bitmasked values for the permission actions, as defined like so:
@Permissions({
@Permission(action = "view", mask = 1),
@Permission(action = "comment", mask = 2)
})
@Entity
public class MemberImage implements Serializable {
The action
property will instead simply contain "3" (with both the 1 bit and 2 bit switched on). Obviously
for a large number of allowable actions for any particular target class, the storage required for the permission records
is greatly reduced by using bitmasked actions.
Obviously, it is very important that the mask
values specified are powers of 2.
When storing or looking up permissions, JpaPermissionStore
must be able to uniquely identify specific
object instances to effectively operate on its permissions. To achieve this, an identifier strategy
may be assigned to each target class for the generation of unique identifier values. Each identifier strategy
implementation knows how to generate unique identifiers for a particular type of class, and it is a simple matter to
create new identifier strategies.
The IdentifierStrategy
interface is very simple, declaring only two methods:
public interface IdentifierStrategy {
boolean canIdentify(Class targetClass);
String getIdentifier(Object target);
}
The first method, canIdentify()
simply returns true
if the identifier strategy
is capable of generating a unique identifier for the specified target class. The second method,
getIdentifier()
returns the unique identifier value for the specified target object.
Seam provides two IdentifierStrategy
implementations, ClassIdentifierStrategy
and EntityIdentifierStrategy
(see next sections for details).
To explicitly configure a specific identifier strategy to use for a particular class, it should be annotated with
org.jboss.seam.annotations.security.permission.Identifier
, and the value should be set to
a concrete implementation of the IdentifierStrategy
interface. An optional name
property can also be specified, the effect of which is dependent upon the actual IdentifierStrategy
implementation used.
This identifier strategy is used to generate unique identifiers for classes, and will use the value of the
name
(if specified) in the @Identifier
annotation. If there is no
name
property provided, then it will attempt to use the component name of the class
(if the class is a Seam component), or as a last resort it will create an identifier based on the name
of the class (excluding the package name). For example, the identifier for the following class will
be "customer
":
@Identifier(name = "customer")
public class Customer {
The identifier for the following class will be "customerAction
":
@Name("customerAction")
public class CustomerAction {
Finally, the identifier for the following class will be "Customer
":
public class Customer {
This identifier strategy is used to generate unique identifiers for entity beans. It does so by
concatenating the entity name (or otherwise configured name) with a string representation of the
primary key value of the entity. The rules for generating the name section of the identifier are
similar to ClassIdentifierStrategy
. The primary key value (i.e. the
id of the entity) is obtained using the PersistenceProvider
component, which is able to correctly determine the value regardless of which persistence implementation
is used within the Seam application. For entities not annotated with @Entity
, it is
necessary to explicitly configure the identifier strategy on the entity class itself, for example:
@Identifier(value = EntityIdentifierStrategy.class)
public class Customer {
For an example of the type of identifier values generated, assume we have the following entity class:
@Entity
public class Customer {
private Integer id;
private String firstName;
private String lastName;
@Id
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
For a Customer
instance with an id
value of 1
,
the value of the identifier would be "Customer:1
". If the entity class is annotated
with an explicit identifier name, like so:
@Entity
@Identifier(name = "cust")
public class Customer {
Then a Customer
with an id
value of 123
would have an identifier value of "cust:123
".
In much the same way that Seam Security provides an Identity Management API for the management of users and roles,
it also provides a Permissions Management API for the management of persistent user permissions, via the
PermissionManager
component.
The PermissionManager
component is an application-scoped Seam component that provides a number of
methods for managing permissions. Before it can be used, it must be configured with a permission store (although by
default it will attempt to use JpaPermissionStore
if it is available). To explicitly configure a
custom permission store, specify the permission-store
property in components.xml:
<security:permission-manager permission-store="#{ldapPermissionStore}"/>
The following table describes each of the available methods provided by PermissionManager
:
Table 15.11. PermissionManager API methods
Return type |
Method |
Description |
---|---|---|
|
|
Returns a list of |
|
|
Returns a list of |
|
|
Persists (grants) the specified |
|
|
Persists (grants) the specified list of |
|
|
Removes (revokes) the specified |
|
|
Removes (revokes) the specified list of |
|
|
Returns a list of the available actions for the specified target object. The actions that this
method returns are dependent on the |
Invoking the methods of PermissionManager
requires that the currently-authenticated user
has the appropriate authorization to perform that management operation. The following table lists the required
permissions that the current user must have.
Table 15.12. Permission Management Security Permissions
Method |
Permission Target |
Permission Action |
---|---|---|
|
The specified |
|
|
The target of the specified |
|
|
The target of the specified |
|
|
Each of the targets of the specified list of |
|
|
The target of the specified |
|
|
Each of the targets of the specified list of |
|
Seam includes basic support for serving sensitive pages via the HTTPS protocol. This is easily
configured by specifying a scheme
for the page in pages.xml
.
The following example shows how the view /login.xhtml
is configured to use
HTTPS:
<page view-id="/login.xhtml" scheme="https"/>
This configuration is automatically extended to both s:link
and
s:button
JSF controls, which (when specifying the view
)
will also render the link using the correct protocol. Based on the previous example, the following
link will use the HTTPS protocol because /login.xhtml
is configured to use it:
<s:link view="/login.xhtml" value="Login"/>
Browsing directly to a view when using the incorrect protocol will cause a
redirect to the same view using the correct protocol. For example, browsing
to a page that has scheme="https"
using HTTP will cause a redirect to the same
page using HTTPS.
It is also possible to configure a default scheme for all pages. This is useful
if you wish to use HTTPS for a only few pages. If no default scheme is specified then the normal
behavior is to continue use the current scheme. So once the user accessed a page that required
HTTPS, then HTTPS would continue to be used after the user navigated away to other non-HTTPS pages.
(While this is good for security, it is not so great for performance!). To define HTTP as the
default scheme
, add this line to pages.xml
:
<page view-id="*" scheme="http" />
Of course, if none of the pages in your application use HTTPS then it is not required to specify a default scheme.
You may configure Seam to automatically invalidate the current HTTP session each time the scheme
changes. Just add this line to components.xml
:
<web:session invalidate-on-scheme-change="true"/>
This option helps make your system less vulnerable to sniffing of the session id or leakage of sensitive data from pages using HTTPS to other pages using HTTP.
If you wish to configure the HTTP and HTTPS ports manually, they may be configured in
pages.xml
by specifying the http-port
and
https-port
attributes on the pages
element:
<pages xmlns="http://jboss.com/products/seam/pages"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.2.xsd"
no-conversation-view-id="/home.xhtml"
login-view-id="/login.xhtml"
http-port="8080"
https-port="8443">
Though strictly not part of the security API, Seam provides a built-in CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) algorithm to prevent automated processes from interacting with your application.
To get up and running, it is necessary to configure the Seam Resource Servlet, which will provide the Captcha
challenge images to your pages. This requires the following entry in web.xml
:
<servlet>
<servlet-name>Seam Resource Servlet</servlet-name>
<servlet-class>org.jboss.seam.servlet.SeamResourceServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Seam Resource Servlet</servlet-name>
<url-pattern>/seam/resource/*</url-pattern>
</servlet-mapping>
Adding a CAPTCHA challenge to a form is extremely easy. Here's an example:
<h:graphicImage value="/seam/resource/captcha"/>
<h:inputText id="verifyCaptcha" value="#{captcha.response}" required="true">
<s:validate />
</h:inputText>
<h:message for="verifyCaptcha"/>
That's all there is to it. The graphicImage
control displays the CAPTCHA challenge,
and the inputText
receives the user's response. The response is automatically
validated against the CAPTCHA when the form is submitted.
You may customize the CAPTCHA algorithm by overriding the built-in component:
@Name("org.jboss.seam.captcha.captcha")
@Scope(SESSION)
public class HitchhikersCaptcha extends Captcha
{
@Override @Create
public void init()
{
setChallenge("What is the answer to life, the universe and everything?");
setCorrectResponse("42");
}
@Override
public BufferedImage renderChallenge()
{
BufferedImage img = super.renderChallenge();
img.getGraphics().drawOval(5, 3, 60, 14); //add an obscuring decoration
return img;
}
}
The following table describes a number of events (see Chapter 6, Events, interceptors and exception handling) raised by Seam Security in response to certain security-related events.
Table 15.13. Security Events
Event Key |
Description |
---|---|
|
Raised when a login attempt is successful. |
|
Raised when a login attempt fails. |
|
Raised when a user that is already authenticated attempts to log in again. |
|
Raised when a security check fails when the user is not logged in. |
|
Raised when a security check fails when the user is logged in however doesn't have sufficient privileges. |
|
Raised just prior to user authentication. |
|
Raised just after user authentication. |
|
Raised after the user has logged out. |
|
Raised when the user's credentials have been changed. |
|
Raised when the Identity's rememberMe property is changed. |
Sometimes it may be necessary to perform certain operations with elevated privileges, such
as creating a new user account as an unauthenticated user. Seam Security supports such a
mechanism via the RunAsOperation
class. This class allows either the
Principal
or Subject
, or the user's roles to be
overridden for a single set of operations.
The following code example demonstrates how RunAsOperation
is used, by
calling its addRole()
method to provide a set of roles to masquerade
as for the duration of the operation. The execute()
method contains the
code that will be executed with the elevated privileges.
new RunAsOperation() {
public void execute() {
executePrivilegedOperation();
}
}.addRole("admin")
.run();
In a similar way, the getPrincipal()
or getSubject()
methods can also be overriden to specify the Principal
and
Subject
instances to use for the duration of the operation.
Finally, the run()
method is used to carry out the
RunAsOperation
.
Sometimes it might be necessary to extend the Identity component if your application has
special security requirements. The following example (contrived, as credentials would normally
be handled by the Credentials
component instead) shows an extended Identity
component with an additional companyCode
field. The install precendence of APPLICATION
ensures that this extended Identity gets installed in preference to the built-in Identity.
@Name("org.jboss.seam.security.identity")
@Scope(SESSION)
@Install(precedence = APPLICATION)
@BypassInterceptors
@Startup
public class CustomIdentity extends Identity
{
private static final LogProvider log = Logging.getLogProvider(CustomIdentity.class);
private String companyCode;
public String getCompanyCode()
{
return companyCode;
}
public void setCompanyCode(String companyCode)
{
this.companyCode = companyCode;
}
@Override
public String login()
{
log.info("###### CUSTOM LOGIN CALLED ######");
return super.login();
}
}
Note that an Identity
component must be marked @Startup
, so that it is
available immediately after the SESSION
context begins. Failing to do this may render
certain Seam functionality inoperable in your application.
OpenID is a community standard for external web-based authentication. The basic idea is that any web application can supplement (or replace) its local handling of authentication by delegating responsibility to an external OpenID server of the user's chosing. This benefits the user, who no longer has to remember a name and password for every web application he uses, and the developer, who is relieved of some of the burden of maintaining a complex authentication system.
When using OpenID, the user selects an OpenID provider, and the provider assigns the user
an OpenID. The id will take the form of a URL, for example http://maximoburrito.myopenid.com
however,
it's acceptable to leave off the http://
part of the identifier when logging into a site. The web application
(known as a relying party in OpenID-speak) determines which OpenID server to contact and redirects the user to the remote
site for authentication. Upon successful authentication the user is given
the (cryptographically secure) token proving his identity and is redirected back to the original web application.The
local web application can then be sure the user accessing the application controls the OpenID he presented.
It's important to realize at this point that authentication does not imply authorization. The web application still needs to make a determination of how to use that information. The web application could treat the user as instantly logged in and give full access to the system or it could try and map the presented OpenID to a local user account, prompting the user to register if he hasn't already. The choice of how to handle the OpenID is left as a design decision for the local application.
Seam uses the openid4java package and requires four additional JARs to make use of the Seam integration. These
are: htmlparser.jar
, openid4java.jar
, openxri-client.jar
and openxri-syntax.jar
.
OpenID processing requires the use of the OpenIdPhaseListener
, which should be added to your
faces-config.xml
file. The phase listener processes the callback from the OpenID provider, allowing
re-entry into the local application.
<lifecycle>
<phase-listener>org.jboss.seam.security.openid.OpenIdPhaseListener</phase-listener>
</lifecycle>
With this configuration, OpenID support is available to your application.
The OpenID support component, org.jboss.seam.security.openid.openid
, is installed automatically if the openid4java
classes are on the classpath.
To initiate an OpenID login, you can present a simply form to the user asking for the user's OpenID. The #{openid.id}
value
accepts the user's OpenID and the #{openid.login}
action initiates an authentication request.
<h:form>
<h:inputText value="#{openid.id}" />
<h:commandButton action="#{openid.login}" value="OpenID Login"/>
</h:form>
When the user submits the login form, he will be redirected to his OpenID provider. The user will eventually
return to your application through the Seam pseudo-view /openid.xhtml
, which is
provided by the OpenIdPhaseListener
. Your application can handle the OpenID response by means
of a pages.xml
navigation from that view, just as if the user had never left your application.
The simplest strategy is to simply login the user immediately. The following navigation rule shows how to handle this using
the #{openid.loginImmediately()}
action.
<page view-id="/openid.xhtml">
<navigation evaluate="#{openid.loginImmediately()}">
<rule if-outcome="true">
<redirect view-id="/main.xhtml">
<message>OpenID login successful...</message>
</redirect>
</rule>
<rule if-outcome="false">
<redirect view-id="/main.xhtml">
<message>OpenID login rejected...</message>
</redirect>
</rule>
</navigation>
</page>
Thie loginImmediately()
action checks to see if the OpenID is valid. If it is valid, it adds an
OpenIDPrincipal to the identity component, marks the user as logged in (i.e. #{identity.loggedIn}
will be true)
and returns true. If the OpenID was not validated, the method returns false, and the user re-enters the application un-authenticated.
If the user's OpenID is valid, it will be accessible using the expression #{openid.validatedId}
and
#{openid.valid}
will be true.
You may not want the user to be immediately logged in to your application. In that case, your navigation
should check the #{openid.valid}
property and redirect the user to a local registration or processing
page. Actions you might take would be asking for more information and creating a local user account or presenting a captcha
to avoid programmatic registrations. When you are done processing, if you want to log the user in, you can call
the loginImmediately
method, either through EL as shown previously or by directly interaction with the
org.jboss.seam.security.openid.OpenId
component. Of course, nothing prevents you from writing custom
code to interact with the Seam identity component on your own for even more customized behaviour.
Logging out (forgetting an OpenID association) is done by calling #{openid.logout}
. If you
are not using Seam security, you can call this method directly. If you are using Seam security, you should
continue to use #{identity.logout}
and install an event handler to capture the logout event, calling
the OpenID logout method.
<event type="org.jboss.seam.security.loggedOut">
<action execute="#{openid.logout}" />
</event>
It's important that you do not leave this out or the user will not be able to login again in the same session.