Hi all, I've recently spent some time getting Roller to work in an SSO
environment with CAS and thought I'd share my config and code with the
group in case anybody else is trying to do this.  But before the config
files and code, a couple of notes:

First: if you want to get Roller working with CAS, I *highly* recommend
working through the Acegi tutorials, and get the minimal sample app
working with CAS in your environment first.  Also read over
the CAS docs and understand how to build and deploy CAS, including
configuring custom plugins.  Trust me, having some understanding of
what's going on will help a lot.

Second: what's required for Roller to work with CAS is almost
exclusively configuration in security.xml.  I added one class to
my Roller build, which is basically just a stub implementation of
a CasAuthoritiesPopulator, because one of the Acegi classes expects
to be injected with one of those.  My implementation doesn't actually
do anything though, except pass through the object returned by the
UserDetailsService.

Third: this config is *not* perfect.  It was done using some copy and
paste from the Acegi tutorial app and was adapted and tweaked until it
worked.  So there's probably left over cruft in there that's not even
necessary. Also I temporarily took out the "remember me" stuff to
simplify things until I got the base functionality working.  But while
not perfect, in my preliminary testing it does work as intended.

If anybody tries this and runs into problems, please post back to this
list and I'll help if I can.

Ok, finally, there should be two attachments. One is my security.xml.
You should be able to replace your security.xml with this, tweak a few
Urls and be close to good to go.  I think you'll also need to find the
cas client jar file and add it to your WEB-INF/lib.  Also, if you don't
have SSL enabled for your app server, you'll need to do that.  CAS
assumes the availability of SSL.

The other attachment is my users.RollerPopulator class, which is the
aforementioned stub implementation of CasAuthoritiesPopulator.  There
may be a way to avoid needing this, but I haven't dug into things deeply
enough yet to be sure.


HTH, YMMV, IANAL, etc.


Phil
<?xml version="1.0" encoding="UTF-8"?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  The ASF licenses this file to You
  under the Apache License, Version 2.0 (the "License"); you may not
  use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.  For additional information regarding
  copyright in this work, please see the NOTICE file in the top level
  directory of this distribution.
-->
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd";>

