January 13, 2019
Business logic housed in Enterprise Java Beans (EJBs) is a leveraged position. The packaging allows various callers -- different RESTful web services, a remoted desktop client, or a web app -- to be built on reusable components. Changing business rules, logic, and data access is centralized. Because of the many consumers, it's important to handle security consistently. This article shows how to provide Role Based Access Control at the EJB level and a RESTful application.
Role Based Access Control (RBAC) is a standard EJB capability. With RBAC, you mark up EJB classes or methods with a @RolesAllowed annotation. When the user authenticates to the JavaEE app, say by a login form or the Basic Authentication of a RESTful web service, that identity is associated with one or more roles. Some applications might have only one role. In that case, the EJBs of the app will be either public (@PermitAll) or secured. By using different roles, the secured EJBs have a further refinement.
RBAC applies to the functions and not the data. For example, every authenticated user might be able to use a particular lookup function. RBAC permits an authenticated user to use the shared lookup. However, another mechanism is needed if there is a requirement to restrict the results to particular users. A user might not be able to lookup another users account.
This demo application is an account management API. An authenticated user allows access to operations that add, update, and query accounts. The user must be an admin to gain access to the "expunge account" function which will remove an account from the database. The app allows a normal user to execute a logical delete which is an update that sets a special "deleted" flag. The deleted flag can be used to filter for active accounts and lets the administrators revert any errant deletions made by a user.
The following class diagram shows a JAX-RS AccountsResource RESTful service calling a Stateless EJB "AccountServiceBean". AccountsResource and AccountServiceBean work with distinct Java objects. AccountServiceBean uses a special AccountEntity to emphasize the JPA management of that object. AccounstResource uses Account to communicate JSON values back to the caller. AccountsResource serializes the Account to and from an AccountEntity as an interface to the EJB.
AccountsResource is a set of HTTP-marked mappings that will invoke the functionality of the EJB (AccountServiceBean). AccountsResource accepts and returns Account objects which are serialized from JSON. AccountsResource forms AccountEntity object to be passed into the EJB. EJBAccessException is an ExceptionMapper to be discussed later. APIApplication is the JAX-RS registrar of resource, specifically AccountsResource.
This is the transport between the API user, say the test tool ReadyAPI or an external webapp, and the RESTful endpoint AccountsResouce.
package securitydemo.rs;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
public class Account implements Serializable {
private Long id;
@NotNull @Size(max=25)
private String firstName;
@NotNull @Size(max=25)
private String lastName;
@NotNull @Size(max=25)
private String gamertag;
private Boolean deleted = false;
public Account() {
}
public Account(Long id, String firstName, String lastName, String gamertag, Boolean deleted) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.gamertag = gamertag;
this.deleted = deleted;
}
public Long getId() {
return id;
}
public void setId(Long 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;
}
public String getGamertag() {
return gamertag;
}
public void setGamertag(String gamertag) {
this.gamertag = gamertag;
}
public Boolean getDeleted() {
return deleted;
}
public void setDeleted(Boolean deleted) {
this.deleted = deleted;
}
}
AccountResource has methods that map to HTTP functions. To see a discussion on the rationale for the mappings, check out this article on API design.
package securitydemo.rs;
import securitydemo.ejb.AccountEntity;
import securitydemo.ejb.AccountServiceBean;
import javax.ejb.EJB;
import javax.validation.Valid;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.stream.Collectors;
@Path("/accounts")
public class AccountsResource {
@EJB
private AccountServiceBean ejb;
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Account> findAccounts(@QueryParam("name") String name,
@DefaultValue("false") @QueryParam("showDeleted") Boolean deleted) {
return ejb.findAccountsByName("%" + name + "%", deleted)
.stream()
.map( ae -> new Account(ae.getAccountId(), ae.getFirstName(), ae.getLastName(), ae.getGamertag(), ae.getDeleted()))
.collect( Collectors.toList() );
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Account findAccount(@PathParam("id") Long id) {
return ejb
.findAccount(id)
.map(
entity -> new Account(entity.getAccountId(), entity.getFirstName(), entity.getLastName(), entity.getGamertag(), entity.getDeleted())
)
.orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Account addAccount(@Valid Account account) {
AccountEntity ae = new AccountEntity();
ae.setFirstName( account.getFirstName() );
ae.setLastName( account.getLastName() );
ae.setGamertag( account.getGamertag() );
AccountEntity fromDB = ejb.addAccount(ae);
return new Account(fromDB.getAccountId(), fromDB.getFirstName(), fromDB.getLastName(), fromDB.getGamertag(), false);
}
@PUT
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public void updateAccount(@PathParam("id") Long id, @Valid Account account) {
ejb
.findAccount(id)
.map(
entity -> {
entity.setFirstName( account.getFirstName() );
entity.setLastName( account.getLastName() );
entity.setGamertag( account.getGamertag() );
entity.setDeleted( account.getDeleted() );
ejb.updateAccount(entity);
return entity;
}
)
.orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
}
@DELETE
@Path("/{id}")
public void expungeAccount(@PathParam("id") Long id) {
ejb.removeAccount( id );
}
}
The code in AccountsResource delegates to the EJB AccountServiceBean and converts between Account and AccountEntity.
This class is a JAX-RS extension that will map an unhandled EJB, EJBAccessException. This is thrown when there is a security exception which can be thrown by the EJB. This will be referenced in the later section that talks about RBAC.
package securitydemo.rs;
import javax.ejb.EJBAccessException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
public class EJBAccessExceptionMapper implements ExceptionMapper<EJBAccessException> {
@Override
public Response toResponse(EJBAccessException e) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
This is a class needed by JAX-RS to locate the endpoint and mapper.
package securitydemo.rs;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
@ApplicationPath("/api")
public class APIApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> set = new HashSet<>();
set.add( AccountsResource.class );
set.add( EJBAccessExceptionMapper.class );
return set;
}
}
The web.xml file enables JavaEE security. In this security-constraint definition, I establish that any URLs leading off with /api in the path are secured and only accessible by the role "user". Alternatively, I can add roles to the auth-constraint such as "admin", but I'm going to use a one-role-per-resource convention. When I define the roles in the WildFly Admin Console, I'm going to give an admin user both "user" and "admin".
The login-config element defines a realm, ApplicationRealm, and says that the challenge credentials will be based on the Basic Authentication scheme.
WildFly comes with a pair of properties files that serve as a development authentication store. In this example, a hashed password is created with the add-user script. That password is stored with a username that is cross-referenced with a role. I created two users "mydemouser" and "mydemoadmin". mydemouser is a member of the user role. mydemoadmin is a member of both the user role and the admin role.
These .properties files are referenced in the various standlone.xml files. There is a definition that says the authentication is in the application-users.properties file and the authorization is in the application-roles.properties. The aforementioned add-user script modifies these files.
You will want a more robust credentials store such as a relational database or LDAP server. The flat files are used for demo purposes since they are already configured.
The backened logic and data access is implemented in a single, No-Interface View EJB. The EJB is stateless. A @RolesAllowed annotation marks the class as requiring the "user" role. This means that all of the methods in the class will require the user role unless overridden. One of the methods, removeAccount(), does override this setting and requires an admin role.
package securitydemo.ejb;
import javax.annotation.security.RolesAllowed;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.List;
import java.util.Optional;
@Stateless
@RolesAllowed("user")
public class AccountServiceBean {
@PersistenceContext
private EntityManager em;
public Optional<AccountEntity> findAccount(Long accountId) {
return Optional.ofNullable( em.find(AccountEntity.class, accountId) );
}
public AccountEntity addAccount(AccountEntity account) {
em.persist(account);
return account;
}
@RolesAllowed("admin")
public void removeAccount(Long accountId) { em.remove( em.find(AccountEntity.class, accountId) ); }
public void updateAccount(AccountEntity ae) {
AccountEntity fromDB = em.find( AccountEntity.class, ae.getAccountId() );
fromDB.setFirstName( ae.getFirstName() );
fromDB.setLastName(ae.getLastName());
fromDB.setGamertag(ae.getGamertag());
fromDB.setDeleted(ae.getDeleted());
}
public List<AccountEntity> findAccountsByName(String name, Boolean showDeleted) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<AccountEntity> cq = cb.createQuery(AccountEntity.class);
Root<AccountEntity> ae = cq.from(AccountEntity.class);
cq.select(ae);
cq.orderBy(cb.asc(ae.get(AccountEntity_.gamertag)));
Predicate names = cb.or(
cb.like(cb.upper(ae.get(AccountEntity_.firstName)), name.toUpperCase()),
cb.like(cb.upper(ae.get(AccountEntity_.lastName)), name.toUpperCase()),
cb.like(cb.upper(ae.get(AccountEntity_.gamertag)), name.toUpperCase())
);
if( showDeleted ) {
cq.where(names); // no clause involving deleted; both true and false returned
} else {
cq.where(
cb.and(
names,
cb.equal(ae.get(AccountEntity_.deleted), false))
);
}
TypedQuery<AccountEntity> q = em.createQuery(cq);
return q.getResultList();
}
}
In JavaEE, the security from the web layer (AccountsResource) and the business logic layer (AccountServiceBean) is integrated. There's no need to prompt for more credentials to validate the EJB call. Instead, the login-config used by the web front-end will establish a Principal that can be accessed by the EJB application and framework. Recall that the user with role=user (and not also admin) can call any method in the RESTful API. However, if a role=user attempts to call the HTTP DELETE expungeAccount() method, the EJB will throw an EJBAccessException.
The purpose of the EJBAccessExceptionMapper is then to map the EJB security violation into a 403 Forbidden HTTP response. This way, we don't need to explictly code a condition for each and every RESTful function. This boilerplate might be missed by a developer and an error case not handled correctly unless thoroughly tested.
The following screenshot shows an unauthorized user calling the EJB "expungeAcount" method without the Mapper, the general 500 error is returned with a stack trace in an HTML document.
With the ExceptionMapper configured, a standard 403 Forbidden is returned. This is more helpful to developers learning the API.
Like the Account class, AccountEntity has fields for name and gamertag. AccountEntity is a JPA Entity managed by the app server.
package securitydemo.ejb;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Version;
@Entity
public class AccountEntity {
@Id @GeneratedValue
private Long accountId;
private String firstName;
private String lastName;
/**
* gamertag is a unique account identifier that relates this record to the auth subsystem
*/
private String gamertag;
@Version
private Integer version;
/**
* a flag supporting a logical delete
*/
private Boolean deleted = false;
public AccountEntity() {
}
public AccountEntity(String firstName, String lastName, String gamertag, Integer version) {
this.firstName = firstName;
this.lastName = lastName;
this.gamertag = gamertag;
this.version = version;
}
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
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;
}
public String getGamertag() {
return gamertag;
}
public void setGamertag(String gamertag) {
this.gamertag = gamertag;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
public Boolean getDeleted() {
return deleted;
}
public void setDeleted(Boolean deleted) {
this.deleted = deleted;
}
@Override
public String toString() {
return "AccountEntity{" +
"accountId=" + accountId +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", gamertag='" + gamertag + '\'' +
", version=" + version +
", deleted=" + deleted +
'}';
}
}
persistence.xml is a file that enables JPA. It links the object definitions to a datastore. In this example, we're using the off-the-shelf ExampleDS set up in the standalone.xml file. This is an H2 in-memory database. While not suitable for production, it let's me add data to the API by saving the data (in memory) so that I can query, update, and delete it later.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
version="2.2">
<persistence-unit name="myPU">
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<properties>
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
The following is the Maven file used in the demo app. The JavaEE dependency is provided because the implementation JARs are found in the target WildFly app server. The jpamodelgen dependency is for the JPA Metamodel as decribed here.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.bekwam</groupId>
<artifactId>javaee-security-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.3.0.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</project>
Securing EJBs means securing the application for a range of entry points. For instance, several RESTful web services and a remoted desktop client can share the same EJB and be assured that all of the authorization rules will be applied. The article did not address data security however. But security is developed in layers and this first step helps to restrict access to the safest set of users possible.
By Carl Walker
President and Principal Consultant of Bekwam, Inc