Security is a very intriguing topic. You will find it everywhere and you need to implement it everywhere. Yes, you need. Unfortunately, one can easily forget it while implementing the last mile.
The Last Mile
In a multi-tier application it is a common practice to use connection pools between the business layer and the database layer. Connection pools are quite useful to speed database connection creation and to split the load. Another very common practice is to use a specific, often called technical, user to connect to the database. This user has authentication and authorization rules that apply to all application users. Imagine you've put every effort to define roles for different types of users that use your application. These roles are necessary to differentiate between normal users, premium users, and administrators (I bet you will find or already have more roles in your application). While these user roles are pretty well used within your application, once the flow of execution enters the database everything is gone. Each and every user just has one role and is the same database user.
Issues? What Issues?
As long as things go well, this is not a real issue. However, things do not go well all the time. Once your application becomes famous performance decreases in certain situations or, more importantly, current and upcoming regulations and laws require that your application must be able to apply different security measures on a per user role basis at every stage of your application. If you only have a bunch of users with the same name and role you are not able to find the application usage profile that causes the performance issue, or which user has accessed data that he/she is not allowed to.
Another thread to your role concept is that databases tend to be used by different applications and tools. These tools can be developer tools like SQL*Plus, SQL Developer, etc. or end user applications like BI Publisher, Oracle Forms and so on. These tools have no idea of your applications role concept and access the database the way they think is appropriate. A big oversight for your perfect role model and a big nightmare for your Chief Security Officer.
Speaking of the CSO, brings up another issue: Password management. Once your technical user account is compromised, every user is able to do things that he/she is not expected to do from the design of your application.
Counter Measures
In the Oracle world a common counter measure is to use Virtual Private Database (VPD). This restricts the values a database user can see to the allowed minimum. However, it doesn't help in regard of a connection pool user, because this one is still not the real user.
Oracle Proxy Authentication
Another feature of the Oracle database is Proxy Authentication. First introduced with version 9i it is a quite useful feature for nearly every situation. The main idea behind Proxy Authentication is, to create a crippled database user who has only connect rights. Even if this user is compromised the risks are well understood and fairly limited. This user can be used in every situation in which you need to connect to the database, no matter which tool or application (see above) you use.The proxy user is perfect for multi-tier connection pools.
CREATE USER app_user IDENTIFIED BY abcd1234;
GRANT CREATE SESSION TO app_user;
But what if you need to access real data? Well, this is the primary use case, isn't it? Now is the time to bring the application's role concept into play. You define database roles that define the grants for your identified user groups. Once you have these groups you grant access through the proxy user with the application role to the specific user.
CREATE ROLE app_role_a;
GRANT app_role_a TO scott;
ALTER USER scott GRANT CONNECT THROUGH app_user WITH ROLE app_role_a;
Now, hr has permission to connect to the database through the proxy user. Through the role you can restrict the hr's rights the are needed for the application only. If hr connects to the database directly all assigned role and permissions apply.
Testing the Setup
To test the setup you can use SQL*Plus and connect to your database:
$ sqlplus app_user[hr]/abcd1234
Java Persistence API
The Java Persistence API (JPA) is a fairly easy means to build applications that retrieve data from the database and put it into Java objects. You use plain old Java objects (POJOs) and mixin some Java annotations that define how the attributes of the object are used for storing data from the database into the Java object. Here is a sample for objects from the HR sample schema EMPLOYEES table. When using Java annotations you only specify what can not be deduced from the code. If your Java class name is Employee but the table name is EMPLOYEES, you need to specify the table name, otherwise it will fail.
package demo.proxy.ejb;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@NamedQueries({
@NamedQuery(name = "Employee.findAll", query = "select o from Employee o")
})
@Table(name = "EMPLOYEES")
public class Employee implements Serializable {
@Column(name="COMMISSION_PCT")
private Double commissionPct;
@Column(name="DEPARTMENT_ID")
private Long departmentId;
@Column(nullable = false, unique = true, length = 25)
private String email;
@Id
@Column(name="EMPLOYEE_ID", nullable = false)
private Long employeeId;
@Column(name="FIRST_NAME", length = 20)
private String firstName;
@Column(name="HIRE_DATE", nullable = false)
private Timestamp hireDate;
@Column(name="JOB_ID", nullable = false, length = 10)
private String jobId;
@Column(name="LAST_NAME", nullable = false, length = 25)
private String lastName;
@Column(name="PHONE_NUMBER", length = 20)
private String phoneNumber;
private Double salary;
@ManyToOne
@JoinColumn(name = "MANAGER_ID")
private Employee employee;
@OneToMany(mappedBy = "employee")
private List employeeList;
public Employee() {
}
public Employee(Double commissionPct, Long departmentId, String email,
Long employeeId, String firstName, Timestamp hireDate,
String jobId, String lastName, Employee employee,
String phoneNumber, Double salary) {
this.commissionPct = commissionPct;
this.departmentId = departmentId;
this.email = email;
this.employeeId = employeeId;
this.firstName = firstName;
this.hireDate = hireDate;
this.jobId = jobId;
this.lastName = lastName;
this.employee = employee;
this.phoneNumber = phoneNumber;
this.salary = salary;
}
public Double getCommissionPct() {
return commissionPct;
}
public void setCommissionPct(Double commissionPct) {
this.commissionPct = commissionPct;
}
public Long getDepartmentId() {
return departmentId;
}
public void setDepartmentId(Long departmentId) {
this.departmentId = departmentId;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public Timestamp getHireDate() {
return hireDate;
}
public void setHireDate(Timestamp hireDate) {
this.hireDate = hireDate;
}
public String getJobId() {
return jobId;
}
public void setJobId(String jobId) {
this.jobId = jobId;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public List getEmployeeList() {
return employeeList;
}
public void setEmployeeList(List employeeList) {
this.employeeList = employeeList;
}
public Employee addEmployee(Employee employee) {
getEmployeeList().add(employee);
employee.setEmployee(this);
return employee;
}
public Employee removeEmployee(Employee employee) {
getEmployeeList().remove(employee);
employee.setEmployee(null);
return employee;
}
}
JPA could be used in standalone applications and Java EE containers. In both worlds you normally create a Facade to retrieve or store the values of the Entities to or from the database. The Facade does this via an EntityManager which will be injected by the Java EE container. Here is sample Facade Session Bean for a Java EE container.
package demo.proxy.ejb;
import java.util.HashMap;
import java.util.List;
import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import oracle.jdbc.driver.OracleConnection;
import org.eclipse.persistence.config.EntityManagerProperties;
import org.eclipse.persistence.internal.jpa.EntityManagerImpl;
@Stateless(name = "DataFacade", mappedName = "ProxyUser-TestEJB-DataFacade")
@Remote
@Local
public class DataFacadeBean implements DataFacade, DataFacadeLocal {
@PersistenceContext(unitName = "TestEJB")
private EntityManager em;
private String username;
public Object queryByRange(String jpqlStmt, int firstResult,
int maxResults) {
// setSessionUser();
Query query = em.createQuery(jpqlStmt);
if (firstResult 0) {
query = query.setFirstResult(firstResult);
}
if (maxResults 0) {
query = query.setMaxResults(maxResults);
}
return query.getResultList();
}
public Employee persistEmployee(Employee employee) {
// setSessionUser();
em.persist(employee);
return employee;
}
public Employee mergeEmployee(Employee employee) {
// setSessionUser();
return em.merge(employee);
}
public void removeEmployee(Employee employee) {
// setSessionUser();
employee = em.find(Employee.class, employee.getEmployeeId());
em.remove(employee);
}
/** select o from Employee o */
public List getEmployeeFindAll() {
Query q = em.createNamedQuery("Employee.findAll");
return q.getResultList();
}
Putting Both Together
To use Proxy Authentication with JPA and within a Java EE container you have to take care of the additional requirements:
Use an OCI JDBC driver
Provide the user name that connects through the proxy user
Use an OCI JDBC driver
To use the OCI JDBC driver you need to set up your JDBC data source file to use the correct JDBC URL.
hr
jdbc:oracle:oci8:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=XE)))
oracle.jdbc.OracleDriver
user
app_user
62C32F70E98297522AD97E15439FAC0E
SQL SELECT 1 FROM DUAL
jdbc/hrDS
Application
Additionally you need to make sure that the version of the shared libraries of the OCI driver match the version of the JDBC driver in your Java EE container or Java application and are within your PATH (on Windows) or LD_LIBRARY_PATH (on most Unix-based systems). Installing the Oracle Database Instance Client software works perfectly.
Provide the user name that connects through the proxy user
This part needs some modification of your application software and session facade.
Session Facade Changes
In the Session Facade we must ensure that every call that goes through the EntityManager must be prepared correctly and uniquely assigned to this session. The second is really important, as the EntityManager works with a connection pool and can not guarantee that we set the proxy user on the connection that will be used for the database activities.
To avoid changing every method call of the Session Facade we provide a method to set the username of the user that connects through the proxy user. This method needs to be called by the Facade client bfore doing anything else.
public void setUsername(String name) {
username = name;
}
Next we provide a means to instruct the TopLink EntityManager Delegate to use Oracle Proxy Authentication. (I love small helper methods to hide the nitty-gritty details and avoid repeating myself.)
private void setSessionUser() {
setSessionUser(username);
}
private void setSessionUser(String user) {
if (user != null && !user.isEmpty()) {
EntityManagerImpl emDelegate = ((EntityManagerImpl)em.getDelegate());
emDelegate.setProperty(EntityManagerProperties.ORACLE_PROXY_TYPE,
OracleConnection.PROXYTYPE_USER_NAME);
emDelegate.setProperty(OracleConnection.PROXY_USER_NAME, user);
emDelegate.setProperty(EntityManagerProperties.EXCLUSIVE_CONNECTION_MODE,
"Always");
}
}
The final step is use the EJB 3.0 AroundInvoke interceptor. This interceptor will be called around every method invocation. We therefore check whether the Facade methods will be called or not. If so, we set the user for proxy authentication and the normal method flow continues.
@AroundInvoke
public Object proxyInterceptor(InvocationContext invocationCtx) throws Exception {
if (invocationCtx.getTarget() instanceof DataFacadeBean) {
setSessionUser();
}
return invocationCtx.proceed();
}
Benefits
Using Oracle Proxy Authentification has a number of additional benefits appart from implementing the role model of your application:
Fine grained access control for temporary users of the account, without compromising the original password.
Enabling database auditing and logging.
Better identification of performance bottlenecks.
References
Effective Oracle Database 10g Security by Design, David Knox
TopLink Developer's Guide, Chapter 98