[python-fedora] Add the flask_fas_openid identity provider for flask

Toshio くらとみ toshio at fedoraproject.org
Wed Jul 24 00:14:11 UTC 2013


commit d19ebc0f312b015a06be78155151c686813d6cb8
Author: Toshio Kuratomi <toshio at fedoraproject.org>
Date:   Tue Jul 23 17:13:36 2013 -0700

    Add the flask_fas_openid identity provider for flask

 flask_fas_openid.py |  211 +++++++++++++++++++++++++++++++++++++++++++++++++++
 python-fedora.spec  |   16 ++++-
 2 files changed, 226 insertions(+), 1 deletions(-)
---
diff --git a/flask_fas_openid.py b/flask_fas_openid.py
new file mode 100644
index 0000000..97dec0e
--- /dev/null
+++ b/flask_fas_openid.py
@@ -0,0 +1,211 @@
+# -*- coding: utf-8 -*-
+# Flask-FAS-OpenID - A Flask extension for authorizing users with FAS-OpenID
+#
+# Primary maintainer: Patrick Uiterwijk <puiterwijk at fedoraproject.org>
+#
+# Copyright (c) 2013, Patrick Uiterwijk
+# This file is part of python-fedora
+#
+# python-fedora is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# python-fedora is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with python-fedora; if not, see <http://www.gnu.org/licenses/>
+
+'''
+FAS-OpenID authentication plugin for the flask web framework
+
+.. moduleauthor:: Patrick Uiterwijk <puiterwijk at fedoraproject.org>
+
+..versionadded:: 0.3.33
+'''
+from functools import wraps
+
+from bunch import Bunch
+import flask
+try:
+    from flask import _app_ctx_stack as stack
+except ImportError:
+    from flask import _request_ctx_stack as stack
+
+import openid
+from openid.consumer import consumer
+from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
+from openid.extensions import pape, sreg
+
+from fedora import __version__
+import openid_cla.cla
+import openid_teams.teams
+
+class FAS(object):
+
+    def __init__(self, app=None):
+        self.app = app
+        if self.app is not None:
+            self._init_app(app)
+
+    def _init_app(self, app):
+        app.config.setdefault('FAS_OPENID_ENDPOINT',
+                              'http://id.fedoraproject.org/')
+        app.config.setdefault('FAS_OPENID_CHECK_CERT', True)
+
+        if not self.app.config['FAS_OPENID_CHECK_CERT']:
+            setDefaultFetcher(Urllib2Fetcher())
+
+        @app.route('/_flask_fas_openid_handler/', methods=['GET', 'POST'])
+        def flask_fas_openid_handler():
+            return self._handle_openid_request()
+
+        app.before_request(self._check_session)
+
+    def _handle_openid_request(self):
+        return_url = flask.session['FLASK_FAS_OPENID_RETURN_URL']
+        cancel_url = flask.session['FLASK_FAS_OPENID_CANCEL_URL']
+        base_url = self.normalize_url(flask.request.base_url)
+        oidconsumer = consumer.Consumer(flask.session, None)
+        info = oidconsumer.complete(flask.request.values, base_url)
+        display_identifier = info.getDisplayIdentifier()
+
+        if info.status == consumer.FAILURE and display_identifier:
+            return 'FAILURE. display_identifier: %s' % display_identifier
+        elif info.status == consumer.CANCEL:
+            if cancel_url:
+                return flask.redirect(cancel_url)
+            return 'OpenID request was cancelled'
+        elif info.status == consumer.SUCCESS:
+            sreg_resp =  sreg.SRegResponse.fromSuccessResponse(info)
+            pape_resp = pape.Response.fromSuccessResponse(info)
+            teams_resp = teams.TeamsResponse.fromSuccessResponse(info)
+            cla_resp = cla.CLAResponse.fromSuccessResponse(info)
+            user = {'fullname': '', 'username': '', 'email': '', 'timezone': '', 'cla_done': False, 'groups': []}
+            if not sreg_resp:
+                # If we have no basic info, be gone with them!
+                return flask.redirect(cancel_url)
+            user['username'] = sreg_resp.get('nickname')
+            user['fullname'] = sreg_resp.get('fullname')
+            user['email'] = sreg_resp.get('email')
+            user['timezone'] = sreg_resp.get('timezone')
+            if cla_resp:
+                user['cla_done'] = cla.CLA_URI_FEDORA_DONE in cla_resp.clas
+            if teams_resp:
+                user['groups'] = frozenset(teams_resp.teams) # The groups do not contain the cla_ groups
+            flask.session['FLASK_FAS_OPENID_USER'] = user
+            flask.session.modified = True
+            return flask.redirect(return_url)
+        else:
+            return 'Strange state: %s' % info.status
+
+    def _check_session(self):
+        if not 'FLASK_FAS_OPENID_USER' in flask.session or flask.session['FLASK_FAS_OPENID_USER'] is None:
+            flask.g.fas_user = None
+        else:
+            user = flask.session['FLASK_FAS_OPENID_USER']
+            # Add approved_memberships to provide backwards compatibility
+            # New applications should only use g.fas_user.groups
+            user['approved_memberships'] = []
+            for group in user['groups']:
+                membership = dict()
+                membership['name'] = group
+                user['approved_memberships'].append(Bunch.fromDict(membership))
+            flask.g.fas_user = Bunch.fromDict(user)
+        flask.g.fas_session_id = 0
+
+    def login(self, username=None, password=None, return_url=None, cancel_url=None):
+        """Tries to log in a user.
+
+        Sets the user information on :attr:`flask.g.fas_user`.
+        Will set 0 to :attr:`flask.g.fas_session_id, for compatibility
+        with flask_fas.
+
+        :arg username: Not used, but accepted for compatibility with the flask_fas module
+        :arg password: Not used, but accepted for compatibility with the flask_fas module
+        :arg return_url: The URL to forward the user to after login
+        :returns: True if the user was succesfully authenticated.
+        :raises: Might raise an redirect to the OpenID endpoint
+        """
+        if return_url is None:
+            if 'next' in flask.request.args.values:
+                return_url = flask.request.args.values['next']
+            else:
+                return_url = flask.request.url
+        oidconsumer = consumer.Consumer(flask.session, None)
+        try:
+            request = oidconsumer.begin(self.app.config['FAS_OPENID_ENDPOINT'])
+        except consumer.DiscoveryFailure, exc:
+            # VERY strange, as this means it could not discover an OpenID endpoint at FAS_OPENID_ENDPOINT
+            return 'discoveryfailure'
+        if request is None:
+            # Also very strange, as this means the discovered OpenID endpoint is no OpenID endpoint
+            return 'no-request'
+        request.addExtension(sreg.SRegRequest(required=['nickname', 'fullname', 'email', 'timezone']))
+        request.addExtension(pape.Request([]))
+        request.addExtension(teams.TeamsRequest(requested=['_FAS_ALL_GROUPS_'])) # Magic value which requests all groups from FAS-OpenID >= 0.2.0
+        request.addExtension(cla.CLARequest(requested=[cla.CLA_URI_FEDORA_DONE]))
+
+        trust_root = self.normalize_url(flask.request.url_root)
+        return_to = trust_root + '_flask_fas_openid_handler/'
+
+        flask.session['FLASK_FAS_OPENID_RETURN_URL'] = return_url
+        flask.session['FLASK_FAS_OPENID_CANCEL_URL'] = cancel_url
+        if request.shouldSendRedirect():
+            redirect_url = request.redirectURL(trust_root, return_to, False)
+            return flask.redirect(redirect_url)
+        else:
+            return request.htmlMarkup(trust_root, return_to, form_tag_attrs={'id': 'openid_message'}, immediate=False)
+
+    def logout(self):
+        '''Logout the user associated with this session
+        '''
+        flask.session['FLASK_FAS_OPENID_USER'] = None
+        flask.g.fas_session_id = None
+        flask.g.fas_user = None
+        flask.session.modified = True
+
+    def normalize_url(self, url):
+        ''' Replace the scheme prefix of a url with our preferred scheme.
+        '''
+        scheme = self.app.config['PREFERRED_URL_SCHEME']
+        scheme_index = url.index('://')
+        return scheme + url[scheme_index:]
+
+
+# This is a decorator we can use with any HTTP method (except login, obviously)
+# to require a login.
+# If the user is not logged in, it will redirect them to the login form.
+# http://flask.pocoo.org/docs/patterns/viewdecorators/#login-required-decorator
+def fas_login_required(function):
+    """ Flask decorator to ensure that the user is logged in against FAS.
+    To use this decorator you need to have a function named 'auth_login'.
+    Without that function the redirect if the user is not logged in will not
+    work.
+    """
+    @wraps(function)
+    def decorated_function(*args, **kwargs):
+        if flask.g.fas_user is None:
+            return flask.redirect(flask.url_for('auth_login',
+                                                next=flask.request.url))
+        return function(*args, **kwargs)
+    return decorated_function
+
+
+def cla_plus_one_required(function):
+    """ Flask decorator to retrict access to CLA+1.
+    To use this decorator you need to have a function named 'auth_login'.
+    Without that function the redirect if the user is not logged in will not
+    work.
+    """
+    @wraps(function)
+    def decorated_function(*args, **kwargs):
+        if flask.g.fas_user is None or not flask.g.fas_user.cla_done or len(flask.g.fas_user.groups) < 1:  # FAS-OpenID does not return cla_ groups
+            return flask.redirect(flask.url_for('auth_login',
+                                                next=flask.request.url))
+        else:
+            return function(*args, **kwargs)
+    return decorated_function
diff --git a/python-fedora.spec b/python-fedora.spec
index beec2f5..423a5f1 100644
--- a/python-fedora.spec
+++ b/python-fedora.spec
@@ -4,13 +4,16 @@
 
 Name:           python-fedora
 Version:        0.3.32.3