<beans>

    <!-- ======================== FILTER CHAIN ======================= -->
	<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
		<property name="filterInvocationDefinitionSource">
			<value>
				CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
				PATTERN_TYPE_APACHE_ANT
				/**=channelProcessingFilter,httpSessionContextIntegrationFilter,logoutFilter,casProcessingFilter,basicProcessingFilter,securityContextHolderAwareRequestFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
				</value>
		</property>
	</bean>


	<!-- ===================== HTTP CHANNEL REQUIREMENTS ==================== -->
	
	<!-- Enabled by default for CAS, as a CAS deployment uses HTTPS -->
	<bean id="channelProcessingFilter" class="org.acegisecurity.securechannel.ChannelProcessingFilter">
		<property name="channelDecisionManager">
			<ref local="channelDecisionManager"/>
		</property>
 		<property name="filterInvocationDefinitionSource">
			<value>
			    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
				\A/j_acegi_cas_security_check.*\Z=REQUIRES_SECURE_CHANNEL	
				\A.*\Z=REQUIRES_INSECURE_CHANNEL
			</value>
		</property>
	</bean>
	<bean id="channelDecisionManager" class="org.acegisecurity.securechannel.ChannelDecisionManagerImpl">
	    <property name="channelProcessors">
      		<list>
 	        	<ref local="secureChannelProcessor"/>
        		<ref local="insecureChannelProcessor"/>
     		</list>
	    </property>
	</bean>

	<bean id="secureChannelProcessor" class="org.acegisecurity.securechannel.SecureChannelProcessor"/>
	<bean id="insecureChannelProcessor" class="org.acegisecurity.securechannel.InsecureChannelProcessor"/>

	<!-- ======================== HTTP SESSION CONTEXT INTEGRATION ======================= -->  	
    <bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"/>
	
	
	<!-- ======================== LOGOUT ======================= -->

	<!-- note logout has little impact, due to CAS reauthentication functionality (it will cause a refresh of the authentication though) -->
   	<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
      	<constructor-arg value="/index.jsp"/> <!-- URL redirected to after logout -->
      	<constructor-arg>
         	<list>
              	<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
         	</list>
      	</constructor-arg>
 	</bean>

	<!-- ======================== CENTRAL AUTHENTICATION SERVICE (CAS) ======================= -->

	<bean id="casProcessingFilter" class="org.acegisecurity.ui.cas.CasProcessingFilter">
		<property name="authenticationManager">
			<ref local="authenticationManager"/>
		</property>
		<property name="authenticationFailureUrl">
			<value>/casfailed.jsp</value>
		</property>
		<property name="defaultTargetUrl">
			<value>/</value>
		</property>
		<property name="filterProcessesUrl">
			<value>/j_acegi_cas_security_check</value>
		</property>
	</bean>

	<!--  ***NOTE*** change the loginUrl below to the correct value for your environment -->
	<bean id="casProcessingFilterEntryPoint" class="org.acegisecurity.ui.cas.CasProcessingFilterEntryPoint">
		<property name="loginUrl"><value>https://localhost:8443/cas/login</value></property>
		<property name="serviceProperties"><ref local="serviceProperties"/></property>
	</bean>

	<bean id="casAuthenticationProvider" class="org.acegisecurity.providers.cas.CasAuthenticationProvider">
		<property name="casAuthoritiesPopulator">
			<ref local="casAuthoritiesPopulator"/>
		</property>
		<property name="casProxyDecider"><ref local="casProxyDecider"/></property>
		<property name="ticketValidator"><ref local="casProxyTicketValidator"/></property>
		<property name="statelessTicketCache"><ref local="statelessTicketCache"/></property>
		<property name="key"><value>my_password_for_this_auth_provider_only</value></property>
	</bean>

	<!--  ***NOTE*** change the Urls below to the correct value for your environment -->
	<bean id="casProxyTicketValidator" class="org.acegisecurity.providers.cas.ticketvalidator.CasProxyTicketValidator">
		<property name="casValidate"><value>https://localhost:8443/cas/proxyValidate</value></property>
		<property name="proxyCallbackUrl"><value>https://localhost:8443/Roller31/casProxy/receptor</value></property>
		<property name="serviceProperties"><ref local="serviceProperties"/></property>
        <!-- <property name="trustStore"><value>/some/path/to/your/lib/security/cacerts</value></property> -->
	</bean>

    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>
    
    <bean id="ticketCacheBackend" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
       <property name="cacheManager">
          <ref local="cacheManager"/>
       </property>
       <property name="cacheName">
          <value>ticketCache</value>
       </property>
    </bean>
   
	<bean id="statelessTicketCache" class="org.acegisecurity.providers.cas.cache.EhCacheBasedTicketCache">
      <property name="cache"><ref local="ticketCacheBackend"/></property>
	</bean>
	
	<!--  ***NOTE*** the class RollerPopulator doesn't really do anything, but pass through the
	      user object obtained from the UserDetailsService.  A more complicated environment might require
	      a class to do some lookups to another database or service to determine GrantedAuthoritys for the
	      user object.  That's not necessary here since the jdbcAuthenticationDao gives us everything we need -->
	<bean id="casAuthoritiesPopulator" class="users.RollerPopulator">
		<property name="userDetailsService">
			<ref local="jdbcAuthenticationDao" />
		</property>
	</bean>

	<bean id="casProxyDecider" class="org.acegisecurity.providers.cas.proxy.RejectProxyTickets">
	</bean>

	<!--  ***NOTE*** set the url below to the correct value for your environment -->
	<bean id="serviceProperties" class="org.acegisecurity.ui.cas.ServiceProperties">
		<property name="service"><value>https://localhost:8443/Roller31/j_acegi_cas_security_check</value></property>
		<property name="sendRenew"><value>false</value></property>
	</bean>

	<!-- ======================== BASIC PROCESSING ======================= -->

   <bean id="basicProcessingFilter" class="org.acegisecurity.ui.basicauth.BasicProcessingFilter">
      <property name="authenticationManager">
      	<ref local="authenticationManager"/>
      </property>
      <property name="authenticationEntryPoint">
      	<ref local="basicProcessingFilterEntryPoint"/>
      </property>
   </bean>

   <bean id="basicProcessingFilterEntryPoint" class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
      <property name="realmName"><value>Contacts Realm</value></property>
   </bean>
   

	<!-- ======================== SECURITY CONTEXT HOLDER AWARE ======================= -->
   
	<bean id="securityContextHolderAwareRequestFilter" class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter"/>


	<!-- ======================== ANONYMOUS PROCESSING ======================= -->
    <bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
        <property name="key" value="anonymous"/>
        <property name="userAttribute" value="anonymous,ROLE_ANONYMOUS"/>
    </bean>
   	
  	<bean id="anonymousAuthenticationProvider" class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
        <property name="key" value="anonymous"/>
    </bean>
   	
   	<!-- ======================== EXCEPTION TRANSLATION ======================= -->

	<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
		<property name="authenticationEntryPoint">
			<ref local="casProcessingFilterEntryPoint"/>
		</property>
		<property name="accessDeniedHandler">
			<bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
				<property name="errorPage" value="/accessDenied.jsp"/>
			</bean>
		</property>
	</bean>   

   	<!-- ======================== FILTER INVOCATION INTERCEPTOR ======================= -->

	<!-- Note the order that entries are placed against the objectDefinitionSource is critical.
	     The FilterSecurityInterceptor will work from the top of the list down to the FIRST pattern that matches the request URL.
	     Accordingly, you should place MOST SPECIFIC (ie a/b/c/d.*) expressions first, with LEAST SPECIFIC (ie a/.*) expressions last -->
	<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
    	<property name="authenticationManager"><ref local="authenticationManager"/></property>
    	<property name="accessDecisionManager"><ref local="accessDecisionManager"/></property>
 		<property name="objectDefinitionSource">
			<value>
                PATTERN_TYPE_APACHE_ANT
                /roller-ui/login-redirect.jsp=admin,editor
                /roller-ui/yourProfile**=admin,editor
                /roller-ui/createWebsite**=admin,editor
                /roller-ui/yourWebsites**=admin,editor
                /roller-ui/authoring/**=admin,editor
                /roller-ui/admin/**=admin
                /rewrite-status*=admin
			</value>
		</property>
	</bean>

<!--  				CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON (apparently we can't use this because the URLs have things
like yourProfile which has uppercase init  -->


	<!-- ======================== AUTHENTICATION ======================= -->
   	
   	<!-- Read users from database -->
    <bean id="jdbcAuthenticationDao" class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
        <property name="dataSource">
            <bean class="org.springframework.jndi.JndiObjectFactoryBean">
                <property name="jndiName" value="java:comp/env/jdbc/rollerdb"/>
            </bean>
        </property>
        <property name="usersByUsernameQuery">
            <value>SELECT username,passphrase,isenabled FROM rolleruser WHERE username = ?</value>
        </property>
        <property name="authoritiesByUsernameQuery">
            <value>SELECT username,rolename FROM userrole WHERE username = ?</value>
        </property>
	</bean>

	<!-- ======================== AUTHENTICATION ======================= -->

   	<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
      	<property name="providers">
         	<list>
		    	<ref local="casAuthenticationProvider"/>
		    	<ref local="anonymousAuthenticationProvider"/>
         	</list>
      	</property>
   	</bean>

	<!-- ===================== HTTP REQUEST SECURITY ==================== -->
	<bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
   		<property name="allowIfAllAbstainDecisions">
   			<value>false</value>
   		</property>
		<property name="decisionVoters">
		  <list>
		    <ref bean="roleVoter"/>
		  </list>
		</property>
	</bean>


	<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
		<property name="basenames">
			<list>
				<value>classpath:/org/acegisecurity/messages</value>
			</list>
		</property>
	</bean>


   <!-- ~~~~~~~~~~~~~~~~~~ AUTHORIZATION VOTER ~~~~~~~~~~~~~~~~ -->

   <!-- An access decision voter -->
   <bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter">
       	<property name="rolePrefix" value=""/>
   </bean>
   

   <!-- ~~~~~~~~~~~~~~~~~~ "BEFORE INVOCATION" AUTHORIZATION DEFINITIONS ~~~~~~~~~~~~~~~~ -->

   <!-- ACL permission masks used by this application -->
   <bean id="org.acegisecurity.acls.domain.BasePermission.ADMINISTRATION" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
      <property name="staticField"><value>org.acegisecurity.acls.domain.BasePermission.ADMINISTRATION</value></property>
   </bean>
   <bean id="org.acegisecurity.acls.domain.BasePermission.READ" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
      <property name="staticField"><value>org.acegisecurity.acls.domain.BasePermission.READ</value></property>
   </bean>
   <bean id="org.acegisecurity.acls.domain.BasePermission.DELETE" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
      <property name="staticField"><value>org.acegisecurity.acls.domain.BasePermission.DELETE</value></property>
   </bean>


   	<!-- ~~~~~~~~~~~~~~~~~~ LOGGER ~~~~~~~~~~~~~~~~ -->

	<!-- This bean is optional; it isn't used by any other bean as it only listens and logs -->
	<bean id="loggerListener" class="org.acegisecurity.event.authentication.LoggerListener"/>



	<!-- ################################### WORRY ABOUT THIS STUFF LATER #############################################-->
    <!-- ===================== REMEMBER ME ==================== -->
    <!-- 
    <bean id="rememberMeProcessingFilter" class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="rememberMeServices" ref="rememberMeServices"/>
    </bean>
 
    <bean id="rememberMeServices" class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices"> 
        <property name="userDetailsService" ref="jdbcAuthenticationDao"/>
        <property name="key" value="rollerlovesacegi"/> 
        <property name="parameter" value="rememberMe"/>
    </bean> 
  
    <bean id="rememberMeAuthenticationProvider" class="org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider"> 
        <property name="key" value="rollerlovesacegi"/>
    </bean>
-->

</beans>
package users;

import org.acegisecurity.AuthenticationException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.providers.cas.CasAuthoritiesPopulator;
import org.acegisecurity.userdetails.User;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.apache.log4j.Logger;

public class RollerPopulator implements CasAuthoritiesPopulator
{
        private static final Logger logger = Logger.getLogger( 
RollerPopulator.class );
        
        private UserDetailsService userDetailsService;
        
        public UserDetailsService getUserDetailsService()
        {
                return userDetailsService;
        }
        
        public void setUserDetailsService( UserDetailsService 
userDetailsService )
        {
                this.userDetailsService = userDetailsService;
        }
        
        public UserDetails getUserDetails( String userName )
                        throws AuthenticationException
        {
                logger.info( "RollerPopulator.getUserDetails() called!" );
                
                User userObject = (User)userDetailsService.loadUserByUsername( 
userName );
                
                // in  a more sophisticated implementation we would look up and 
insert
                // GrantedAuthoritys here.
                        
                return( userObject );
        }
}

Reply via email to