Blame progit/flask_fas_openid.py

Pierre-Yves Chibon f1a2e7
# -*- coding: utf-8 -*-
Pierre-Yves Chibon f1a2e7
# Flask-FAS-OpenID - A Flask extension for authorizing users with FAS-OpenID
Pierre-Yves Chibon f1a2e7
#
Pierre-Yves Chibon f1a2e7
# Primary maintainer: Patrick Uiterwijk <puiterwijk@fedoraproject.org></puiterwijk@fedoraproject.org>
Pierre-Yves Chibon f1a2e7
#
Pierre-Yves Chibon f1a2e7
# Copyright (c) 2013, Patrick Uiterwijk
Pierre-Yves Chibon f1a2e7
# This file is part of python-fedora
Pierre-Yves Chibon f1a2e7
#
Pierre-Yves Chibon f1a2e7
# python-fedora is free software; you can redistribute it and/or
Pierre-Yves Chibon f1a2e7
# modify it under the terms of the GNU Lesser General Public
Pierre-Yves Chibon f1a2e7
# License as published by the Free Software Foundation; either
Pierre-Yves Chibon f1a2e7
# version 2.1 of the License, or (at your option) any later version.
Pierre-Yves Chibon f1a2e7
#
Pierre-Yves Chibon f1a2e7
# python-fedora is distributed in the hope that it will be useful,
Pierre-Yves Chibon f1a2e7
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Pierre-Yves Chibon f1a2e7
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Pierre-Yves Chibon f1a2e7
# Lesser General Public License for more details.
Pierre-Yves Chibon f1a2e7
#
Pierre-Yves Chibon f1a2e7
# You should have received a copy of the GNU Lesser General Public
Pierre-Yves Chibon f1a2e7
# License along with python-fedora; if not, see <http: licenses="" www.gnu.org=""></http:>
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
'''
Pierre-Yves Chibon f1a2e7
FAS-OpenID authentication plugin for the flask web framework
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
.. moduleauthor:: Patrick Uiterwijk <puiterwijk@fedoraproject.org></puiterwijk@fedoraproject.org>
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
..versionadded:: 0.3.33
Pierre-Yves Chibon f1a2e7
'''
Pierre-Yves Chibon f1a2e7
from functools import wraps
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
from bunch import Bunch
Pierre-Yves Chibon f1a2e7
import flask
Pierre-Yves Chibon f1a2e7
try:
Pierre-Yves Chibon f1a2e7
    from flask import _app_ctx_stack as stack
Pierre-Yves Chibon 944e0e
except ImportError:  # pragma: no cover
Pierre-Yves Chibon f1a2e7
    from flask import _request_ctx_stack as stack
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
import openid
Pierre-Yves Chibon f1a2e7
from openid.consumer import consumer
Pierre-Yves Chibon f1a2e7
from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
Pierre-Yves Chibon f1a2e7
from openid.extensions import pape, sreg
Pierre-Yves Chibon f1a2e7
from openid_cla import cla
Pierre-Yves Chibon f1a2e7
from openid_teams import teams
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
from fedora import __version__
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon 944e0e
class FASJSONEncoder(flask.json.JSONEncoder):  # pragma: no cover
Pierre-Yves Chibon f1a2e7
    """ Dedicated JSON encoder for the FAS openid information. """
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def default(self, o):
Pierre-Yves Chibon f1a2e7
        """Implement this method in a subclass such that it returns a
Pierre-Yves Chibon f1a2e7
        serializable object for ``o``, or calls the base implementation (to
Pierre-Yves Chibon f1a2e7
        raise a ``TypeError``).
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        For example, to support arbitrary iterators, you could implement
Pierre-Yves Chibon f1a2e7
        default like this::
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        def default(self, o):
Pierre-Yves Chibon f1a2e7
            try:
Pierre-Yves Chibon f1a2e7
                iterable = iter(o)
Pierre-Yves Chibon f1a2e7
            except TypeError:
Pierre-Yves Chibon f1a2e7
                pass
Pierre-Yves Chibon f1a2e7
            else:
Pierre-Yves Chibon f1a2e7
                return list(iterable)
Pierre-Yves Chibon f1a2e7
            return JSONEncoder.default(self, o)
Pierre-Yves Chibon f1a2e7
        """
Pierre-Yves Chibon f1a2e7
        if isinstance(o, (set, frozenset)):
Pierre-Yves Chibon f1a2e7
            return list(o)
Pierre-Yves Chibon f1a2e7
        return flask.json.JSONEncoder.default(self, o)
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon 944e0e
class FAS(object):  # pragma: no cover
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def __init__(self, app=None):
Pierre-Yves Chibon f1a2e7
        self.postlogin_func = None
Pierre-Yves Chibon f1a2e7
        self.app = app
Pierre-Yves Chibon f1a2e7
        if self.app is not None:
Pierre-Yves Chibon f1a2e7
            self._init_app(app)
Pierre-Yves Chibon f1a2e7
        # json_encoder is only available from flask 0.10
Pierre-Yves Chibon f1a2e7
        version = flask.__version__.split('.')
Pierre-Yves Chibon f1a2e7
        assume_recent = False
Pierre-Yves Chibon f1a2e7
        try:
Pierre-Yves Chibon f1a2e7
            major = int(version[0])
Pierre-Yves Chibon f1a2e7
            minor = int(version[1])
Pierre-Yves Chibon f1a2e7
        except ValueError:
Pierre-Yves Chibon f1a2e7
            # We'll assume we're using a recent enough flask as the packages
Pierre-Yves Chibon f1a2e7
            # of old versions used sane version numbers.
Pierre-Yves Chibon f1a2e7
            assume_recent = True
Pierre-Yves Chibon f1a2e7
        if assume_recent or (major >= 0 and minor >= 10):
Pierre-Yves Chibon f1a2e7
            self.app.json_encoder = FASJSONEncoder
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def _init_app(self, app):
Pierre-Yves Chibon f1a2e7
        app.config.setdefault('FAS_OPENID_ENDPOINT',
Pierre-Yves Chibon f1a2e7
                              'http://id.fedoraproject.org/')
Pierre-Yves Chibon f1a2e7
        app.config.setdefault('FAS_OPENID_CHECK_CERT', True)
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        if not self.app.config['FAS_OPENID_CHECK_CERT']:
Pierre-Yves Chibon f1a2e7
            setDefaultFetcher(Urllib2Fetcher())
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        @app.route('/_flask_fas_openid_handler/', methods=['GET', 'POST'])
Pierre-Yves Chibon f1a2e7
        def flask_fas_openid_handler():
Pierre-Yves Chibon f1a2e7
            return self._handle_openid_request()
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        app.before_request(self._check_session)
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def postlogin(self, f):
Pierre-Yves Chibon f1a2e7
        """Marks a function as post login handler. This decorator calls your
Pierre-Yves Chibon f1a2e7
        function after the login has been performed.
Pierre-Yves Chibon f1a2e7
        """
Pierre-Yves Chibon f1a2e7
        self.postlogin_func = f
Pierre-Yves Chibon f1a2e7
        return f
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def _handle_openid_request(self):
Pierre-Yves Chibon f1a2e7
        return_url = flask.session.get('FLASK_FAS_OPENID_RETURN_URL', None)
Pierre-Yves Chibon f1a2e7
        cancel_url = flask.session.get('FLASK_FAS_OPENID_CANCEL_URL', None)
Pierre-Yves Chibon f1a2e7
        base_url = self.normalize_url(flask.request.base_url)
Pierre-Yves Chibon f1a2e7
        oidconsumer = consumer.Consumer(flask.session, None)
Pierre-Yves Chibon f1a2e7
        info = oidconsumer.complete(flask.request.values, base_url)
Pierre-Yves Chibon f1a2e7
        display_identifier = info.getDisplayIdentifier()
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        if info.status == consumer.FAILURE and display_identifier:
Pierre-Yves Chibon f1a2e7
            return 'FAILURE. display_identifier: %s' % display_identifier
Pierre-Yves Chibon f1a2e7
        elif info.status == consumer.CANCEL:
Pierre-Yves Chibon f1a2e7
            if cancel_url:
Pierre-Yves Chibon f1a2e7
                return flask.redirect(cancel_url)
Pierre-Yves Chibon f1a2e7
            return 'OpenID request was cancelled'
Pierre-Yves Chibon f1a2e7
        elif info.status == consumer.SUCCESS:
Pierre-Yves Chibon f1a2e7
            sreg_resp = sreg.SRegResponse.fromSuccessResponse(info)
Pierre-Yves Chibon f1a2e7
            pape_resp = pape.Response.fromSuccessResponse(info)
Pierre-Yves Chibon f1a2e7
            teams_resp = teams.TeamsResponse.fromSuccessResponse(info)
Pierre-Yves Chibon f1a2e7
            cla_resp = cla.CLAResponse.fromSuccessResponse(info)
Pierre-Yves Chibon f1a2e7
            user = {'fullname': '', 'username': '', 'email': '',
Pierre-Yves Chibon f1a2e7
                    'timezone': '', 'cla_done': False, 'groups': []}
Pierre-Yves Chibon f1a2e7
            if not sreg_resp:
Pierre-Yves Chibon f1a2e7
                # If we have no basic info, be gone with them!
Pierre-Yves Chibon f1a2e7
                return flask.redirect(cancel_url)
Pierre-Yves Chibon f1a2e7
            user['username'] = sreg_resp.get('nickname')
Pierre-Yves Chibon f1a2e7
            user['fullname'] = sreg_resp.get('fullname')
Pierre-Yves Chibon f1a2e7
            user['email'] = sreg_resp.get('email')
Pierre-Yves Chibon f1a2e7
            user['timezone'] = sreg_resp.get('timezone')
Pierre-Yves Chibon f1a2e7
            if cla_resp:
Pierre-Yves Chibon f1a2e7
                user['cla_done'] = cla.CLA_URI_FEDORA_DONE in cla_resp.clas
Pierre-Yves Chibon f1a2e7
            if teams_resp:
Pierre-Yves Chibon f1a2e7
                # The groups do not contain the cla_ groups
Pierre-Yves Chibon f1a2e7
                user['groups'] = frozenset(teams_resp.teams)
Pierre-Yves Chibon f1a2e7
            flask.session['FLASK_FAS_OPENID_USER'] = user
Pierre-Yves Chibon f1a2e7
            flask.session.modified = True
Pierre-Yves Chibon f1a2e7
            if self.postlogin_func is not None:
Pierre-Yves Chibon f1a2e7
                self._check_session()
Pierre-Yves Chibon f1a2e7
                return self.postlogin_func(return_url)
Pierre-Yves Chibon f1a2e7
            else:
Pierre-Yves Chibon f1a2e7
                return flask.redirect(return_url)
Pierre-Yves Chibon f1a2e7
        else:
Pierre-Yves Chibon f1a2e7
            return 'Strange state: %s' % info.status
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def _check_session(self):
Pierre-Yves Chibon e04c77
        if 'FLASK_FAS_OPENID_USER' not in flask.session \
Pierre-Yves Chibon f1a2e7
                or flask.session['FLASK_FAS_OPENID_USER'] is None:
Pierre-Yves Chibon f1a2e7
            flask.g.fas_user = None
Pierre-Yves Chibon f1a2e7
        else:
Pierre-Yves Chibon f1a2e7
            user = flask.session['FLASK_FAS_OPENID_USER']
Pierre-Yves Chibon f1a2e7
            # Add approved_memberships to provide backwards compatibility
Pierre-Yves Chibon f1a2e7
            # New applications should only use g.fas_user.groups
Pierre-Yves Chibon f1a2e7
            user['approved_memberships'] = []
Pierre-Yves Chibon f1a2e7
            for group in user['groups']:
Pierre-Yves Chibon f1a2e7
                membership = dict()
Pierre-Yves Chibon f1a2e7
                membership['name'] = group
Pierre-Yves Chibon f1a2e7
                user['approved_memberships'].append(Bunch.fromDict(membership))
Pierre-Yves Chibon f1a2e7
            flask.g.fas_user = Bunch.fromDict(user)
Pierre-Yves Chibon f1a2e7
            flask.g.fas_user.groups = frozenset(flask.g.fas_user.groups)