-Release:        1%{?dist}
+Release:        2%{?dist}
 Summary:        Python modules for talking to Fedora Infrastructure Services
 
 Group:          Development/Languages
 License:        LGPLv2+
 URL:            https://fedorahosted.org/python-fedora/
 Source0:        https://fedorahosted.org/releases/p/y/%{name}/%{name}-%{version}%{?prerel}.tar.gz
+# Checked out from https://raw.github.com/fedora-infra/python-fedora/develop/flask_fas_openid.py
+# on 2013-07-23
+Source1: flask_fas_openid.py
 BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
 
 BuildArch:      noarch
@@ -31,6 +34,9 @@ BuildRequires:  python-bunch
 # Needed for tests and for the way we build docs
 BuildRequires: TurboGears python-repoze-who-friendlyform Django
 BuildRequires: python-requests
+BuildRequires: python-openid
+BuildRequires: python-openid-teams
+BuildRequires: python-openid-cla
 
 Requires:       python-simplejson
 Requires:       python-bunch
@@ -105,6 +111,9 @@ License:        LGPLv2+
 Requires: %{name} = %{version}-%{release}
 Requires: python-flask
 Requires: python-flask-wtf
+Requires: python-openid
+Requires: python-openid-teams
+Requires: python-openid-cla
 
 %description flask
 Python modules that help with building Fedora Services.  This package includes
@@ -147,6 +156,7 @@ rm -rf %{buildroot}
 %exclude %{python_sitelib}/fedora/wsgi/
 %exclude %{python_sitelib}/fedora/django/
 %exclude %{python_sitelib}/flask_fas.py*
+%exclude %{python_sitelib}/flask_fas_openid.py*
 
 %files turbogears
 %{python_sitelib}/fedora/tg/
@@ -160,8 +170,12 @@ rm -rf %{buildroot}
 
 %files flask
 %{python_sitelib}/flask_fas.py*
+%{python_sitelib}/flask_fas_openid.py
 
 %changelog
+* Tue Jul 23 2013 Toshio Kuratomi <toshio at fedoraproject.org> - 0.3.32.3-2
+- Add the flask_fas_openid identity provider for flask
+
 * Tue Feb  5 2013 Toshio Kuratomi <toshio at fedoraproject.org> - 0.3.32.3-1
 - Upstream update to fix BodhiClient's knowledge of koji tags (ajax)
 


More information about the scm-commits mailing list