cas.py 7.12 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
# -*- coding: iso-8859-1 -*-
"""
    MoinMoin - CAS authentication

    Jasig CAS (see http://www.jasig.org/cas) authentication module.

    @copyright: 2012 MoinMoin:RichardLiao
    @license: GNU GPL, see COPYING for details.
"""

import sys
12
import os
13 14 15
import time, re
import urlparse
import urllib, urllib2
16 17
from lxml import etree
from lxml.etree import XMLSyntaxError
18 19 20 21 22 23

from MoinMoin import log
logging = log.getLogger(__name__)

from MoinMoin.auth import BaseAuth
from MoinMoin import user, wikiutil
24
from MoinMoin.theme import load_theme_fallback
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

class PyCAS(object):
    """A class for working with a CAS server."""

    def __init__(self, server_url, renew=False, login_path='/login', logout_path='/logout',
                 validate_path='/validate', coding='utf-8'):
        self.server_url = server_url
        self.renew = renew
        self.login_path = login_path
        self.logout_path = logout_path
        self.validate_path = validate_path
        self.coding = coding

    def login_url(self, service):
        """Return the login URL for the given service."""
        url = self.server_url + self.login_path + '?service=' + urllib.quote_plus(service)
        if self.renew:
            url += "&renew=true"
        return url
    def logout_url(self, redirect_url=None):
        """Return the logout URL."""
        url = self.server_url + self.logout_path
        if redirect_url:
            url += '?url=' + urllib.quote_plus(redirect_url)
49
            url += '&service=' + urllib.quote_plus(redirect_url)
50 51 52 53 54 55 56 57 58
        return url

    def validate_url(self, service, ticket):
        """Return the validation URL for the given service. (For CAS 1.0)"""
        url = self.server_url + self.validate_path + '?service=' + urllib.quote_plus(service) + '&ticket=' + urllib.quote_plus(ticket)
        if self.renew:
            url += "&renew=true"
        return url

59 60 61 62 63 64 65 66
    def singlesignout(self, callback, body):
        try:
            nodes = etree.fromstring(body).xpath("/samlp:LogoutRequest/samlp:SessionIndex", namespaces={'samlp' : 'urn:oasis:names:tc:SAML:2.0:protocol'})
            for node in nodes:
                callback(node.text)
        except XMLSyntaxError:
            pass

67 68 69 70 71 72 73 74 75 76 77 78
    def validate_ticket(self, service, ticket):
        """Validate the given ticket against the given service."""
        f = urllib2.urlopen(self.validate_url(service, ticket))
        valid = f.readline()
        valid = valid.strip() == 'yes'
        user = f.readline().strip()
        user = user.decode(self.coding)
        return valid, user

class CASAuth(BaseAuth):
    """ handle login from CAS """
    name = 'CAS'
79
    login_inputs = []
80 81
    logout_possible = True

82
    def __init__(self, auth_server, login_path="/login", logout_path="/logout", validate_path="/validate", action="login_cas", create_user=False, fallback_url=None, ticket_path=None):
83 84 85 86 87 88
        BaseAuth.__init__(self)
        self.cas = PyCAS(auth_server, login_path=login_path,
                         validate_path=validate_path, logout_path=logout_path)
        self.action = action
        self.create_user = create_user
        self.fallback_url = fallback_url
89
        self.ticket_path = ticket_path
90 91 92 93

    def request(self, request, user_obj, **kw):
        ticket = request.args.get("ticket", "")
        action = request.args.get("action", "")
94
        force = request.args.get("force", None) is not None
95 96 97 98
        logoutRequest = request.args.get("logoutRequest", [])
        p = urlparse.urlparse(request.url)
        url = urlparse.urlunparse(('https', p.netloc, p.path, "", "", ""))

99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
        def store_ticket(ticket, username):
            with open(self.ticket_path + ticket, 'w') as f:
                 f.write(username)

        def username_of_ticket(ticket):
            try:
                with open(self.ticket_path + ticket) as f:
                    username = f.read()
                os.remove(self.ticket_path + ticket)
                return username
            except IOError:
                return None

        def logout_user(ticket):
            username = username_of_ticket(ticket)
            if username:
                u = user.User(request, None, username)
                checks = []
                if u.exists():
                    def user_matches(session):
                        try:
                            return session['user.id'] == u.id
                        except KeyError:
                            return False
                    session_service = request.cfg.session_service
                    for sid in session_service.get_all_session_ids(request):
                        session = session_service.get_session(request, sid)

                        if user_matches(session):
                            session_service.destroy_session(request, session)

130
        # authenticated user
Daniel STAN's avatar
Daniel STAN committed
131
        if not force and user_obj and user_obj.valid:
132
            if (action == self.action or (ticket and ticket.startswith('ST-'))) and user_obj.auth_method == self.name:
Daniel STAN's avatar
Daniel STAN committed
133
                request.http_redirect(url)
134

135 136 137
        if self.ticket_path and request.method == 'POST':
            logoutRequest=request.form.get('logoutRequest', None)
            if logoutRequest is not None:
138
                sys.stderr.write("Tentative de deconnexion du CAS : %s\n" % logoutRequest)
139 140
                self.cas.singlesignout(logout_user, logoutRequest)

141
        # valid ticket on CAS
142
        if ticket and ticket.startswith('ST-'):
143 144
            valid, username = self.cas.validate_ticket(url, ticket)
            if valid:
145
                sys.stderr.write("Authentifiaction de %s sur le CAS\n" % username)
146 147 148 149 150 151 152 153
                u = user.User(request, auth_username=username, auth_method=self.name)
                # auto create user ?
                if self.create_user:
                    u.valid = valid
                    u.create_or_update(True)
                else:
                    u.valid = u.exists()
                if self.fallback_url and not u.valid:
154
                    request.http_redirect("%s?action=%s&wiki_url=%s" % (self.fallback_url, self.action, url))
155 156
                if u.valid:
                    store_ticket(ticket, username)
157
                    load_theme_fallback(request, u.theme_name)
158
                return u, True
159 160 161 162
            else:
                request.http_redirect(self.cas.login_url(url))
        elif self.action == action: # Redirect login
            request.http_redirect(self.cas.login_url(url))
163 164 165 166 167 168 169 170 171 172 173 174 175 176

        return user_obj, True

    def logout(self, request, user_obj, **kw):
        if self.name and user_obj and user_obj.auth_method == self.name:
            user_obj.valid = False
            request.cfg.session_service.destroy_session(request, request.session)

            p = urlparse.urlparse(request.url)
            url = urlparse.urlunparse((p.scheme, p.netloc, p.path, "", "", ""))
            request.http_redirect(self.cas.logout_url(url))
        return user_obj, False


177
    def login_hint(self, request):
178 179
        p = urlparse.urlparse(request.url)
        url = urlparse.urlunparse(('https', p.netloc, p.path, "", "", ""))
180
        _ = request.getText
181
        msg = _('<p><a href="%s">Se connecter via le CAS</a> (vous devez disposer d\'un compte Cr@ns pour cela)</p>' % self.cas.login_url(url))
182
        return msg