October 17, 2007

Image Handling in Seam Apps part V: Restricting Access

This is the fifth part of a tutorial about how to manage images in your Seam based web application. This tutorial consists of several parts:

Part One: The Database Model
Part Two: File Upload
Part Three: Scaling
Part Four: Displaying and Caching
Part Five: Restricting Access

This fifth installment shows you how to use Seam simplified security mode to restrict access to certain pages and actions. This simple yet powerful mode supports authentication and role based authorization.

If you use seam-gen to generate your projects then most of the work is done for you. Writing an authentication method and defining the roles are the only things you are left with. One way of defining the roles is to have database tables that represent members and roles in your application. The tutorial found here shows you how to define Member and MemberRole classes to serve this purpose.

Authentication

In the login page you need to have an inputText tag with a value binding set to identity.username, an inputSecret tag with a value binding set to identity.password and a commandButton tag with an action set to identity.login.


<h:form id="login-form">
<span class="error"><h:messages globalOnly="true"/></span>
<s:validateAll>
<fieldset>
<legend>Enter your login details</legend>
<div>
<h:outputLabel for="membername" value="Display name"/>
<h:inputText styleClass="input-field" id="membername"
value="#{identity.username}" size="40" maxlength="40"
required="true"/>
<h:message styleClass="error" for="membername"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret styleClass="input-field" id="password"
value="#{identity.password}" size="40" maxlength="40"
required="true"/>
<h:message styleClass="error" for="password"/>
</div>
</fieldset>

<div class="submit-buttons">
<h:commandButton value="Login" action="#{identity.login}"
id="registerButton"/>
</div>
</s:validateAll>
</h:form>

The identity.login method propagates the credentials to the method specified in the security:identity tag defined in components.xml:

<security:identity authenticate-method="#{authenticator.authenticate}" />

The authenticate method validates the credentials and then retrieves a list of roles that member has. These roles are then added to the identity component.

@Name("authenticator")
public class Authenticator {
@Logger
Log log;

@In
Identity identity;

@In(create = true, value = "entityManager")
private EntityManager em;

public boolean authenticate() {
log.info("authenticating #0", identity.getUsername());
String name = identity.getUsername();
if (name == null || name.trim().length() == 0)
return false;

String password = identity.getPassword();
String hash = null;
if (password != null && password.trim().length() != 0)
hash = DigestUtils.shaHex(identity.getPassword());
else
return false;
String query = "from gs_member m "
+ "WHERE m.memberName = :name AND m.passwordHash = :hash";
Object result = null;
try {
result = em.createQuery(query).setParameter("name", name)
.setParameter("hash", hash).getSingleResult();
} catch (NoResultException e) {
log.info("No member was found with name[#0]", name);
FacesMessages.instance().add("Could not find a member "
+ "with the given name and password.");
return false;
}
Member member = (Member) result;
if (member.getStatus().equals(MemberStatus.INACTIVE.getStatus())) {
FacesMessages.instance().add("Sorry but you need "
+ "to activate your account first. Please "
+ "follow the instructions in the email "
+ "you received at the time of registration.");
return false;
} else {
if (member.getRoles() != null) {
for (MemberRole role : member.getRoles())
identity.addRole(role.getName());
}
return true;
}
}
}

Authorization

The @Restrict annotation can be used to restrict access to methods based on a role. The #{s:hasRole('gsadmin')} expression delegates the call to identity.hasRole method passing it the values of gsadmin.

public class UploadService implements Upload {
@Restrict("#{s:hasRole('gsadmin')}")
public void upload() {
// process file upload
}
}

similarly you can define access restriction to pages in pages.xml

<page view-id="/upload.xhtml" login-required="true">
<restrict>#{s:hasRole('gsadmin')}</restrict>
</page>

The restrict tag has the same effect as the @Restrict annotation except that it applies to that specific page. An AuthorizationException is thrown in case a member who does not belong to the gsadmin role accesses the upload page. You can customize exception handling in pages.xml to perform a redirection with a custom error message.

As the name implies, the login-required indicates that only logged-in members are granted access to that specific page. If not yet logged-in, a redirection to the login page is performed and all page parameters are captured. After authenticating the member, another redirection takes place to show the original paeg. This is done with the help of two tags defined in components.xml:

<event type="org.jboss.seam.notLoggedIn">
<action expression="#{redirect.captureCurrentView}" />
</event>
<event type="org.jboss.seam.postAuthenticate">
<action expression="#{redirect.returnToCapturedView}" />
</event>


Calling Identity in Code

You can even apply access restriction inline by directly calling methods of the Identity interface. The the following example calls Identity.isLoggedIn to make sure that only logged-in members are allowed to view the artwroks:

public class ArtworkServlet extends javax.servlet.http.HttpServlet {

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {

if(Identity.instance() == null || !Identity.instance().isLoggedIn()){
// show error message or redirect to home page
}

// code to view the artwork
}
}

I strongly recommend reading chapter 13 of Seam Documentation to learn more about the features and options provided to support Security in Seam.