Pierre-Yves Chibon f1a2e7
        flask.g.fas_session_id = 0
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def login(self, username=None, password=None, return_url=None,
Pierre-Yves Chibon f1a2e7
              cancel_url=None, groups=['_FAS_ALL_GROUPS_']):
Pierre-Yves Chibon f1a2e7
        """Tries to log in a user.
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        Sets the user information on :attr:`flask.g.fas_user`.
Pierre-Yves Chibon f1a2e7
        Will set 0 to :attr:`flask.g.fas_session_id, for compatibility
Pierre-Yves Chibon f1a2e7
        with flask_fas.
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        :kwarg username: Not used, but accepted for compatibility with the
Pierre-Yves Chibon f1a2e7
            flask_fas module
Pierre-Yves Chibon f1a2e7
        :kwarg password: Not used, but accepted for compatibility with the
Pierre-Yves Chibon f1a2e7
            flask_fas module
Pierre-Yves Chibon f1a2e7
        :kwarg return_url: The URL to forward the user to after login
Pierre-Yves Chibon f1a2e7
        :kwarg groups: A string or a list of group the user should belong
Pierre-Yves Chibon f1a2e7
            to to be authentified.
Pierre-Yves Chibon f1a2e7
        :returns: True if the user was succesfully authenticated.
Pierre-Yves Chibon f1a2e7
        :raises: Might raise an redirect to the OpenID endpoint
Pierre-Yves Chibon f1a2e7
        """
Pierre-Yves Chibon f1a2e7
        if return_url is None and return_func is None:
Pierre-Yves Chibon f1a2e7
            return_url = flask.request.args.get('next', flask.request.url)
Pierre-Yves Chibon f1a2e7
        session = {}
Pierre-Yves Chibon f1a2e7
        oidconsumer = consumer.Consumer(session, None)
Pierre-Yves Chibon f1a2e7
        try:
Pierre-Yves Chibon f1a2e7
            request = oidconsumer.begin(self.app.config['FAS_OPENID_ENDPOINT'])
Pierre-Yves Chibon f1a2e7
        except consumer.DiscoveryFailure, exc:
Pierre-Yves Chibon f1a2e7
            # VERY strange, as this means it could not discover an OpenID
Pierre-Yves Chibon f1a2e7
            # endpoint at FAS_OPENID_ENDPOINT
Pierre-Yves Chibon f1a2e7
            return 'discoveryfailure'
Pierre-Yves Chibon f1a2e7
        if request is None:
Pierre-Yves Chibon f1a2e7
            # Also very strange, as this means the discovered OpenID
Pierre-Yves Chibon f1a2e7
            # endpoint is no OpenID endpoint
Pierre-Yves Chibon f1a2e7
            return 'no-request'
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        if isinstance(groups, basestring):
Pierre-Yves Chibon f1a2e7
            groups = [groups]
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        request.addExtension(sreg.SRegRequest(
Pierre-Yves Chibon f1a2e7
            required=['nickname', 'fullname', 'email', 'timezone']))
Pierre-Yves Chibon f1a2e7
        request.addExtension(pape.Request([]))
Pierre-Yves Chibon f1a2e7
        request.addExtension(teams.TeamsRequest(requested=groups))
Pierre-Yves Chibon f1a2e7
        request.addExtension(cla.CLARequest(
Pierre-Yves Chibon f1a2e7
            requested=[cla.CLA_URI_FEDORA_DONE]))
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        trust_root = self.normalize_url(flask.request.url_root)
Pierre-Yves Chibon f1a2e7
        return_to = trust_root + '_flask_fas_openid_handler/'
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
        flask.session['FLASK_FAS_OPENID_RETURN_URL'] = return_url
Pierre-Yves Chibon f1a2e7
        flask.session['FLASK_FAS_OPENID_CANCEL_URL'] = cancel_url
Pierre-Yves Chibon f1a2e7
        if request.shouldSendRedirect():
Pierre-Yves Chibon f1a2e7
            redirect_url = request.redirectURL(
Pierre-Yves Chibon f1a2e7
                trust_root, return_to, False)
Pierre-Yves Chibon f1a2e7
            return flask.redirect(redirect_url)
Pierre-Yves Chibon f1a2e7
        else:
