Articles
Developer: Java
Implementing Row-Level Security in Java Applicationsby Lonneke Dikmans, Oracle Fusion Middleware Regional Director Learn how to enforce row-level security across your JEE application, from database to cache. Published July 2007 Security is a very important aspect of applications. Users need to be authenticated, and data needs to be protected against unauthorized access. Oracle provides an out-of-the-box solution for row-level security in the database: Oracle Label Security. By using the open-source Oracle TopLink as your ORM solution, you can also ensure that this row-level security is maintained in the cache and other parts of the application as well. This article shows how to make this work via a very simple sample application. Sample ApplicationThe sample application is a simple Web application that uses the HR Schema. Users can log in to it and view all locations that they are authorized to see. The schematic overview of the application shows four parts:
Oracle Label Security in a NutshellOracle provides different types of access control in the database. Let's explore each in detail. Discretionary Access Control (DAC). With DAC, object-level permissions are granted to database users. Access is granted or denied to the entire object. For example, the database user SKING can be granted permission to select from locations with the following statement: grant select to hr.locations to sking;SKING can now select all rows in the LOCATIONS table. Instead of using database users, you can define roles. Instead of granting the select permission to the database user, you grant the permission to the role and then grant the role to the database user. create role emp_role; grant connect to emp_role; grant select to hr.locations to emp_role; grant emp_role to SKING;Fine-Grained Access Control (FGAC). FGAC, also called row-level security, is a method where you can restrict access based on the content of the data. The Oracle Database solution for this type of requirement is called Virtual Private Database (VPD), which is a feature of Enterprise Edition. The database dynamically alters the query based on the security policy and session context. Apart from restricting the rows that are returned, you can also mask columns. Security policies can be created using stored procedures. Such policies restrict access by using the content of application data stored in Oracle Database or context variables, such as user name or IP address. Oracle Label Security (OLS), an Enterprise Edition option, is an implementation of VPD. Using OLS, administrators can create policies without writing PL/SQL. Access to data is mediated based on four factors:
Row LabelEach row of a table can be labeled based on its level of confidentiality. Every label contains three components:
In the sample application, you will use a policy named "access_locations" that determines who can see what location. You won't use compartments or groups. Levels are 'public' ('pub'), 'confidential' ('conf'), and 'sensitive' ('sens'). User LabelUsers can be granted label authorizations that determine the kind of access (read or write) they have to the rows that are labeled. When a label has been applied to a row, only users authorized for access to that label can see it or change it. Whenever a user connects to the database, the level of the user session is matched to that of the rows. PrivilegesOLS supports special privileges that allow authorized users to bypass certain parts of the policy. HR is the owner of the schema and has been granted FULL privileges in the sample application. This allows full read and write access to all data protected by the policy. Policy EnforcementWhen a user writes data, he or she can set the label for that row. The level of this label can be set to any level within the range specified by the administrator. When a user writes data without specifying its label, a row label is assigned automatically using the user's session label. The following user labels are defined in the sample application:
Combining DAC and FGAC. The figure below shows what happens when a user queries the LOCATIONS table: First, the database determines if the user has sufficient privileges based on the DAC. If the user is allowed to select data from hr.locations, the database checks if the user has any privileges that bypass the policy. If the user has no special privileges, access is mediated based on the policy. If the user has special privileges, mediation is bypassed and the query is executed based on the privileges. The sample application contains a test script (that runs in SQL*Plus) that shows what happens when different users log in and execute the same query. Select all locations with different users by opening SQL*Plus and typing @[path-to_file]\ols_test_security_policy.sqlYou will see that the same query has different results, depending on the user who connects to the database. Oracle TopLink SessionsAn Oracle TopLink session is the communication mechanism with the Oracle TopLink runtime. There are different types of sessions. A session consists of the following components:
Reading Data. To read data from a datasource, several interactions with the Oracle TopLink runtime occur:
/**
* finds all countries
* @return List<Country> or an empty list if none are found
*/
public List<Country> findAllCountries() {
Session session = getSessionFactory().acquireSession();
List<Country> results =
(List<Country>)session.executeQuery("findAllCountries", Country.class);
session.release();
results = (List<Country>)getSessionFactory().detach(results);
return results;
}
The query is defined in the class descriptor, as can be seen below:
<class-descriptor-query-manager>
<query-manager>
<descriptor-alias>Countries</descriptor-alias>
<query-list>
<query>
<name>findAllCountries</name>
<query-type>
oracle.toplink.queryframework.ReadAllQuery
</query-type>
<cache-usage>Check Cache by Primary Key</cache-usage>
<lock-mode>Do Not Acquire Locks</lock-mode>
<distinct-state>Uncomputed Distinct</distinct-state>
<in-memory-query-indirection-policy>
Throw Indirection Exception
..etc..
</query>
</query-list>
</query-manager>
session.executeQuery(...) will first try to fetch the countries from the shared cache. If the objects are not in the cache, they will be read from the database, using a connection that is defined in the sessions.xml.
Persisting Data. To write data to the datasource, similar steps need to be taken:
/**
* saves an entity
* @param entity that needs to persisted
* @return Object that is persisted.
*/
public Object persistEntity(Object entity) {
UnitOfWork uow = getSessionFactory().acquireUnitOfWork();
Object existingObject = uow.readObject(entity);
if (existingObject != null)
throw new RuntimeException("Entity already exists");
Object newInstance = uow.deepMergeClone(entity);
uow.commit();
return newInstance;
}
The UnitOfWork acts as a transactional unit and ensures that writes to the database are also written to the server cache. This way the correct updates are returned when a read fetches a changed country from the cache.
Connection Pools. Typically, when developing a JEE application, you use an external connection pool with Oracle TopLink. This means that you have one connection pool, for both reading and writing data. By default, TopLink uses internal connection pools. There are two pools in that case: one for writing and one for reading data. Oracle TopLink and VPDIn the previous section, we discussed regular Java EE application configuration with shared cache and connection pooling for performance purposes. For your sample application, you need a more sophisticated solution, however. First of all, you can't use a shared cache for all users, because not all users are authorized to see all data. Second, the database needs to have information about the user that is connected, to be able to execute mediation. Finally, you need to control the security level of new data that is inserted into the database. Thus, to use VPD with Oracle TopLink, configure isolated client sessions and use proxy authentication. Isolated Client Sessions. An isolated client session is a client session that provides its own session cache. Every table that uses VPD or OLS needs to be isolated. You can isolate either your entire project or just classes. References to shared classes from isolated classes are allowed; the other way around, obviously not. In the sample application, you reference countries from location, not the other way around. This means you can use isolation on the class level. There are two ways to isolate a class: declaratively using the workbench or in the Java code. In the workbench, you can easily isolate a class through the following:
<transactional-policy type="relational">
<descriptor-alias>Location</descriptor-alias>
<refresh-cache-policy/>
<caching-policy>
<cache-coordination>None</cache-coordination>
<cache-isolation>Isolated</cache-isolation>
</caching-policy>
<query-manager type="relational"/>
<locking-policy type="relational"/>
<primary-key-policy>
<primary-key-handles>
<column-handle>
<column-table-name>LOCATIONS</column-table-name>
<column-name>LOCATION_ID</column-name>
</column-handle>
</primary-key-handles>
</primary-key-policy>
</transactional-policy>
In Oracle JDeveloper, you can't configure isolated caching with the mapping editor. You can customize the session in two different ways: Use the preLogin method of the session SessionEventListener, or use the getSession() method from the SessionManager.
To use the preLogin method, you need to implement the SessionEventListener and register this listener with the session.
public class LocationEventListener extends SessionEventAdapter {
//other methods you want to override
/**
* We isolate the Location class here.
* @param event that is raised before the session is logged in.
*/
public void preLogin(SessionEvent event) {
logger.info("in prelogin event");
ClassDescriptor descriptor = event.getSession().getClassDescriptor(Location.class);
descriptor.setIsIsolated(true);
}
}
Add the class to the session in the mapping editor, as shown below.
To use the SessionManager.getSession(...) method, when you acquire the session without logging in, you can set the class descriptor to false and log in after that.
private Server getSession(){
Server server = (Server)sessionManager.getSession(xmlSessionConfigLoader, "hr", false);
ClassDescriptor descriptor = server.getClassDescriptor(Location.class);
descriptor.setIsIsolated(true);
server.login();
//.... rest of code....
}
ConnectionsSince OC4J 10.1.3, there are two types of datasources: managed and native. A managed datasource is an OC4J-provided implementation of the java.sql.DataSource interface that acts as a wrapper to a JDBC driver or datasource. It can participate in global transactions and make use of a connection pool. Native datasources implement the java.sql.DataSource interface and are provided by a JDBC driver vendor. In an Oracle TopLink project, you can define different server sessions. In the example below, you see an example of a managed datasource (jdbc/hrDS) and a native datasource. This is done in the sessions.xml:
<?xml version = '1.0' encoding = 'UTF-8'?>
<!DOCTYPE toplink-configuration PUBLIC "-//Oracle Corp.//DTD TopLink Sessions 9.0.4//EN" "sessions_9_0_4.dtd">
<toplink-configuration>
<session>
<!-- managed datasource -->
<name>hr</name>
<project-xml>META-INF/locationMap.xml</project-xml>
<session-type>
<server-session/>
</session-type>
<login>
<datasource>jdbc/hrDS</datasource>
<uses-native-sequencing>true</uses-native-sequencing>
</login>
<!-- etc -->
</session>
<session>
<!-- native datasource -->
<name>sking</name>
<project-xml>META-INF/locationMap.xml</project-xml>
<session-type>
<server-session/>
</session-type>
<login>
<driver-class>oracle.jdbc.OracleDriver</driver-class>
<connection-url>jdbc:oracle:thin:@localhost:1521:ORCL</connection-url>
<platform-class>oracle.toplink.platform.database.oracle.Oracle10Platform</platform-class>
<user-name>sking</user-name>
<encryption-class-name>oracle.toplink.internal.security.JCEEncryptor</encryption-class-name>
<encrypted-password>F21B2AE50E304BA0D81243DD794296A5</encrypted-password>
</login>
</session>
</toplink-configuration>
The session factory can be used to acquire the correct session by name. The listing shows how you define the session you want to use.
this.sessionFactory = new SessionFactory("META-INF/sessions.xml", "hr");
Session session = getSessionFactory().acquireSession();
When you use the managed datasource, you need to add the logic that appends the VPD-specific SQL to the queries yourself. But there is an easier way: using proxy authentication. In this sample, all the users are known to the database, so you can use proxy authentication. OC4J 10g (10.1.3.x) supports this with Oracle JDBC native datasources. This cannot be configured with the workbench or Oracle JDeveloper; you need Java code to accomplish this.
In this case, we will use proxy authentication based on the username only. (See How-To Configure and Use Proxy-authentication with OC4J 10g (10.1.3) Data Sources for a detailed explanation.) You need to take the following steps:
Exclusive Connections. Typically, when you use VPD and OLS, you use exclusive connections in your application. An exclusive connection is allocated by Oracle TopLink to a client session for reading of isolated data and writing for the duration of the session's lifecycle. It is obtained from the server session's write connection pool. Oracle TopLink still acquires a shared connection from the read connection pool for reading nonisolated data—in the sample application to fetch countries, for example. To use an exclusive connection, add the following code:
//......
ConnectionPolicy policy = new ConnectionPolicy(login);
policy.setShouldUseExclusiveConnection(server.getDefaultConnectionPolicy().
shouldUseExclusiveConnection());
//... rest of the code
What's Next?In this article, you have learned how to implement Oracle Label Security in a Java EE Web application via TopLink. In this sample, you used jazn to configure the security in the Web application and the database to define your security policies. This combination is very powerful: it uses security features of the Oracle Database as well as Java EE. In a real application, it would make sense to define both in Oracle Internet Directory, making users, roles, and privileges easier to maintain. This example only reads data from the database; no data is persisted. When data is persisted to the database and optimistic locking is used, you need to add a handler for the no-rows modified event. This exception can be thrown when there is a security violation as well as for locking exceptions. In case of a lock, the application can try to persist the object again. In case of a security violation, an exception needs to be thrown. Happy coding! Lonneke Dikmans, an Oracle Fusion Middleware Regional Director and Oracle ACE, is managing partner at Approach Alliance in The Netherlands. She is an architect, specializing in SOA and agile development. Lonneke has been using JDeveloper since 2000, and has experience designing, developing, and deploying Java applications. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||