cas.py 7.13 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
import time
import re
15 16
import urlparse
import urllib, urllib2
17 18
from lxml import etree
from lxml.etree import XMLSyntaxError
19 20 21 22 23 24

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

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

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)
50
            url += '&service=' + urllib.quote_plus(redirect_url)
51 52 53 54 55 56 57 58 59
        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

60 61 62 63 64 65 66 67
    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

68 69 70 71 72 73 74 75 76 77 78 79
    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'
80
    login_inputs = []
81 82
    logout_possible = True

83
    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):
84 85 86 87 88 89
        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
90
        self.ticket_path = ticket_path
91 92 93 94

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

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 130
        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)

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

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

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

        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


178
    def login_hint(self, request):
179 180
        p = urlparse.urlparse(request.url)
        url = urlparse.urlunparse(('https', p.netloc, p.path, "", "", ""))
181
        _ = request.getText
182
        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))
183
        return msg