Pierre-Yves Chibon f1a2e7
            return request.htmlMarkup(
Pierre-Yves Chibon f1a2e7
                trust_root, return_to,
Pierre-Yves Chibon f1a2e7
                form_tag_attrs={'id': 'openid_message'}, immediate=False)
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def logout(self):
Pierre-Yves Chibon f1a2e7
        '''Logout the user associated with this session
Pierre-Yves Chibon f1a2e7
        '''
Pierre-Yves Chibon f1a2e7
        flask.session['FLASK_FAS_OPENID_USER'] = None
Pierre-Yves Chibon f1a2e7
        flask.g.fas_session_id = None
Pierre-Yves Chibon f1a2e7
        flask.g.fas_user = None
Pierre-Yves Chibon f1a2e7
        flask.session.modified = True
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
    def normalize_url(self, url):
Pierre-Yves Chibon f1a2e7
        ''' Replace the scheme prefix of a url with our preferred scheme.
Pierre-Yves Chibon f1a2e7
        '''
Pierre-Yves Chibon f1a2e7
        scheme = self.app.config['PREFERRED_URL_SCHEME']
Pierre-Yves Chibon f1a2e7
        scheme_index = url.index('://')
Pierre-Yves Chibon f1a2e7
        return scheme + url[scheme_index:]
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
# This is a decorator we can use with any HTTP method (except login, obviously)
Pierre-Yves Chibon f1a2e7
# to require a login.
Pierre-Yves Chibon f1a2e7
# If the user is not logged in, it will redirect them to the login form.
Pierre-Yves Chibon f1a2e7
# http://flask.pocoo.org/docs/patterns/viewdecorators/#login-required-decorator
Pierre-Yves Chibon 944e0e
def fas_login_required(function):  # pragma: no cover
Pierre-Yves Chibon f1a2e7
    """ Flask decorator to ensure that the user is logged in against FAS.
Pierre-Yves Chibon f1a2e7
    To use this decorator you need to have a function named 'auth_login'.
Pierre-Yves Chibon f1a2e7
    Without that function the redirect if the user is not logged in will not
Pierre-Yves Chibon f1a2e7
    work.
Pierre-Yves Chibon f1a2e7
    """
Pierre-Yves Chibon f1a2e7
    @wraps(function)
Pierre-Yves Chibon f1a2e7
    def decorated_function(*args, **kwargs):
Pierre-Yves Chibon f1a2e7
        if flask.g.fas_user is None:
Pierre-Yves Chibon f1a2e7
            return flask.redirect(flask.url_for('auth_login',
Pierre-Yves Chibon f1a2e7
                                                next=flask.request.url))
Pierre-Yves Chibon f1a2e7
        return function(*args, **kwargs)
Pierre-Yves Chibon f1a2e7
    return decorated_function
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon f1a2e7
Pierre-Yves Chibon 944e0e
def cla_plus_one_required(function):  # pragma: no cover
Pierre-Yves Chibon f1a2e7
    """ Flask decorator to retrict access to CLA+1.
Pierre-Yves Chibon f1a2e7
    To use this decorator you need to have a function named 'auth_login'.
Pierre-Yves Chibon f1a2e7
    Without that function the redirect if the user is not logged in will not
Pierre-Yves Chibon f1a2e7
    work.
Pierre-Yves Chibon f1a2e7
    """
Pierre-Yves Chibon f1a2e7
    @wraps(function)
Pierre-Yves Chibon f1a2e7
    def decorated_function(*args, **kwargs):
Pierre-Yves Chibon f1a2e7
        if flask.g.fas_user is None or not flask.g.fas_user.cla_done \
Pierre-Yves Chibon f1a2e7
                or len(flask.g.fas_user.groups) < 1:
Pierre-Yves Chibon f1a2e7
            # FAS-OpenID does not return cla_ groups
Pierre-Yves Chibon f1a2e7
            return flask.redirect(flask.url_for('auth_login',
Pierre-Yves Chibon f1a2e7
                                                next=flask.request.url))
Pierre-Yves Chibon f1a2e7
        else:
Pierre-Yves Chibon f1a2e7
            return function(*args, **kwargs)
Pierre-Yves Chibon f1a2e7
    return decorated_function