Adding Spring-Security to Openxava
Join the DZone community and get the full member experience.
Join For FreeIntroduction
The purpose of this article is to see how to integrate Spring Security on top of an Openxava standalone application.
Openxava builds portlets as well as standalone applications. When working with portlets deployed on a portal such as Liferay, they handle secured access by configuration. A standalone application lets you have to handle this functionality yourself.
This page will illustrate how to add spring security (authentication/authorisation) functionalities. The focus will be on the authorizations aspects since authorization is often enterprise-environment specific.
To demonstrate the integration, this article will use the minuteproject Lazuly showcase application generated for Openxava.
The first part identifies and explains the actions to undertake. The second part explains what minuteproject can do to fasten your development by generated a customed spring-security integration for you Openxava application.
Eventually a set of tests will ensure that the resulting application is correctly protected for URL direct access as well as content display. Furthermore, the integration is technologically non-intruisive. You do not have to change Openxava code for it to work.
Spring-Security Openxava integration
Technical Access
URL access
The url pattern is the following
http://servername:port/applicationcontext/xava/module.jsp?application=appName&module=moduleName
given like that it is hard to protect.
The module and application are passed as parameters.
The URL has to be revisited with
http://servername:port/applicationcontext/applicationPath/module
And the 'parameter' access are banned.
Enabling new URL access
Add a servlet
package net.sf.minuteproject.openxava.web.servlet; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ModuleHomeServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher; String [] uri = request.getRequestURI().split("/"); if (uri.length < 4) { dispatcher = request.getRequestDispatcher("/xava/homeMenu.jsp"); } else { dispatcher = request.getRequestDispatcher( "/xava/home.jsp?application=" + uri[1] + "&module=" + uri[3]); } dispatcher.forward(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
homeMenu.jsp is a page including a header with menu (to protect and whose menu link URL are correspond to the secured format) and a footer.
Add a servlet configuration
Servlet configuration snippet done in Openxava servlets.xml.
<servlet> <servlet> <servlet-name>moduleHome</servlet-name> <servlet-class>net.sf.minuteproject.openxava.web.servlet.ModuleHomeServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>moduleHome</servlet-name> <url-pattern>/MenuModules/*</url-pattern> </servlet-mapping> </servlet>
This snippet will be package in war web.xml at build time by OpenXava ant script.
Jsp access
Prohibit any Openxava jsp access except the one of the menu
To do that add an spring applicationContext-security.xml in you classpath (ex: Openxava src folder).
<b:beans xmlns="http://www.springframework.org/schema/security" ... <http realm="conference Realm"> <!-- default url --> <intercept-url pattern="/xava/homeMenu.jsp" access="ROLE_APPLICATION_USER"/> <intercept-url pattern="/xava/**/*.jsp" access="ROLE_NOT_PRESENT"/> ...
This means that all path after xava will be accessible (ex: css...) safe jsp expect one homeMenu.jsp is available to all registered user (ie having role ROLE_APPLICATION_USER cf attribution at authorisation part further).
Of course ensure that the role ROLE_NOT_PRESENT is really not present in your app.
Business Access
The idea is to give CRUD access on a entity base on role.
Define roles and UC
To be more explicit, I define 3 roles with their scope.
Administrator can administrate ROLE and COUNTRY entities
Application_user can manage all the other conference related tables safe the master data table mentionned above.
Reviewer can access to the statistic views but not the administration.
Both reviewer and Administrator can do what Application_user can do.
In applicationContext-security.xml the role can be mapped to specific URLs
<b:beans xmlns="http://www.springframework.org/schema/security" .... <http realm="conference Realm"> <!-- secured country --> <intercept-url pattern="/MenuModules/Country" access="ROLE_ADMINISTRATOR"/> <!-- secured role --> <intercept-url pattern="/MenuModules/Role" access="ROLE_ADMINISTRATOR"/> <!-- secured stat_mb_by_role --> <intercept-url pattern="/MenuModules/MemberPerRoleCountryAndConference" access="ROLE_REVIEWER"/> <!-- secured stat_mb_per_ctry_conf --> <intercept-url pattern="/MenuModules/MemberPerCountryAndConference" access="ROLE_REVIEWER"/> <intercept-url pattern="/MenuModules/**" access="ROLE_APPLICATION_USER"/>
Impact of the roles access on your model modal navigation
Be coherent
As I said before 'the CRUD access on a entity is role based' but the affectation mechanism has to reflect that.
OpenXava has annotation to create an entity from another one. It is then logical that we cannot create entity B from entity A, if we do not have CRUD rights on entity B.
The mechanism will consist in this case of affectation only with search functionalities.
In our scenario it means that a user with 'application_user' only can select a country but can not create any (no create or update icons available).
It is also true at the menu level, a user is entitled to see only its menu items corresponding to its profile.
Here the menu is done in JSP. To secure the access you can wrap to code to secure with taglib code coming with spring security or add a little taglib such as the following isUserInRole.tag located in web/WEB-INF/tags/common.
<%@ attribute name="role" required="true" %> <%! public boolean hasRole(javax.servlet.http.HttpServletRequest request, String role) { return request.isUserInRole(role) || request.isUserInRole(role.toUpperCase()) || request.isUserInRole("ROLE_"+role.toUpperCase()); } %> <% String [] roles = role.split(","); int length = roles.length; boolean isInRole = false; for (int i = 0; i < length;i++) { String role = (roles[i]); if(hasRole(request, role)) { isInRole = true; break; } } if(isInRole) { %> <jsp:doBody/> <% } %>
Wrap the code to protect here the administrator menu and each menu item
<mp:isUserInRole role="administrator"> <li class="topitem"> <a href="#" onclick="return false;"> Administration </a> <ul class="submenu"> <mp:isUserInRole role="administrator"> <li><a href="/conference/MenuModules/Country" >Country</a></li> </mp:isUserInRole> <mp:isUserInRole role="administrator"> <li><a href="/conference/MenuModules/Role" >Role</a></li> </mp:isUserInRole> </ul> </li> </mp:isUserInRole>
Authentication/Authorization
For the user to operate, he must be authenticated and authorised (moment where his role profile is loaded granting him with business access rights). I use an simple authentication and authorisation based a DB information.
Of course you are not supposed to use that in production ;) In applicationContext-security.xml add the following snippet.
<authentication-manager> <authentication-provider> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="SELECT username,password,active FROM user_authentication WHERE username = ?" authorities-by-username-query="SELECT username,role FROM user_authorisation WHERE username = ?" /> </authentication-provider> </authentication-manager> <b:bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <b:property name="jndiName"><b:value>java:comp/env/jdbc/conferenceDS</b:value></b:property> </b:bean>
Both authorisation and authentication queries have to be valid.
Here, they are done on top of views, which means that you have to implement 2 views: user_authentication and user_authorisation.
The datasource is the same as the one of the Openxava application
View gives you flexibility because if you have indirection level of granularity such as (user-role-permission), your view can associate user to role
Authentication flow
Eventually you need to handle an authentication flow composed of
- welcome page
- login page
- access denied page
- logout link
Add the following snippet.
<authentication-manager> <http realm="conference Realm"> <intercept-url pattern="/" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/index.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/hello.htm" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?login_error=1"/> <http-basic/> <logout logout-success-url="/index.jsp"/> <remember-me /> <access-denied-handler error-page="/accessDenied.jsp"/>
Login.jsp is strongly inspired by spring petclinic sample
<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %> <%@ page pageEncoding="UTF-8" %> <html> <head> <title>Login</title> </head> <body onload="document.f.j_username.focus();"> <h1>Login test</h1> <p>Locale is: <%= request.getLocale() %></p> <%-- this form-login-page form is also used as the form-error-page to ask for a login again. --%> <c:if test="${ not empty param.login_error}"> <font color="red"> Your login attempt was not successful, try again. Reason: <c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}"/>. </font> </c:if> <form name="f" action="<c:url value='j_spring_security_check'/>" method="POST"> <table> <tr><td>User:</td><td><input type='text' name='j_username' value='<c:if test="${ not empty param.login_error }"><c:out value="${SPRING_SECURITY_LAST_USERNAME}"/></c:if>'/></td></tr> <tr><td>Password:</td><td><input type='password' name='j_password'></td></tr> <tr><td><input type="checkbox" name="_spring_security_remember_me"></td><td>Don't ask for my password for two weeks</td></tr> <tr><td colspan='2'><input name="submit" type="submit"></td></tr> <tr><td colspan='2'><input name="reset" type="reset"></td></tr> </table> </form> </body> </html>
index.jsp
<html> <head> <title>Welcome to Conference</title> </head> <body> <h1>Welcome to Conference</h1> <a href="/conference/xava/homeMenu.jsp">login</a> </body> </html>
accessDenied.jsp
Access denied!
Not to forget a logout functionality here added on the menu
<span id="logout"><a href="../j_spring_security_logout">Logoff</a></span>
Spring security dependencies
Add spring security jars into web/WEB-INF/libspring-aop-3.0.4.RELEASE.jar
spring-asm-3.0.4.RELEASE.jar
spring-beans-3.0.4.RELEASE.jar
spring-context-3.0.4.RELEASE.jar
spring-core-3.0.4.RELEASE.jar
spring-expression-3.0.4.RELEASE.jar
spring-jdbc-3.0.4.RELEASE.jar
spring-security-acl-2.0.3.jar
spring-security-config-3.1.0.M1.jar
spring-security-core-2.0.3.jar
spring-security-core-3.1.0.M1.jar
spring-security-core-tiger-2.0.3.jar
spring-security-taglibs-2.0.3.jar
spring-security-web-3.1.0.M1.jar
spring-tx-3.0.4.RELEASE.jar
spring-web-3.0.4.RELEASE.jar
Spring security context
Spring security context had been mentioned at different level, here is the complete version<?xml version="1.0" encoding="UTF-8"?> <b:beans xmlns="http://www.springframework.org/schema/security" xmlns:b="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <authentication-manager> <authentication-provider> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="SELECT username,password,active FROM user_authentication WHERE username = ?" authorities-by-username-query="SELECT username,role FROM user_authorisation WHERE username = ?" /> </authentication-provider> </authentication-manager> <http realm="conference Realm"> <intercept-url pattern="/" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/index.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/hello.htm" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <!-- default url --> <intercept-url pattern="/xava/homeMenu.jsp" access="ROLE_APPLICATION_USER"/> <intercept-url pattern="/xava/**/*.jsp" access="ROLE_NOT_PRESENT"/> <!-- secured country --> <intercept-url pattern="/MenuModules/Country" access="ROLE_ADMINISTRATOR"/> <!-- secured role --> <intercept-url pattern="/MenuModules/Role" access="ROLE_ADMINISTRATOR"/> <!-- secured stat_mb_by_role --> <intercept-url pattern="/MenuModules/MemberPerRoleCountryAndConference" access="ROLE_REVIEWER"/> <!-- secured stat_mb_per_ctry_conf --> <intercept-url pattern="/MenuModules/MemberPerCountryAndConference" access="ROLE_REVIEWER"/> <intercept-url pattern="/MenuModules/**" access="ROLE_APPLICATION_USER"/> <form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?login_error=1"/> <http-basic/> <logout logout-success-url="/index.jsp"/> <remember-me /> <access-denied-handler error-page="/accessDenied.jsp"/> </http> <b:bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <b:property name="jndiName"><b:value>java:comp/env/jdbc/conferenceDS</b:value></b:property> </b:bean> </b:beans>Reference the context
Openxava listeners.xml is the place where you can set web.xml-snippets to be package in web.xml at Openxava build time
Add the following snippet
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext-security.xml </param-value> </context-param> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
The minuteproject way
Doing the integration can be time consuming. As you can notice there is some effort to have the code compliant for a webapp here Openxava to be bodyguard by Spring-Security. Meanwhile when dealing with data centric application, this knowledge can be crystalized to be instantly available.Because...there is an underlying concept that guides our choice and lead to best pratices.
It is one thing to execute them, it is another to state it. The question is how do we specify which entity to access and to which role. The idea is to express with simplicity the relationship between role or permission and action.
In our case the actions are:
- a full CRUD
- an affectation mechanism
The affection (linkage of an entity from another by search) is when to entities are linked but not all the role of the main entities are the same as the roles of the target. Otherwise affection goes with creation and update.
And the roles are:
- Administrator
- Application_user
- Reviewer
If you represent an entity-relationship diagram, you should see boxes and links. Boxes for entities and links for relationships. Give each role/permission a color.
Paint all the boxes that are full CRUD with the corresponding role color... Yes, you may paint the same box twice (resulting is color combination).
The result gives you the Color access spectrum of your DB.
Of course, we can further decline the gradient with other function (read-only, controller specific...) But the underlying idea is evident.
What Minuteproject allows you to do it by enriching your model with this color spectrum at the entity level or at the package level. This enables you to work with concept only closed to UC agnostic of technology implementations.
Minuteproject configuration snippet
<package name="admin" alias="Administration"> <security-color roles="administrator" /> </package> <package name="statistics"> <security-color roles="reviewer" /> </package>
Generation
Minuteproject configuration fullThe configuration is similar to lazuly show case enhanced with security aspects
<!DOCTYPE root> <generator-config> <configuration> <model name="conference" version="1.0" package-root="net.sf.mp.demo"> <data-model> <driver name="mysql" version="5.1.16" groupId="mysql" artifactId="mysql-connector-java"></driver> <dataSource> <driverClassName>org.gjt.mm.mysql.Driver</driverClassName> <url>jdbc:mysql://127.0.0.1:3306/conference</url> <username>root</username> <password>mysql</password> </dataSource> <!-- for Oracle and DB2 please set the schema <schema> </schema> --> <primaryKeyPolicy oneGlobal="true"> <primaryKeyPolicyPattern name="autoincrementPattern"></primaryKeyPolicyPattern> </primaryKeyPolicy> </data-model> <business-model> <generation-condition> <condition type="exclude" startsWith="user_"></condition> </generation-condition> <business-package default="conference"> <condition type="package" startsWith="STAT" result="statistics"></condition> <condition type="package" startsWith="COUNTRY" result="admin"></condition> <condition type="package" startsWith="ROLE" result="admin"></condition> </business-package> <enrichment> <conventions> <column-naming-convention type="apply-strip-column-name-suffix" pattern-to-strip="_ID" /> <reference-naming-convention type="apply-referenced-alias-when-no-ambiguity" is-to-plurialize="true" /> </conventions> <package name="admin" alias="Administration"> <security-color roles="administrator" /> </package> <package name="statistics"> <security-color roles="reviewer" /> </package> <entity name="COUNTRY" content-type="reference-data"> <semantic-reference> <sql-path path="NAME" /> </semantic-reference> </entity> <entity name="CONFERENCE_MEMBER"> <semantic-reference> <sql-path path="FIRST_NAME" /> <sql-path path="LAST_NAME" /> </semantic-reference> <field name="STATUS"> <property tag="checkconstraint" alias="conference_member_status"> <property name="PENDING" value="PENDING" /> <property name="ACTIVE" value="ACTIVE" /> </property> </field> <field name="EMAIL"> <stereotype stereotype="EMAIL" /> </field> </entity> <entity name="SPEAKER"> <field name="BIO"> <stereotype stereotype="HTML_TEXT" /> </field> <field name="PHOTO"> <stereotype stereotype="PHOTO" /> </field> <field name="WEB_SITE_URL"> <stereotype stereotype="WEBURL" /> </field> </entity> <entity name="PRESENTATION"> <field name="STATUS"> <property tag="checkconstraint" alias="presentation_status"> <property name="PROPOSAL" value="PROPOSAL" /> <property name="ACTIVE" value="ACTIVE" /> </property> </field> </entity> <entity name="SPONSOR"> <field name="STATUS"> <property tag="checkconstraint" alias="sponsor_status"> <property name="PENDING" value="PENDING" /> <property name="ACTIVE" value="ACTIVE" /> </property> </field> <field name="PRIVILEGE_TYPE"> <property tag="checkconstraint" alias="sponsor_privilege"> <property name="GOLDEN" value="Golden" /> <property name="SILVER" value="Silver" /> <property name="BRONZE" value="Bronze" /> </property> </field> </entity> <!-- views --> <entity name="stat_mb_per_ctry_conf" alias="MEMBER_PER_COUNTRY_AND_CONFERENCE"> <virtual-primary-key isRealPrimaryKey="true"> <property name="virtualPrimaryKey" value="ID" /> </virtual-primary-key> </entity> <entity name="stat_mb_by_role" alias="MEMBER_PER_ROLE_COUNTRY_AND_CONFERENCE"> <virtual-primary-key isRealPrimaryKey="true"> <property name="virtualPrimaryKey" value="id" /> </virtual-primary-key> <field name="stat_mb_per_ctry_conf_ID" linkToTargetEntity="stat_mb_per_ctry_conf" linkToTargetField="id"></field> </entity> </enrichment> </business-model> </model> <targets> <!-- openxava --> <target refname="OpenXava" name="OpenXava" fileName="mp-template-config-openxava-last-features.xml" outputdir-root="../../DEV/output/openxava-springsecurity/conference" templatedir-root="../../template/framework/openxava"> <property name="add-spring-security" value="true" /> </target> <target refname="CACHE-LIB" fileName="mp-template-config-CACHE-LIB.xml" templatedir-root="../../template/framework/cache"> </target> <target refname="springsecurity" name="springsecurity" fileName="mp-template-config-spring-security.xml" outputdir-root="../../DEV/output/openxava-springsecurity/conference" templatedir-root="../../template/framework/security/spring"> </target> <target refname="JPA2-LIB" fileName="mp-template-config-JPA2-LIB.xml" templatedir-root="../../template/framework/jpa"> </target> <target refname="BSLA-LIB" fileName="mp-template-config-bsla-LIB-features.xml" templatedir-root="../../template/framework/bsla"> </target> </targets> </configuration> </generator-config>The main points are
- exclude entities starting with user_ (i.e. the security entity used by spring configuration)
- add security access on package level
- package admin is accessible by role administrator only
- package statistics is accessible by role reviewer only
- default package (conference) is accessible by any application_user
- add spring-security track in the target
- add reference in openxava to spring-security
Set up Database
Implement the views
Here a very dummy implementation.
create view user_authentication as select email as username, first_name as password, '1' as active from conference_member ; create view user_authorisation as select cm.email as username, r.name as role from conference_member cm, role r, member_role mr where mr.role_id = r.id and mr.conference_member_id = cm.id union select cm.email as username, concat('ROLE_',r.name) as role from conference_member cm, role r, member_role mr where mr.role_id = r.id and mr.conference_member_id = cm.id ;As you can not there is a little redundancy in the user_authentication view, since sometimes the role administrator is refered sometimes role_administrator. This will be homogenized in next release.
Add some default value
Here a very dummy implementation.
INSERT INTO country (id, name, iso_name) VALUES (-1, 'France', 'FR'); INSERT INTO address (id, street1, street2, country_id) VALUES(-1, 'rue 1', 'rue 2', -1); INSERT INTO conference_member (id, conference_id, first_name, last_name, email, address_id, status ) VALUES (-1, -1, 'f', 'a', 'fa@test.com', -1, 'ACTIVE' ); INSERT INTO role (id, name) VALUES (-1, 'ADMINSTRATOR' ); INSERT INTO role (id, name) VALUES (-2, 'ROLE_APPLICATION_USER' ); INSERT INTO member_role (conference_member_id, role_id) VALUES (-1, -1); INSERT INTO member_role (conference_member_id, role_id) VALUES (-1, -2);So when user fa@test.com connects he will get the role Administrator which allows him to access the administrator menu and create a new role called 'REVIEWER'. He can also create a new conference member and associate with the role 'REVIEWER'.
Set up Application
Download the lazuly-openxava-springsecurity minuteproject configuration from google code minuteproject.
Copy file into /mywork/config
Execute
In /mywork/config: model-generation.cmd mp-config-LAZULY-Openxava-with-spring-security.xml
The generated code goes to /DEV/output/openxava-springsecurity/conference
Packaging
Here the packaging/deployment is a 2 steps exercices (unfortunately):
- there is no more the start-tomcat/stop-tomcat command in OX distribution
- spring dependencies are not included
- Check that Openxava 4.3 is available, and OX_HOME is set to Openxava 4.3
- from /DEV/output/openxava-springsecurity/conference run build-conference(.cmd/sh). This will trigger the build that is successful but not the deployment due to information before.
- Open the project generated by the build in Openxava workspace
- Add Spring security dependencies
- Start tomcat server (remark: The Datasource for the application is present in tomcat/config/context.xml)
- Deploy
- Enjoy
Welcome page
Default URL at context root of the application.
Login page
Any other direct called where the user is not authenticated will be intercepted and routed to this page
Contextual Menu
The user have access to the admin and conference part not the statistics.
The URLs have been modified. When the user tries to access the standard OX style URL he recieves an
access denied (ex: module.jsp)
Add role reviewer
Affect user with role reviewer and default (application_user)
Logoff
Login as Reviewer
Now the application is secured.
Conclusion
This article showed the configuration and manipulation to integrate spring security with openxava in a non-intrusive manner.It stressed a new concept 'DB color access spectrum' and how to densify the security information in minuteproject configuration.
DB color access spectrum is a concept which ask only to be extended:
- Ad-hoc functions, controllers
- Store procedures
It is not bound to a technology.
It is a step in easily defining fine grain access, its combination with profile based access and state based access (to do manually... for the moment ;)) could pave the way to intuitive and implicit workflows instead of heavy BPM solutions.
Published at DZone with permission of Florian Adler, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments