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

Last change on this file since 10509 was 10509, checked in by Henrik Bettermann, 11 years ago

Add authenticator which authenticate against a running Kofa instance and transfer data to Moodle. No test available. sample4.ini serves as an example configuration file and isn't used by any test.

File size: 8.2 KB
Line 
1"""Components that do the real authentication stuff.
2"""
3import re
4from pkg_resources import iter_entry_points
5try:
6    import xmlrpclib                     # Python 2.x
7except ImportError:                      # pragma: no cover
8    import xmlrpc.client as xmlrpclib    # Python 3.x
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=''):
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.
110        """
111        reason = ''
112        result = username == 'bird' and password == 'bebop'
113        if not result:
114            reason = 'Invalid username or password.'
115        return result, reason
116
117
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
123class KofaAuthenticator(Authenticator):
124    """Authenticate against a running Kofa instance.
125    """
126
127    name = 'kofa1'
128
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()
136
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
156    def check_credentials(self, username='', password=''):
157        """Do the real check.
158        """
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_applicant_credentials(username, password)
167            if valid is None:
168                valid = proxy.check_student_credentials(username, password)
169            if valid is not None:
170                return (True, '')
171        return (False, 'Invalid username or password.')
172
173class KofaMoodleAuthenticator(KofaAuthenticator):
174    """Authenticate against a running Kofa instance and transfer
175    data to Moodle.
176
177    Configuration of Moodle:
178    1. Set 'passwordpolicy' to No
179    2. Create external web service 'Kofa' with the following functions:
180      core_user_create_users, core_user_get_users,
181      core_user_update_users, enrol_manual_enrol_users
182    3. Create token for the admin user (no special web service user needed)
183      and for service 'Kofa'
184    4. Enable and configure CAS server authentication.
185      CAS protocol version is 1.0. Moodle expects SSL/TLS protocol.
186    """
187
188    name = 'kofa_moodle1'
189
190    def check_credentials(self, username='', password=''):
191        """Do the real check.
192        """
193        for backend_name, backend in self.backends.items():
194            if not re.match(backend['marker'], username):
195                continue
196            # remove school marker
197            username = RE_SCHOOL_MARKER.sub('', username)
198            proxy = xmlrpclib.ServerProxy(
199                backend['url'], allow_none=True)
200            moodle = xmlrpclib.ServerProxy(
201                backend['moodle_url'], allow_none=True)
202            principal = proxy.check_applicant_credentials(username, password)
203            if principal is None:
204                principal = proxy.check_student_credentials(username, password)
205            if principal is not None:
206                if principal['type'] == 'student':
207                    student = proxy.get_moodle_data(username)
208                    try:
209                        # Usernames in Moodle must not contain uppercase
210                        # letters even if extendedusernamechars is set True.
211                        result = moodle.core_user_create_users([
212                            {'username':username.lower(),
213                             'password':'dummy',
214                             'firstname':student['firstname'],
215                             'lastname':student['lastname'],
216                             'email':student['email']}])
217                    except xmlrpclib.Fault:
218                        # user exists
219                        pass
220                    result = moodle.core_user_get_users([
221                        {'key':'username', 'value':username}])
222                    user_id = result['users'][0]['id']
223                    # Due to a lack of Moodle (Moodle requires an LDAP
224                    # connection) the authentication method can't
225                    # be set when the user is created. It must be updated
226                    # after creation.
227                    result = moodle.core_user_update_users([
228                        {'id':user_id,'auth':'cas'}])
229                return (True, '')
230        return (False, 'Invalid username or password.')
Note: See TracBrowser for help on using the repository browser.