source: main/waeup.cas/trunk/waeup/cas/server.py @ 10399

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

Remove debugging stuff.

File size: 7.3 KB
Line 
1"""A WSGI app for serving CAS.
2"""
3import os
4import random
5import time
6from webob import exc, Response
7from webob.dec import wsgify
8from waeup.cas.authenticators import get_authenticator
9from waeup.cas.db import (
10    DB, DBSessionContext, LoginTicket, ServiceTicket, TicketGrantingCookie)
11
12template_dir = os.path.join(os.path.dirname(__file__), 'templates')
13
14RANDOM = random.SystemRandom(os.urandom(1024))
15
16#: The chars allowed by protocol specification for tickets and cookie
17#: values.
18ALPHABET = ('abcdefghijklmnopqrstuvwxyz'
19            'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
20            '01234567789-')
21
22
23def get_random_string(length):
24    """Get a random string of length `length`.
25
26    The returned string should be hard to guess but is not
27    neccessarily unique.
28    """
29    return ''.join([RANDOM.choice(ALPHABET) for x in range(length)])
30
31
32def get_unique_string():
33    """Get a unique string based on current time.
34
35    The returned string contains only chars from `ALPHABET`.
36
37    We try to be unique by using a timestamp in high resolution, so
38    that even tickets created shortly after another should differ. On
39    very fast machines, however, this might be not enough (currently
40    we use 16 decimal places).
41
42    This is fast because we don't have to fetch foreign data sources
43    nor have to do database lookups.
44
45    The returned string will be unique but it won't be hard to guess
46    for people able to read a clock.
47    """
48    return ('%.16f' % time.time()).replace('.', '-')
49
50
51def create_service_ticket(user, service=None):
52    """Get a service ticket.
53
54    Ticket length will be 32 chars, randomly picked from `ALPHABET`.
55    """
56    t_id = 'ST-' + get_random_string(29)
57    return ServiceTicket(t_id, user, service)
58
59
60def create_login_ticket():
61    """Create a unique login ticket.
62
63    Login tickets are required to be unique (but not neccessarily hard
64    to guess), according to protocol specification.
65    """
66    t_id = 'LT-%s' % get_unique_string()
67    return LoginTicket(t_id)
68
69
70def check_login_ticket(db, lt_string):
71    """Check whether `lt_string` represents a valid login ticket in `db`.
72    """
73    if lt_string is None:
74        return False
75    q = db.query(LoginTicket).filter(LoginTicket.ticket == lt_string)
76    result = [x for x in q]
77    if result:
78        db.delete(result[0])
79    return len(result) > 0
80
81
82def create_tgc_value():
83    """Get a ticket granting cookie value.
84    """
85    value = 'TGC-' + get_random_string(128)
86    return TicketGrantingCookie(value)
87
88
89def set_session_cookie(response, db):
90    """Create a session cookie (ticket granting cookie) on `response`.
91
92    The `db` database is used to make the created cookie value
93    persistent.
94    """
95    tgc = create_tgc_value()
96    db.add(tgc)
97    response.set_cookie(
98        'cas-tgc', tgc.value, path='/', secure=True, httponly=True)
99    return response
100
101
102def check_session_cookie(db, cookie_value):
103    """Check whether `cookie_value` represents a valid ticket granting
104    ticket in `db`.
105    """
106    if cookie_value is None:
107        return False
108    print("VAL1: ", cookie_value)
109    try:
110        # turn value into unicode (py2.x) / str (py3.x)
111        cookie_value = cookie_value.decode('utf-8')
112    except AttributeError:                         # pragma: no cover
113        pass
114    print("VAL2: ", cookie_value)
115    q = db.query(TicketGrantingCookie).filter(
116        TicketGrantingCookie.value == cookie_value)
117    result = [x for x in q]
118    if len(result):
119        return result[0]
120    return None
121
122
123class CASServer(object):
124    """A WSGI CAS server.
125
126    This CAS server stores credential data (tickets, etc.) in a local
127    sqlite3 database file.
128
129    `db_path` -
130       The filesystem path to the database to use. If none is given
131       CAS server will create a new one in some new, temporary
132       directory. Please note that credentials will be lost after a
133       CAS server restart.
134
135       If the path is given and the file exists already, it will be
136       used.
137
138       If the database file does not exist, it will be created.
139    """
140    def __init__(self, db='sqlite:///:memory:', auth=None):
141        self.db_connection_string = db
142        self.db = DB(self.db_connection_string)
143        self.auth = auth
144
145    @wsgify
146    def __call__(self, req):
147        with DBSessionContext():
148            if req.path in ['/login', '/validate', '/logout']:
149                return getattr(self, req.path[1:])(req)
150        return exc.HTTPNotFound()
151
152    def _get_template(self, name):
153        path = os.path.join(template_dir, name)
154        if os.path.isfile(path):
155            return open(path, 'r').read()
156        return None
157
158    def login(self, req):
159        service = req.POST.get('service', req.GET.get('service', None))
160        service_field = ''
161        msg = ''
162        username = req.POST.get('username', None)
163        password = req.POST.get('password', None)
164        valid_lt = check_login_ticket(self.db, req.POST.get('lt'))
165        tgc = req.cookies.get('cas-tgc', None)
166        tgc = check_session_cookie(self.db, tgc)
167        if username and password and valid_lt or tgc:
168            # act as credentials acceptor
169            if tgc:
170                cred_ok, reason = True, ''
171                if not service:
172                    msg = 'You logged in already.'
173            else:
174                cred_ok, reason = self.auth.check_credentials(
175                    username, password)
176            if cred_ok:
177                if service is None:
178                    # show logged-in screen
179                    html = self._get_template('login_successful.html')
180                    html = html.replace('MSG_TEXT', msg)
181                    resp = Response(html)
182                    if not tgc:
183                        resp = set_session_cookie(resp, self.db)
184                    return resp
185                else:
186                    # safely redirect to service given
187                    st = create_service_ticket(service)
188                    self.db.add(st)
189                    service = '%s?ticket=%s' % (service, st.ticket)
190                    html = self._get_template('login_service_redirect.html')
191                    html = html.replace('SERVICE_URL', service)
192                    resp = exc.HTTPSeeOther(location=service)
193                    resp.cache_control = 'no-store'
194                    resp.pragma = 'no-cache'
195                    # some arbitrary date in the past
196                    resp.expires = 'Thu, 01 Dec 1994 16:00:00 GMT'
197                    resp.text = html
198                    return resp
199            else:
200                # login failed
201                msg = '<i>Login failed</i><br />Reason: %s' % reason
202        if service is not None:
203            service_field = (
204                '<input type="hidden" name="service" value="%s" />' % (
205                    service)
206                )
207        lt = create_login_ticket()
208        self.db.add(lt)
209        html = self._get_template('login.html')
210        html = html.replace('LT_VALUE', lt.ticket)
211        html = html.replace('SERVICE_FIELD_VALUE', service_field)
212        html = html.replace('MSG_TEXT', msg)
213        return Response(html)
214
215    def validate(self, req):
216        return exc.HTTPNotImplemented()
217
218    def logout(self, req):
219        return exc.HTTPNotImplemented()
220
221cas_server = CASServer
222
223
224def make_cas_server(global_conf, **local_conf):
225    local_conf = get_authenticator(local_conf)
226    return CASServer(**local_conf)
Note: See TracBrowser for help on using the repository browser.