Monday 7 October 2013

[JasperServer 5.2] Reading additional attribute from Apache LDAP with CAS (Timezone and Locale)

After struggling and googling with no result, finally we found a way to read additional attribute from LDAP and set it to JasperServer. What I am sharing here again is based on CAS Server 3.5.2 and CAS Client above 3.1.5 (Jasper bundled).

On CAS Server part:
1. Open file "/WEB-INF/view/jsp/protocol/2.0/casServiceValidationSuccess.jsp"
2. Below "<cas:user>...</cas:user> add tag to read attribute

<cas:attributes>
  <c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) >= 1}">
    <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"
                    varStatus="loopStatus" begin="0" end="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes)-1}"
                    step="1">
        <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
    </c:forEach>
  </c:if>
</cas:attributes>

3. Open file "/WEB-INF/deployerConfigContext.xml"
4. Find bean id "attributeRepository" property "resultAttributeMapping" and add attribute that you need here. In my case it will be timezone and locale

<property name="resultAttributeMapping">
  <map>
    <!-- Mapping beetween LDAP entry attributes (key) and Principal's (value) -->
    <entry value="locale" key="locale" />
    <entry value="timezone" key="timezone" />
    <entry value="mail" key="mail" />
    <entry value="userpassword" key="userpassword" />
    <entry value="userstatus" key="userstatus" />
    <entry value="name" key="cn" />
  </map>
</property>

5. Start the server once you are ready

On JasperServer part:
1. Open file "/WEB-INF/applicationContext-externalAuth-CAS-db-mt.xml"
2. Find bean id "proxyAuthenticationProcessingFilter" and replace it with your custom class with full package and class name
3. Create above mentioned class inside your project (again I'm using overlay method) which extend "com.jaspersoft.jasperserver.api.security.externalAuth.cas.JSCasProcessingFilter"
4. Override method "onSuccessfulAuthentication" and read the attribute from XML Response. Code below is for your reference only:

import org.jasig.cas.client.authentication.AttributePrincipal;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.security.Authentication;
import org.springframework.security.providers.cas.CasAuthenticationToken;
import com.jaspersoft.jasperserver.api.common.util.TimeZoneContextHolder;
import com.jaspersoft.jasperserver.api.metadata.user.service.impl.HttpOnlyResponseWrapper;
import com.jaspersoft.jasperserver.api.security.externalAuth.cas.JSCasProcessingFilter;
import com.jaspersoft.jasperserver.war.common.JasperServerConstImpl;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Locale;
import java.util.TimeZone;

public class CustomJSCasProcessingFilter extends JSCasProcessingFilter {
 
  private static final String TIMEZONE_ATTRIBUTE_KEY = "timezone";
  private static final String LOCALE_ATTRIBUTE_KEY = "locale";
  private int cookieAge;
 
  @Override
  protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
      Authentication authResult) throws IOException {
    super.onSuccessfulAuthentication(request, response, authResult);
   
    //Applying locale and timezone
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpSession session = httpRequest.getSession();
   
    //New Attribute to support ldap locale and timezone
    String strLdapLocale = null;
    String strLdapTimeZone = null;
   
    HttpOnlyResponseWrapper httpOnlyResponseWrapper = new HttpOnlyResponseWrapper(
        (HttpServletResponse) response);
   
    CasAuthenticationToken casAuthenticationToken = (CasAuthenticationToken) authResult;
   
    if(casAuthenticationToken.getAssertion().getPrincipal() instanceof AttributePrincipal) {
      AttributePrincipal attributePrincipal = (AttributePrincipal) casAuthenticationToken.getAssertion().getPrincipal();

      if(!attributePrincipal.getAttributes().containsKey(LOCALE_ATTRIBUTE_KEY)) {
        strLdapLocale = Locale.getDefault().toString();
      }
      strLdapLocale = (String) attributePrincipal.getAttributes().get(LOCALE_ATTRIBUTE_KEY);
     
      if(!attributePrincipal.getAttributes().containsKey(TIMEZONE_ATTRIBUTE_KEY)) {
        strLdapTimeZone = TimeZone.getDefault().toString();
      }
      strLdapTimeZone = (String) attributePrincipal.getAttributes().get(TIMEZONE_ATTRIBUTE_KEY);

    }

    Locale sessionLocale = (Locale) session.getAttribute(JasperServerConstImpl
        .getUserLocaleSessionAttr());
   
    if (strLdapLocale != null) {
      Locale ldapLocale = toLocale(strLdapLocale);
      if (sessionLocale == null || !sessionLocale.equals(ldapLocale)) {
        session.setAttribute(JasperServerConstImpl.getUserLocaleSessionAttr(), ldapLocale);
        Cookie cookie = new Cookie(JasperServerConstImpl.getUserLocaleSessionAttr(), strLdapLocale);
        cookie.setMaxAge(cookieAge);
        httpOnlyResponseWrapper.addCookie(cookie);
      }
    }

    if (sessionLocale != null) {
      LocaleContextHolder.setLocale(sessionLocale);
    }

    if (strLdapTimeZone != null) {
      String sessionTimezone = (String) session.getAttribute(JasperServerConstImpl
          .getUserTimezoneSessionAttr());
      if (sessionTimezone == null || !sessionTimezone.equals(strLdapTimeZone)) {
        session.setAttribute(JasperServerConstImpl.getUserTimezoneSessionAttr(), strLdapTimeZone);
        Cookie cookie = new Cookie(JasperServerConstImpl.getUserTimezoneSessionAttr(), strLdapTimeZone);
        cookie.setMaxAge(cookieAge);
        httpOnlyResponseWrapper.addCookie(cookie);
        TimeZoneContextHolder.setTimeZone(TimeZone.getTimeZone(strLdapTimeZone));
      }
    }
   
  }
 
  private Locale toLocale(String str) {
    if (str == null) {
      return null;
    }
    int len = str.length();
    if (len != 2 && len != 5 && len < 7) {
        throw new IllegalArgumentException("Invalid locale format: " + str);
    }
    char ch0 = str.charAt(0);
    char ch1 = str.charAt(1);
    if (ch0 < 'a' || ch0 > 'z' || ch1 < 'a' || ch1 > 'z') {
        throw new IllegalArgumentException("Invalid locale format: " + str);
    }
    if (len == 2) {
        return new Locale(str, "");
    } else {
        if (str.charAt(2) != '_') {
            throw new IllegalArgumentException("Invalid locale format: " + str);
        }
        char ch3 = str.charAt(3);
        if (ch3 == '_') {
            return new Locale(str.substring(0, 2), "", str.substring(4));
        }
        char ch4 = str.charAt(4);
        if (ch3 < 'A' || ch3 > 'Z' || ch4 < 'A' || ch4 > 'Z') {
            throw new IllegalArgumentException("Invalid locale format: " + str);
        }
        if (len == 5) {
            return new Locale(str.substring(0, 2), str.substring(3, 5));
        } else {
            if (str.charAt(5) != '_') {
                throw new IllegalArgumentException("Invalid locale format: " + str);
            }
            return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));
        }
    }
  }
 
  public int getCookieAge() {
    return cookieAge;
  }

  public void setCookieAge(int cookieAge) {
    this.cookieAge = cookieAge;
  }
 
}

