springspring-securityroleswaffle

Spring Security Role Hierarchy issues


I am trying to enable role hierarchy voting in Spring Security when authenticating using Waffle NTML but having some unknown issues in that the inherited roles do not appear as authorities on the principal as expected preventing hasRole expressions in both the intercept urls and using the authorize jsp taglibs.

I have been integrating waffle based on the following guide: https://github.com/dblock/waffle/blob/master/Docs/spring/SpringSecuritySingleSignOnFilter.md

This works within the application as expected on its own using the standard RoleVoter but the problem starts when I try to customise it to use the RoleHierarchyVoter which I have also tested on its own (using an LDAP Authentication Provider) and the role hierarchies work exactly as expected.

The config for the combined Waffle and RoleHierarchyVoter approach is as follows:

Waffle Specfic Config

<!-- windows authentication provider -->
<bean id="waffleWindowsAuthProvider" class="waffle.windows.auth.impl.WindowsAuthProviderImpl" />

<!-- collection of security filters -->
<bean id="negotiateSecurityFilterProvider" class="waffle.servlet.spi.NegotiateSecurityFilterProvider">
    <constructor-arg ref="waffleWindowsAuthProvider" />
</bean>

<bean id="basicSecurityFilterProvider" class="waffle.servlet.spi.BasicSecurityFilterProvider">
    <constructor-arg ref="waffleWindowsAuthProvider" />
</bean>

<bean id="waffleSecurityFilterProviderCollection" class="waffle.servlet.spi.SecurityFilterProviderCollection">
    <constructor-arg>
        <list>
            <ref bean="negotiateSecurityFilterProvider" />              
            <ref bean="basicSecurityFilterProvider" />              
        </list>
    </constructor-arg>
</bean>

<bean id="negotiateSecurityFilterEntryPoint" class="waffle.spring.NegotiateSecurityFilterEntryPoint">
    <property name="Provider" ref="waffleSecurityFilterProviderCollection" />
</bean>

<!-- spring security filter -->
<bean id="waffleNegotiateSecurityFilter" class="waffle.spring.NegotiateSecurityFilter">
    <property name="Provider" ref="waffleSecurityFilterProviderCollection" />
    <property name="AllowGuestLogin" value="false" />
    <property name="PrincipalFormat" value="fqn" />
    <property name="RoleFormat" value="fqn" />
    <property name="GrantedAuthorityFactory" ref="simpleGrantedAuthorityFactory" />
    <!-- set the default granted authority to null as we don't need to assign a default role of ROLE_USER -->
    <property name="defaultGrantedAuthority"><null/></property>

</bean>

<!-- custom granted authority factory so the roles created are based on the name rather than the fqn-->
<bean id="simpleGrantedAuthorityFactory" class="xx.yy.zz.SimpleGrantedAuthorityFactory">
    <constructor-arg name="prefix" value="ROLE_"/>
    <constructor-arg name="convertToUpperCase" value="true"/>
</bean>

Familiar Spring Security Config

<!-- declare the entry point ref as the waffle defined entry point -->
<sec:http use-expressions="true"
          disable-url-rewriting="true"
          access-decision-manager-ref="accessDecisionManager"
          entry-point-ref="negotiateSecurityFilterEntryPoint" >

    <sec:intercept-url pattern="/**" access="isAuthenticated()" requires-channel="any"/>

    .
    . access denied handlers, concurrency control, port mappings etc
    .

    <sec:custom-filter ref="waffleNegotiateSecurityFilter" position="BASIC_AUTH_FILTER" />

</sec:http>

<!-- spring authentication provider -->
<sec:authentication-manager alias="authenticationProvider" />


<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
    <property name="decisionVoters">
        <list>
            <ref bean="roleHierarchyVoter" />
            <bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
                <property name="expressionHandler">
                    <bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">
                        <property name="roleHierarchy" ref="roleHierarchy"/>
                    </bean>
                </property>
            </bean>
        </list>
    </property>
</bean>

<bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
    <property name="hierarchy">
        <value>
            ROLE_TEST_1 > ROLE_TEST_2
            ROLE_TEST_2 > ROLE_TEST_3
            ROLE_TEST_3 > ROLE_TEST_4
        </value>
    </property>
</bean>

<bean id="roleHierarchyVoter"
            class="org.springframework.security.access.vote.RoleHierarchyVoter">
    <constructor-arg ref="roleHierarchy"/>
</bean>

Solution

  • Managed to fix my issues which was down to an omission in my http namespace configuration which I found from hours of debugging the spring security source.

    The issue was how the DefaultWebSecurityExpressionHandler was created. In the snipped above it had created it as inner bean inside the bean definition of the accessDecisionManager:

    <bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
        <property name="expressionHandler">
            <bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">
                <property name="roleHierarchy" ref="roleHierarchy"/>
            </bean>
        </property> 
    </bean>
    

    With this the role heirachies are used to determine whether access should be granted when processing rules defined as intercept urls such as:

    <sec:intercept-url pattern="/**" access="isAuthenticated()" requires-channel="any"/>
    

    But if you want to check authorisation using the JSP Authorize taglib as below (this is in freemarker) it will not work as the roleHeirachies do not get taken into account:

    <@security.authorize access="hasRole('ROLE_TEST_1)">
        <p>You have role 1</p>
    </@security.authorize>
    
    <@security.authorize access="hasRole('ROLE_TEST_4')">
        <p>You have role 4</p>
    </@security.authorize>
    

    This is because the DefaultWebSecurityExpressionHandler created as an inner bean is only used within the access decision manager but for taglib expressions a NEW default bean will be created (which doesn't use the RoleHierarchy) unless an security http namespace expression-handler is defined.

    So, to resolve my issues I created the bean DefaultWebSecurityExpressionHandler and referenced it within my WebExpressionVoter bean definition and also used it as the expression handler as follows:

    <sec:http ... >
    
        .
        . access denied handlers, concurrency control, port mappings etc
        .
    
        <sec:expression-handler ref="defaultWebSecurityExpressionHandler" />
    
    </sec:http>
    
    <bean id="defaultWebSecurityExpressionHandler"
          class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">
          <property name="roleHierarchy" ref="roleHierarchy"/>
    </bean>
    
    <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
        <property name="decisionVoters">
            <list>
                <ref bean="roleHierarchyVoter" />
                <bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
                    <property name="expressionHandler" ref="defaultWebSecurityExpressionHandler"/>
                </bean>
            </list>
        </property>
    </bean>
    

    Making these changes ensures the roleHeirarchies are taken into account for both Web Security Expressions defined as intercept URLs via the http namespace and also expressions using the JSP Authorize taglib.