Spring Python’s Security module is based on Acegi Security’s architecture. You can read Acegi’s detailed reference manual for a background on this module.
Note
Spring Security vs. Acegi Security
At the time this module was implemented, Spring Security was still Acegi Security. Links include reference documentation that was used at the time to implement this security module.
springpython.security.cherrypy3 package depends on CherryPy 3 being installed prior to using it. Other than that, there are no specific external libraries required by Spring Python’s security system, however the IoC configuration format that you’ll be using may need some, check IoC documentation for more details.
The first level of security involves verifying your credentials. Most systems today use some type of username/password check. To configure Spring Python, you will need to configure one or more AuthenticationProviders. All Authentication implementations are required to store an array of GrantedAuthority objects. These represent the authorities that have been granted to the principal. The GrantedAuthority objects are inserted into the Authentication object by the AuthenticationManager and are later read by AccessDecisionManagers when making authorization decisions. These are chained together inside an AuthenticationManager.
This AuthenticationProvider allows you to build a dictionary of user accounts, and is very handy for integration testing without resorting to complex configuration of 3rd party systems.
To configure this using a pythonic, decorator-based IoC container:
class SampleContainer(PythonConfig):
@Object
def inMemoryDaoAuthenticationProvider(self):
provider = DaoAuthenticationProvider()
provider.user_details_service = inMemoryUserDetailsService()
return provider
@Object
def inMemoryUserDetailsService(self):
user_details_service = InMemoryUserDetailsService()
user_details_service.user_dict = {
"vet1": ("password1", ["VET_ANY"], False),
"bdavis": ("password2", ["CUSTOMER_ANY"], False),
"jblack": ("password3", ["CUSTOMER_ANY"], False),
"disableduser": ("password4", ["VET_ANY"], True),
"emptyuser": ("", [], False) }
return user_details_service
XML configuration using XMLConfig:
<?xml version="1.0" encoding="UTF-8"?>
<objects xmlns="http://www.springframework.org/springpython/schema/objects/1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/springpython/schema/objects/1.1
http://springpython.webfactional.com/schema/context/spring-python-context-1.1.xsd">
<object id="inMemoryUserDetailsService" class="springpython.security.userdetails.InMemoryUserDetailsService">
<property name="user_dict">
<dict>
<entry>
<key><value>user1</value></key>
<value>
<tuple>
<value>password1</value>
<list><value>role1</value><value>blue</value></list>
<value>True</value>
</tuple>
</value>
</entry>
<entry>
<key><value>user2</value></key>
<value>
<tuple>
<value>password2</value>
<list><value>role1</value><value>orange</value></list>
<value>True</value>
</tuple>
</value>
</entry>
<entry>
<key><value>adminuser</value></key>
<value>
<tuple>
<value>password3</value>
<list><value>role1</value><value>admin</value></list>
<value>True</value>
</tuple>
</value>
</entry>
<entry>
<key><value>disableduser</value></key>
<value>
<tuple>
<value>password4</value>
<list><value>role1</value><value>blue</value></list>
<value>False</value>
</tuple>
</value>
</entry>
<entry>
<key><value>emptyuser</value></key>
<value>
<tuple>
<value/>
<list/>
<value>True</value>
</tuple>
</value>
</entry>
</dict>
</property>
</object>
<object id="inMemoryDaoAuthenticationProvider" class="springpython.security.providers.dao.DaoAuthenticationProvider">
<property name="user_details_service" ref="inMemoryUserDetailsService"/>
</object>
</objects>
This is the user map defined for one of the test cases. The first user, user1, has a password of password1, a list of granted authorities (“role1”, “blue”), and is enabled. The fourth user, “disableduser”, has a password and a list of granted authorities, but is NOT enabled. The last user has no password, which will cause authentication to fail.
Spring Python has an LdapAuthenticationProvider that is able to authenticate users against an LDAP server using either binding or password comparison. It will also search the LDAP server for groups in order to identify roles.
Note
Spring Python’s LDAP only works with CPython
Currently, Spring Python only provides LDAP support for CPython. There is on-going effort to extend support to Jython as well.
It is possible to the customize the query parameters, as well as inject an alternative version of authentication as well as role identification.
There are two ways to verify a password in ldap: binding to the server using the password, or fetching the password from ldap and comparing outside the server. Spring Python supports both. You can choose which mechanism by injecting either a BindAuthenticator or a PasswordComparisonAuthenticator into LdapAuthenticationProvider.
The following XML fragment demonstrates how to configure Spring Python’s LdapAuthenticationProvider using a BindAuthenticator combined with a DefaultLdapAuthoritiesPopulator:
<object id="context_source" class="springpython.security.providers.Ldap.DefaultSpringSecurityContextSource">
<property name="url" value="ldap://localhost:53389/dc=springframework,dc=org"/>
</object>
<object id="bindAuthenticator" class="springpython.security.providers.Ldap.BindAuthenticator">
<property name="context_source" ref="context_source"/>
<property name="user_dn_patterns" value="uid={0},ou=people"/>
</object>
<object id="authoritiesPopulator" class="springpython.security.providers.Ldap.DefaultLdapAuthoritiesPopulator">
<property name="context_source" ref="context_source"/>
<property name="group_search_filter" value="member={0}"/>
</object>
<object id="ldapAuthenticationProvider" class="springpython.security.providers.Ldap.LdapAuthenticationProvider">
<property name="ldap_authenticator" ref="bindAuthenticator"/>
<property name="ldap_authorities_populator" ref="authoritiesPopulator"/>
</object>
<object id="ldapAuthenticationManager" class="springpython.security.providers.AuthenticationManager">
<property name="auth_providers">
<list><ref object="ldapAuthenticationProvider"/></list>
</property>
</object>
The following shows the same configuration in pure Python, using PythonConfig:
class LdapContext(PythonConfig):
def __init__(self):
PythonConfig.__init__(self)
@Object
def context_source(self):
return DefaultSpringSecurityContext(url="ldap://localhost:53389/dc=springframework,dc=org")
@Object
def bind_authenticator(self):
return BindAuthenticator(self.context_source(), user_dn_patterns="uid={0},ou=people")
@Object
def authorities_populator(self):
return DefaultLdapAuthoritiesPopulator(self.context_source(), group_search_filter="member={0}")
@Object
def provider(self):
return LdapAuthenticationProvider(self.bind_authenticator(), self.authorities_populator())
@Object
def manager(self):
return AuthenticationManager(auth_providers=[self.provider()])
To use the password comparison mechanism with XMLConfig, substitute PasswordComparisonAuthenticator for BindAuthenticator as follows:
<object id="context_source" class="springpython.security.providers.Ldap.DefaultSpringSecurityContextSource">
<property name="url" value="ldap://localhost:53389/dc=springframework,dc=org"/>
</object>
<object id="passwordAuthenticator" class="springpython.security.providers.Ldap.PasswordComparisonAuthenticator">
<property name="context_source" ref="context_source"/>
<property name="user_dn_patterns" value="uid={0},ou=people"/>
</object>
<object id="authoritiesPopulator" class="springpython.security.providers.Ldap.DefaultLdapAuthoritiesPopulator">
<property name="context_source" ref="context_source"/>
<property name="group_search_filter" value="member={0}"/>
</object>
<object id="ldapAuthenticationProvider" class="springpython.security.providers.Ldap.LdapAuthenticationProvider">
<property name="ldap_authenticator" ref="bindAuthenticator"/>
<property name="ldap_authorities_populator" ref="authoritiesPopulator"/>
</object>
<object id="ldapAuthenticationManager" class="springpython.security.providers.AuthenticationManager">
<property name="auth_providers">
<list><ref object="ldapAuthenticationProvider"/></list>
</property>
</object>
The following block shows the same configuration using the pure Python container:
class LdapContext(PythonConfig):
def __init__(self):
PythonConfig.__init__(self)
@Object
def context_source(self):
return DefaultSpringSecurityContext(url="ldap://localhost:53389/dc=springframework,dc=org")
@Object
def password_authenticator(self):
return PasswordComparisonAuthenticator(self.context_source(), user_dn_patterns="uid={0},ou=people")
@Object
def authorities_populator(self):
return DefaultLdapAuthoritiesPopulator(self.context_source(), group_search_filter="member={0}")
@Object
def provider(self):
return LdapAuthenticationProvider(self.password_authenticator(), self.authorities_populator())
@Object
def manager(self):
return AuthenticationManager(auth_providers=[self.provider()])
By default, PasswordComparisonAuthenticator handles SHA encrypted passwords as well passwords stored in plain text. However, you can inject a custom PasswordEncoder to support alternative password encoding schemes.
So far, Spring Python has implemented a DaoAuthenticationProvider than can link with any database or use an in-memory user data structure, as well as an LdapAuthenticationProvider. Future releases should include:
An AuthenticationManager holds a list of one or more AuthenticationProvider’s, and will go through the list when attempting to authenticate. PetClinic configures it like this using PythonConfig:
class SampleContainer(PythonConfig):
@Object
def authenticationManager(self):
return AuthenticationManager(auth_providers = [self.authenticationProvider()])
XML-based configuration with :doc`XMLConfig <objects-xmlconfig>`:
<object id="authenticationManager" class="springpython.security.providers.AuthenticationManager">
<property name="auth_providers">
<list><ref object="authenticationProvider"/></list>
</property>
</object>
This AuthenticationManager has a list referencing one object already defined in the ApplicationContext, authenticationProvider. The authentication manager is supplied as an argument to the security interceptor, so it can perform checks as needed.
After successful authentication, a user is granted various roles. The next step of security is to determine if that user is authorized to conduct a given operation or access a particular web page. The AccessDecisionManager is called by the AbstractSecurityInterceptor and is responsible for making final access control decisions. The AccessDecisionManager interface contains two methods:
def decide(self, authentication, object, config)
def supports(self, attr)
As can be seen from the first method, the AccessDecisionManager is passed via method parameters all information that is likely to be of value in assessing an authorization decision. In particular, passing the secure object enables those arguments contained in the actual secure object invocation to be inspected. For example, let’s assume the secure object was a MethodInvocation. It would be easy to query the MethodInvocation for any Customer argument, and then implement some sort of security logic in the AccessDecisionManager to ensure the principal is permitted to operate on that customer. Implementations are expected to throw an AccessDeniedException if access is denied.
Whilst users can implement their own AccessDecisionManager to control all aspects of authorization, Spring Python Security includes several AccessDecisionManager implementations that are based on voting. Using this approach, a series of AccessDecisionVoter implementations are polled on an authorization decision. The AccessDecisionManager then decides whether or not to throw an AccessDeniedException based on its assessment of the votes.
The AccessDecisionVoter interface has two methods:
def supports(self, attr)
def vote(self, authentication, object, config)
Concrete implementations return an integer, with possible values being reflected in the AccessDecisionVoter static fields ACCESS_ABSTAIN, ACCESS_DENIED and ACCESS_GRANTED. A voting implementation will return ACCESS_ABSTAIN if it has no opinion on an authorization decision. If it does have an opinion, it must return either ACCESS_DENIED or ACCESS_GRANTED.
There are three concrete AccessDecisionManagers provided with Spring Python Security that tally the votes. The ConsensusBased implementation will grant or deny access based on the consensus of non-abstain votes. Properties are provided to control behavior in the event of an equality of votes or if all votes are abstain. The AffirmativeBased implementation will grant access if one or more ACCESS_GRANTED votes were received (ie a deny vote will be ignored, provided there was at least one grant vote). Like the ConsensusBased implementation, there is a parameter that controls the behavior if all voters abstain. The UnanimousBased provider expects unanimous ACCESS_GRANTED votes in order to grant access, ignoring abstains. It will deny access if there is any ACCESS_DENIED vote. Like the other implementations, there is a parameter that controls the behavior if all voters abstain.
It is possible to implement a custom AccessDecisionManager that tallies votes differently. For example, votes from a particular AccessDecisionVoter might receive additional weighting, whilst a deny vote from a particular voter may have a veto effect.
Python Security. The RoleVoter class will vote if any config attribute begins with ROLE_. It will vote to grant access if there is a GrantedAuthority which returns a string representation exactly equal to one or more config attributes starting with ROLE_. If there is no exact match of any config attribute starting with ROLE_, the RoleVoter will vote to deny access. If no config attribute begins with ROLE_, the voter will abstain. RoleVoter is case sensitive on comparisons as well as the ROLE_ prefix.
PetClinic has two RoleVoters in its Python-config based configuration:
class SampleContainer(PythonConfig):
@Object
def vetRoleVoter(self):
return RoleVoter(role_prefix="VET")
@Object
def customerRoleVoter(self):
return RoleVoter(role_prefix="CUSTOMER")
XML-based configuration with XMLConfig:
<object id="vetRoleVoter" class="springpython.security.vote.RoleVoter">
<property name="role_prefix"><value>VET</value></property>
</object>
<object id="customerRoleVoter" class="springpython.security.vote.RoleVoter">
<property name="role_prefix"><value>CUSTOMER</value></property>
</object>
The first one votes on VET authorities, and the second one votes on CUSTOMER authorities.
The other concrete AccessDecisionVoter is the LabelBasedAclVoter. It can be seen in the test cases. Maybe later it will be incorporated into a demo.
Petclinic has a custom AccessDecisionVoter, which votes on whether a user “owns” a record:
class SampleContainer(PythonConfig):
...
@Object
def ownerVoter(self):
return OwnerVoter(controller = self.controller())
XML-based configuration using XMLConfig:
<object id="ownerVoter" class="controller.OwnerVoter">
<property name="controller" ref="controller"/>
</object>
This class is wired in the PetClinic controller module as part of the sample, which demonstrates how easy it is to plugin your own custom security handler to this module.
PetClinic wires together these AccessDecisionVoters into an AccessDecisionManager:
class SampleContainer(PythonConfig):
@Object
def accessDecisionManager(self):
manager = AffirmativeBased()
manager.allow_if_all_abstain = False
manager.access_decision_voters = [self.vetRoleVoter(), self.customerRoleVoter(), self.ownerVoter()]
return manager
XML-based configuration using XMLConfig:
<object id="accessDecisionManager" class="springpython.security.vote.AffirmativeBased">
<property name="allow_if_all_abstain"><value>False</value></property>
<property name="access_decision_voters">
<list>
<ref object="vetRoleVoter"/>
<ref object="customerRoleVoter"/>
<ref object="ownerVoter"/>
</list>
</property>
</object>