5. Compile the application and package it using maven
6. Deploy the application and enjoy. Locale and Timezone now setup to your LDAP attribute
7. For additional information, if you want to show these attribute, just modify file "/WEB-INF/decorators/decorator.jsp". Below this tag:

 <li id="main_logOut" class="last"><a id="main_logOut_link"><spring:message code="menu.logout"/></a></li>






Add this tag:

<li id="main_test" class="leaf"><%=session.getAttribute(JasperServerConstImpl.getUserLocaleSessionAttr())%></li>
<li id="main_test" class="last"><%=session.getAttribute(JasperServerConstImpl.getUserTimezoneSessionAttr())%></li>

8. Also you need to import the constant by putting this on top of the same file:

<%@ page import="com.jaspersoft.jasperserver.war.common.JasperServerConstImpl" %>

Cheers,
Deddy

[JasperServer 5.2] Single Sign out from CAS

Single Sign Out give me a lot of headache for few days (LOL). Please take note that this is applicable when I use CAS Server 3.5.2 and CAS Client above 3.1.5 (Jasper bundled) and I haven't test any other version yet.

Please refer to this post on additional information:
http://jaspershare.blogspot.sg/2013/10/concurrent-user-control.html

Here are the steps using overlay method:
1. Open the file "/WEB-INF/applicationContext-externalAuth-CAS-db-mt.xml"
2. Add single logout filter to the bean


<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter" />

3. Very important! Find bean id "proxyAuthenticationProcessingFilter" and change property "invalidateSessionOnSuccessfulAuthentication" to false

<property name="invalidateSessionOnSuccessfulAuthentication" value="false"/>

4. Open the file "/WEB-INF/applicationContext-security-web.xml"
5. Find bean id "filterChainProxy" and for pattern "/**" add "logoutFilter,singleLogoutFilter" before "httpSessionContextIntegrationFilter". Please refer to another blog for what is "logoutFilter"
6. Very important! XML Validation for logout request will fail because of default jasper behaviour. Therefore, we need to add new pattern to file "/WEB-INF/classes/esapi/security.properties" below "DEFAULT"

#########################################################
# Logout Context
logoutRequest=AlphaUnderscore,Script,1000,true,logoutRequest-Logout_context


7. Next we need to register this by opening file "/WEB-INF/web.xml"
8. Add new listener

<!-- listener to response on single sign out -->
<listener>
    <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>

</listener>

9. Add new filter for Single Logout

<filter>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>

</filter>
<filter-mapping>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>


10. For me, I need to put configuration into one place, so adding property file to the classpath by modifying file "/WEB-INF/applicationContext-webapp.xml"
11. Find bean id "propertyConfigurer"
12. Add new value inside property "locations"

<value>classpath*:jasperserver-cas.properties</value>

13. I can then make use of ${cas.slo.expired.path} as stated above
14. Startup the application and enjoy. Logout from cas server will be processed accordingly.

