cas.py 6.44 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

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

from MoinMoin.auth import BaseAuth
from MoinMoin import user, wikiutil

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

58 59 60 61 62 63 64 65
    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

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

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

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

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

129
        # authenticated user
130
        if not force and user_obj and user_obj.valid:
131 132
            if self.action == action:
                request.http_redirect(url)
133 134
            return user_obj, True

135 136 137 138 139
        if self.ticket_path and request.method == 'POST':
            logoutRequest=request.form.get('logoutRequest', None)
            if logoutRequest is not None:
                self.cas.singlesignout(logout_user, logoutRequest)

140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
        # anonymous
        if not ticket and not self.action == action:
            return user_obj, True

        # valid ticket on CAS
        if ticket:
            valid, username = self.cas.validate_ticket(url, ticket)
            if valid:
                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:
156
                    request.http_redirect("%s?action=%s&wiki_url=%s" % (self.fallback_url, self.action, url))
157 158
                if u.valid:
                    store_ticket(ticket, username)
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
                return u, True

        # login
        request.http_redirect(self.cas.login_url(url))

        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