source: main/waeup.cas/trunk/waeup/cas/authenticators.py @ 10492

Last change on this file since 10492 was 10478, checked in by uli, 11 years ago

Kofa authenticator now really contacts Kofa instances for authentication.

File size: 5.4 KB
RevLine 
[10394]1"""Components that do the real authentication stuff.
2"""
[10475]3import re
[10394]4from pkg_resources import iter_entry_points
[10478]5try:
6    import xmlrpclib                     # Python 2.x
7except ImportError:                      # pragma: no cover
8    import xmlrpc.client as xmlrpclib    # Python 3.x
[10394]9
10
11def get_all_authenticators():
12    """Get all authenticators registered via entry points.
13
14    To create an own authenticator, in the ``setup.py`` of your
15    package add a block like this::
16
17        entry_points='''
18           [waeup.cas.authenticators]
19           myname = my.pkg.my.module:MyAuthenticator
20           '''
21
22    where ``my.pkg.my.module`` must be a module importable at runtime
23    and ``MyAuthenticator`` must be some Authenticator class defined
24    in this module.
25
26    ``myname`` can be chosen as you like, but must not clash with
27    other ``waeup.cas.authenticators``. The name should contain ASCII
28    letters and numbers only, and start with a letter.
29    """
30    return dict(
31        [(x.name, x.load())
32         for x in iter_entry_points(group='waeup.cas.authenticators')])
33
34
35def get_authenticator(local_conf):
36    """Get an `Authenticator` configured by ``local_conf``.
37
38    `local_conf` must be a dictionary of settings.
39
40    An authenticator is identified by its entry-point name which must
41    be given as value of the ``auth`` key.
42
43    It is created passing all keys/values where key starts with
44    ``auth_``.
45
46    Returns a dict of remaining keys/values in `local_conf` with the
47    value of ``auth`` key replaced by an authenticator instance.
48    """
49    gen_opts, auth_opts = filter_auth_opts(local_conf)
50    if 'auth' in local_conf:
51        auth_dict = get_all_authenticators()
52        factory = auth_dict.get(local_conf['auth'])  # get authenticator
53        auth = factory(**auth_opts)
54        gen_opts.update(auth=auth)
55    return gen_opts
56
57
58def filter_auth_opts(local_conf):
59    """Filter out auth-related opts in `local_conf`, a dict.
60
61    Returns two dicts ``(<GENERAL_OPTS>, <AUTHENTICATOR_OPTS>)``
62    containing the general options and authenticator related options.
63
64    This is designed to work with typical paste.deploy config files
65    like this::
66
67      [foo]
68      foo = bar
69      auth = foo
70      auth_opt1 = bar
71
72    All settings not starting with `auth_` are put into the general
73    opts dict, while other are put into the authenticator opts dict.
74    """
75    auth_opts = {}
76    general_opts = {}
77    for name, value in local_conf.items():
78        if name.startswith('auth_'):
79            auth_opts[name] = value
80        else:
81            general_opts[name] = value
82    return general_opts, auth_opts
83
84
85class Authenticator(object):
86
87    #: The name of an authenticator
88    name = 'basic'
89
90    def __init__(self, **kw):
91        pass
92
93
94class DummyAuthenticator(Authenticator):
95    """A dummy authenticator for tests.
96
97    This authenticator does no real authentication. It simply approves
98    credentials ``('bird', 'bebop')`` and denies all other.
99    """
100
101    name = 'dummy'
102
103    def check_credentials(self, username='', password=''):
[10396]104        """If username is ``'bird'`` and password ``'bebop'`` check
105        will succeed.
106
107        Returns a tuple ``(<STATUS>, <REASON>)`` with ``<STATUS>``
108        being a boolean and ``<REASON>`` being a string giving the
109        reason why login failed (if so) or an empty string.
[10394]110        """
[10396]111        reason = ''
112        result = username == 'bird' and password == 'bebop'
113        if not result:
114            reason = 'Invalid username or password.'
115        return result, reason
[10462]116
117
[10478]118#: Regular expression matching a starting university marker like
119#: the string 'MA-' in 'MA-M121212' or 'MA-' in 'MA-APP-13123'
120RE_SCHOOL_MARKER = re.compile('^[^\-]+-')
121
122
[10462]123class KofaAuthenticator(Authenticator):
124    """Authenticate against a running Kofa instance.
125    """
126
127    name = 'kofa1'
128
[10475]129    def __init__(self, auth_backends="{}"):
130        try:
131            self.backends = eval(auth_backends)
132        except:
133            raise ValueError('auth_backends must be a '
134                             'valid Python expression.')
135        self._check_options()
[10462]136
[10475]137    def _check_options(self):
138        if not isinstance(self.backends, dict):
139            raise ValueError('Backends must be configured as dicts.')
140        for key, val in self.backends.items():
141            if not isinstance(val, dict):
142                raise ValueError(
143                    'Backend %s: config must be a dict' % key)
144            if not 'url' in val:
145                raise ValueError(
146                    'Backend %s: config must contain an `url` key.' % key)
147            if not 'marker' in val:
148                self.backends[key]['marker'] = '.+'
149            try:
150                re.compile(self.backends[key]['marker'])
151            except:
152                raise ValueError(
153                    'Backend %s: marker must be a valid regular expr.:' % (
154                        key,))
155
[10462]156    def check_credentials(self, username='', password=''):
157        """Do the real check.
158        """
[10478]159        for backend_name, backend in self.backends.items():
160            if not re.match(backend['marker'], username):
161                continue
162            # remove school marker
163            username = RE_SCHOOL_MARKER.sub('', username)
164            proxy = xmlrpclib.ServerProxy(
165                backend['url'], allow_none=True)
166            valid = proxy.check_credentials(username, password)
167            if valid is not None:
168                return (True, '')
169        return (False, 'Invalid username or password.')
Note: See TracBrowser for help on using the repository browser.