Additional info:
1. if you want to remove logout link from jasperserver, open file "/WEB-INF/decorators/decorator.jsp" then comment out this section

<li id="main_logOut" class="last"><a id="main_logOut_link"><spring:message code="menu.logout"/></a></li>

2. If you want to show additional log for CAS, just add this line into file "/WEB-INF/log4j.properties"

log4j.logger.org.jasig.cas.client=debug

Cheers,
Deddy

[JasperServer 5.2] Concurrent User Control

Control concurrency inside jasperserver can be very tricky. However after few iteration, I've manage to get it working. Please take note that this is applicable when I use CAS Server 3.5.2 and CAS Client above 3.1.5 (Jasper bundled) and I haven't test any other version yet.

Here are the steps using overlay method:
1. Open the file "/WEB-INF/applicationContext-externalAuth-CAS-db-mt.xml"
2. Add concurrency handler bean and logout handler on top


<!-- Concurrency control -->
<bean id="concurrentSessionController" class="org.springframework.security.concurrent.ConcurrentSessionControllerImpl">   
    <property name="sessionRegistry" ref="sessionRegistry" />
    <property name="maximumSessions" value="1" />
    <property name="exceptionIfMaximumExceeded" value="false" />

</bean>
 

<bean id="concurrentSessionFilter" class="org.springframework.security.concurrent.ConcurrentSessionFilter">
  <property name="sessionRegistry" ref="sessionRegistry" />
  <property name="expiredUrl" value="${cas.slo.expired.path}" />
  <property name="logoutHandlers">
    <list>
      <ref bean = "logoutHandler"/>
    </list>
  </property>

</bean>
        

<bean id="logoutHandler" class="org.springframework.security.ui.logout.SecurityContextLogoutHandler" />

<bean id="logoutFilter" class="org.springframework.security.ui.logout.LogoutFilter">
  <constructor-arg value="${cas.slo.logout.url}"/>
    <constructor-arg>
      <ref bean = "logoutHandler"/>
    </constructor-arg>
    <property name="filterProcessesUrl" value="${cas.slo.expired.path}"/>

</bean>
<!-- End of Concurrency control -->

3. Find bean id "casAuthenticationManager" and add new property below

<!-- Concurrency control -->
<property name="sessionController" ref="concurrentSessionController" />

4. Open the file "/WEB-INF/applicationContext-security-web.xml"
5. Find bean id "filterChainProxy" and for pattern "/**" add "concurrentSessionFilter" before "filterInvocationInterceptor"
6. For me, I need to put configuration into one place, so adding property file to the classpath by modifying file "/WEB-INF/applicationContext-webapp.xml"
7. Find bean id "propertyConfigurer"
8. Add new value inside property "locations"

<value>classpath*:jasperserver-cas.properties</value>

9. I can then make use of "${cas.slo.expired.path}" as stated above
10. Startup the application and enjoy. User log into first browser then login again into second browser will kick user in the first browser.

Cheers,
Deddy

Tuesday 11 September 2012

[JasperServer] JasperServer SSO with EAR application

Hi All,

We have succesfully integrate jasperserver war file with our application inside Jboss application server. Provided you are using openAM(opensso) and spring security for your application, you are able to do that properly.

Only reminder that you need to make sure your override class also setProfileAttribute inside the user context. This is to make sure your security file can be applied properly.

Thanks.

Wednesday 25 April 2012

[JasperServer] Control Layout Types Value

For all that need control types value.
1 : Pop-up window
2 : Separate page
3 : Top of page
4 : In page
Thanks.

Friday 23 March 2012

[JasperServer] Homepage Role Based

Hi,
If you want user to be redirected to certain page right after his login, you can change this file:
\WEB-INF\jasperserver-servlet.xml
Find bean name "homePageByRole" and change it accordingly.
In my case, I have change it to:

<bean id="homePageByRole" class="java.util.ArrayList">
    <constructor-arg>
      <list>
        <value>ROLE_ADMINISTRATOR|redirect:/flow.html?_flowId=repositoryExplorerFlow</value>
        <value>ROLE_USER|redirect:/flow.html?_flowId=listReportsFlow</value>
      </list>
    </constructor-arg>
  </bean>

Hope it helps.

Wednesday 14 March 2012

[JasperServer] Limitation on Domain Query Text Area

Hi All,
This week I encountered the problem when I want to create query in Domain Designer with derived table using query. I found out that maximum character can be entered there was 5000 character. So I went around and read the configuration to change that to the value that I want since my query is big.
You can try to open this file:
\WEB-INF\classes\esapi\security.properties
Find this row:
createdQuerySql=Alpha,NoTag,5000,true,createdQuerySql-Run_Report_context
sqlDomainDesigner=ValidSQL,ValidSQLSelectStart,5000,true,SQL_Domain_Designer_context

Change value 5000 for this row to the value that you want and there you go.

In my case, I changed it to 20000 and it suite my requirement straight away.
Don't forget to delete all your temp and work folder (if exist) since this file is being cached.
Hope it helps.