Authentication¶
The default policy¶
By default c2cgeoportal
applications use an auth ticket authentication
policy (AuthTktAuthenticationPolicy
). With this policy the user name is
obtained from the “auth ticket” cookie set in the request.
The policy is created, and added to the application’s configuration, in the
application’s main __init__.py
file.
Using another policy¶
When using AuthTktAuthenticationPolicy
an “auth ticket” cookie should be
set in the request for the user to be identified. In some applications using
another identification mechanism may be needed.
Example with a “remote user” policy¶
For example, in a project of ours, the c2cgeoportal application needs to
integrate with the Nevis single sign-on environment. This SSO system is
composed of a central auth server, and a proxy running as an Apache module.
The proxy takes care of communicating with the auth server, and sets the
username in the REMOTE_USER
environment variable when the user has been
identified. With this system an “auth ticket” policy cannot be used, obviously.
A “remote user” authentication policy
should be used instead.
To use a “remote user” authentication policy edit the application’s
main __init__.py
file, and set authentication_policy
to a
RemoteUserAuthenticationPolicy
instance:
from pyramid.config import Configurator
from pyramid.authentication import RemoteUserAuthenticationPolicy
from c2cgeoportal.pyramid_ import locale_negotiator
from c2cgeoportal.resources import FAModels, defaultgroupsfinder
from ${package}.resources import Root
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
authentication_policy = RemoteUserAuthenticationPolicy(
callback=defaultgroupsfinder)
config = Configurator(root_factory=Root, settings=settings,
locale_negotiator=locale_negotiator,
authentication_policy=authentication_policy)
# ...
c2cgeoportal
provides an authentication policy callback, namely
defaultgroupsfinder
, that is appropriate in most cases. This callback
assumes that request.user.role
is a reference to the Role
database
object (more information below). This callback is required for admin users to
be able to access to the admin interface.
It is important to note that when using a “remote user” authentication policy
the authentication process is delegated to an outside system. So calls to
pyramid.security.remember
and pyramid.security.forget
, as done in
c2cgeoportal’s login
and logout
views, have no effect.
With an authentication policy set in the application configuration the user
name can be obtained by calling request.unauthenticated_userid
.
(This function returns None
if there is currently no authenticated user.)
But c2cgeoportal applications need to also know about the user’s role to
work properly.
So when using an external authentication system this system should also provide
the c2cgeoportal application with the name of the user’s role. This can be done
in apache/wsgi.conf.mako
by relying on the mod_setenvif Apache module’s
SetEnvIf
directive. For example:
SetEnvIf isiwebsectoken <role>(.*)</role> rolename=$1
or:
SetEnvIf isiwebsectoken '<field name="roles">([a-zA-Z0-9,\.]*)</field>' rolename=$1
With this mod_setenvif
extracts the role name from the isiwebsectoken
header
and places it in the rolename
environment variable. See the mod_setenvif
documentation for more details.
The connection between the nevisProxy and the application is established using
an Apache module called NINAP. The above Apache configuration may also contain
NINAP directives (see nevisProxy documentation). For instance to indicate what
field in the isiwebsectoken
header contains the username:
NINAP_UserPattern '<field name="loginId">([a-zA-Z0-9\._-]*)</field>'
Eventually the following directives activate the access restriction to the application:
<Location /<instance_id>/wsgi>
AuthType sectoken
Require valid-user
</Location>
The c2cgeoportal
code expects that the user data (user name, role name and
user functionalities) are available through the user
property in the
request
object. More specifically it expects request.user.role.id
to
contain the role id, and request.user.role.name
to contain the role name.
request.user.username
and request.user.functionalities
must be provided
as well.
Therefore the application should redefine the callback function that adds
a user
property to the request. This is done by calling the
set_request_property
function on the Configurator
object.
You may for example add to __init__.py
:
def get_user_from_request(request, username):
from c2cgeoportal.models import DBSession, Role
class O:
pass
if username is None:
username = request.unauthenticated_userid
if username is not None:
user = O()
user.username = username
rolename = request.environ.get('rolename')
user.role_name = rolename
user.role = DBSession.query(Role).filter_by(name=rolename).one()
user.functionalities = []
return user
And then, in the application’s main
function:
config.set_request_property(
get_user_from_request, name='user', property=True)
config.set_request_property(
get_user_from_request, name='get_user')
Please note that c2cgeoportal
expects the admin role to be role_admin
.
If for some reason you need to use another name for this role, you may define
an alias in a project-specific callback and use it instead of the standard
defaultgroupsfinder
as AuthenticationPolicy
argument in __init__.py
:
def mygroupsfinder(username, request):
role = request.user.role
if role:
if role.name == '<your_admin_rolename>':
return ['role_admin']
return [role.name]
return []
def main(global_config, **settings):
...
authentication_policy = RemoteUserAuthenticationPolicy(
callback=mygroupsfinder)
...
Note
c2cgeoportal
registers its own request property callback for user
.
The one registered by the application overwrites it.
You should be set at this point.
Custom user validation¶
For logging in c2cgeoportal
validates the user credentials
(username/password) by reading the user information from the user
database
table. If a c2cgeoportal application should work with another user information
source, like LDAP, another client validation mechanism should be set up.
c2cgeoportal
provides a specific Configurator
function for that, namely
set_user_validator
which allow to register a custom validator.
Here is an example:
def custom_user_validator(request, username, password):
from pyramid_ldap import get_ldap_connector
connector = get_ldap_connector(request)
data = connector.authenticate(username, password)
if data is not None:
return data[0]
return None
...
config.set_user_validator(custom_user_validator)
The validator function is passed three arguments: request
, username
,
and password
. The function should return a string containing all the data
you want to keep if the credentials are valid, and None
otherwise.
In this example the pyramid_ldap package is used as the user information source.
User validators can obviously be chained. For example, a user validator
function that queries the user
database table if the user does not exist in
LDAP would look like this:
def custom_user_validator(request, username, password):
from c2cgeoportal.pyramid_ import default_user_validator
from pyramid_ldap import get_ldap_connector
connector = get_ldap_connector(request)
data = connector.authenticate(username, password)
if data is not None:
return data[0]
return default_user_validator(request, username, password)
Custom user validation - LDAP¶
Full example using pyramid_ldap, see # LDAP / # END LDAP blocs.
# -*- coding: utf-8 -*-
from pyramid.config import Configurator
# LDAP
# get_user_from_request also needed for the same reason
from c2cgeoportal.pyramid_ import locale_negotiator, add_interface, \
INTERFACE_TYPE_NGEO, get_user_from_request
# END LDAP
from c2cgeoportal.lib.authentication import create_authentication
from yourproject.resources import Root
import logging
# LDAP
# dependencies
import ldap
from json import dumps, loads
# END LDAP
log = logging.getLogger(__name__)
# LDAP
# authenticate on LDAP and return cleaned user data
def custom_user_validator(request, username, password):
from c2cgeoportal.pyramid_ import default_user_validator
from pyramid_ldap import get_ldap_connector
connector = get_ldap_connector(request)
data = connector.authenticate(username, password)
if data is not None:
log.debug('user %s found in ldap' % username)
log.debug(pp.pformat(data[1]))
user = {'username': data[1]['uid'][0], 'role': data[1]['roles'][0]}
return dumps(user)
log.debug("user %s not found in ldap, searching locally" % username)
return default_user_validator(request, username, password)
# get custom user data from request and link with existing c2cgeoportal
# role in your project
def custom_get_user_from_request(request, identity):
class O:
pass
from c2cgeoportal.models import DBSession, Role
if hasattr(request, '_user') and identity is None:
# avoid recursive calls from
# get_user_from_request -> request.authenticated_userid -> ...
return request._user
user = get_user_from_request(request)
if user is None:
log.debug("user is not authenticated or is a ldap user")
if identity is None:
identity = request.unauthenticated_userid
if identity is not None:
identity = loads(identity)
user = O()
user.username = identity['username']
user.functionalities = []
user.is_password_changed = True
user.role_name = identity['role']
user.role = DBSession.query(Role).filter_by(name=identity['role']).one()
user.id = -1
request._user = user
return user
# END LDAP
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(
root_factory=Root, settings=settings,
locale_negotiator=locale_negotiator,
authentication_policy=create_authentication(settings)
)
config.include("c2cgeoportal")
# LDAP
# dependencies
config.include('pyramid_ldap')
# LDAP config
config.ldap_setup(
'ldap://ldap.server.host',
bind='CN=ldap user,CN=Users,DC=example,DC=com',
passwd='ld@pu5er'
)
config.ldap_set_login_query(
base_dn='CN=Users,DC=example,DC=com',
filter_tmpl='(uid=%(login)s)',
scope = ldap.SCOPE_ONELEVEL,
)
config.ldap_set_groups_query(
base_dn='CN=Users,DC=example,DC=com',
filter_tmpl='(&(objectCategory=group)(member=%(userdn)s))',
scope = ldap.SCOPE_SUBTREE,
cache_period = 600,
)
# END LDAP
# scan view decorator for adding routes
config.scan()
# add the interfaces
add_interface(config)
add_interface(config, "edit")
add_interface(config, "routing")
add_interface(config, "mobile", INTERFACE_TYPE_NGEO)
# LDAP
# register the customized function
config.set_user_validator(custom_user_validator)
config.add_request_method(custom_get_user_from_request, 'user', property=True)
config.add_request_method(custom_get_user_from_request, 'get_user')
# END LDAP
return config.make_wsgi_app()