From 224a257ade788e1a87aab78032dc3cdc9677cc06 Mon Sep 17 00:00:00 2001 From: Eevee Date: Wed, 29 Jul 2009 22:49:32 -0700 Subject: [PATCH] Added OpenID registration and login. --- development.ini | 4 +- floof/config/routing.py | 6 ++- floof/controllers/account.py | 75 ++++++++++++++++++++++++++++++++++ floof/lib/base.py | 9 ++++ floof/model/__init__.py | 22 ++-------- floof/model/meta.py | 9 ++-- floof/model/users.py | 19 +++++++++ floof/templates/base.mako | 15 +++++++ floof/templates/index.mako | 1 + floof/templates/login.mako | 8 ++++ floof/tests/functional/test_account.py | 7 ++++ setup.py | 1 + 12 files changed, 150 insertions(+), 26 deletions(-) create mode 100644 floof/controllers/account.py create mode 100644 floof/model/users.py create mode 100644 floof/templates/base.mako create mode 100644 floof/templates/index.mako create mode 100644 floof/templates/login.mako create mode 100644 floof/tests/functional/test_account.py diff --git a/development.ini b/development.ini index ca29659..bd6fc70 100644 --- a/development.ini +++ b/development.ini @@ -12,7 +12,7 @@ error_email_from = paste@localhost [server:main] use = egg:Paste#http -host = 127.0.0.1 +host = 0.0.0.0 port = 5000 [app:main] @@ -36,7 +36,7 @@ sqlalchemy.url = sqlite:///%(here)s/development.db # WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* # Debug mode will enable the interactive debugging tool, allowing ANYONE to # execute malicious code after an exception is raised. -#set debug = false +set debug = false # Logging configuration diff --git a/floof/config/routing.py b/floof/config/routing.py index 3ff8e54..1fd081a 100644 --- a/floof/config/routing.py +++ b/floof/config/routing.py @@ -13,12 +13,16 @@ def make_map(): always_scan=config['debug']) map.minimization = False + require_POST = dict(conditions={'method': ['POST']}) + # The ErrorController route (handles 404/500 error pages); it should # likely stay at the top, ensuring it can always be resolved map.connect('/error/{action}', controller='error') map.connect('/error/{action}/{id}', controller='error') - # CUSTOM ROUTES HERE + map.connect('/account/login', controller='account', action='login') + map.connect('/account/login_begin', controller='account', action='login_begin', **require_POST) + map.connect('/account/login_finish', controller='account', action='login_finish') map.connect('/{controller}/{action}') map.connect('/{controller}/{action}/{id}') diff --git a/floof/controllers/account.py b/floof/controllers/account.py new file mode 100644 index 0000000..a32583c --- /dev/null +++ b/floof/controllers/account.py @@ -0,0 +1,75 @@ +import logging +from openid.consumer.consumer import Consumer +from openid.extensions.sreg import SRegRequest, SRegResponse +from openid.store.filestore import FileOpenIDStore +from sqlalchemy.orm.exc import NoResultFound + +from pylons import request, response, session, tmpl_context as c +from pylons.controllers.util import abort, redirect_to +from routes import url_for, request_config + +from floof import model +from floof.model.meta import Session +from floof.lib.base import BaseController, render + +log = logging.getLogger(__name__) + +class AccountController(BaseController): + + openid_store = FileOpenIDStore('/var/tmp') + + def login(self): + return render('/login.mako') + + def login_begin(self): + """Step one of logging in with OpenID; we redirect to the provider""" + + cons = Consumer(session=session, store=self.openid_store) + auth_request = cons.begin(request.params['identity_url']) + sreg_req = SRegRequest(optional=['nickname', 'email', 'dob', 'gender', + 'country', 'language', 'timezone']) + auth_request.addExtension(sreg_req) + + host = request.headers['host'] + protocol = request_config().protocol + return_url = url_for(host=host, controller='account', action='login_finish') + new_url = auth_request.redirectURL(return_to=return_url, + realm=protocol + '://' + host) + redirect_to(new_url) + + def login_finish(self): + """Step two of logging in; the OpenID provider redirects back here.""" + + cons = Consumer(session=session, store=self.openid_store) + host = request.headers['host'] + return_url = url_for(host=host, controller='account', action='login_finish') + res = cons.complete(request.params, return_url) + + if res.status != 'success': + return 'Error! %s' % res.message + + try: + # Grab an existing user record, if one exists + q = Session.query(model.User) \ + .filter(model.User.identity_urls.any(url=res.identity_url)) + user = q.one() + except NoResultFound: + # Try to pull a name out of the SReg response + sreg_res = SRegResponse.fromSuccessResponse(res) + try: + username = sreg_res['nickname'] + except: + username = 'Anonymous' + + # Create db records + user = model.User(name=username) + Session.add(user) + identity_url = model.IdentityURL(url=res.identity_url) + user.identity_urls.append(identity_url) + Session.commit() + + # Remember who's logged in, and we're good to go + session['user_id'] = user.id + session.save() + + return "Hello, %s from %s" % (user.name, res.identity_url) diff --git a/floof/lib/base.py b/floof/lib/base.py index fd6ee47..d2756ba 100644 --- a/floof/lib/base.py +++ b/floof/lib/base.py @@ -2,13 +2,22 @@ Provides the BaseController class for subclassing. """ +from pylons import session, tmpl_context as c from pylons.controllers import WSGIController from pylons.templating import render_mako as render +from floof import model from floof.model import meta class BaseController(WSGIController): + def __before__(self, action, **params): + # Fetch current user object + try: + c.user = meta.Session.query(model.User).get(session['user_id']) + except: + pass + def __call__(self, environ, start_response): """Invoke the Controller""" # WSGIController.__call__ dispatches to the Controller method diff --git a/floof/model/__init__.py b/floof/model/__init__.py index af26ba2..e3f810d 100644 --- a/floof/model/__init__.py +++ b/floof/model/__init__.py @@ -4,6 +4,9 @@ from sqlalchemy import orm from floof.model import meta +# Tables are defined in separate files and imported to here +from floof.model.users import * + def init_model(engine): """Call me before using any of the tables or classes in the model""" ## Reflected tables must be defined and mapped here @@ -15,22 +18,3 @@ def init_model(engine): meta.Session.configure(bind=engine) meta.engine = engine - -## Non-reflected tables may be defined and mapped at module level -#foo_table = sa.Table("Foo", meta.metadata, -# sa.Column("id", sa.types.Integer, primary_key=True), -# sa.Column("bar", sa.types.String(255), nullable=False), -# ) -# -#class Foo(object): -# pass -# -#orm.mapper(Foo, foo_table) - - -## Classes for reflected tables may be defined here, but the table and -## mapping itself must be done in the init_model function -#reflected_table = None -# -#class Reflected(object): -# pass diff --git a/floof/model/meta.py b/floof/model/meta.py index 1a20aa7..03d1f92 100644 --- a/floof/model/meta.py +++ b/floof/model/meta.py @@ -1,8 +1,9 @@ """SQLAlchemy Metadata and Session object""" from sqlalchemy import MetaData +from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker -__all__ = ['Session', 'engine', 'metadata'] +__all__ = ['Session', 'engine', 'TableBase'] # SQLAlchemy database engine. Updated by model.init_model() engine = None @@ -10,6 +11,6 @@ engine = None # SQLAlchemy session manager. Updated by model.init_model() Session = scoped_session(sessionmaker()) -# Global metadata. If you have multiple databases with overlapping table -# names, you'll need a metadata for each database -metadata = MetaData() +# Base class for declarative; creates its own metadata object +TableBase = declarative_base() +metadata = TableBase.metadata diff --git a/floof/model/users.py b/floof/model/users.py new file mode 100644 index 0000000..f51282c --- /dev/null +++ b/floof/model/users.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, ForeignKey +from sqlalchemy.orm import relation +from sqlalchemy.types import Integer, Unicode + +from floof.model import meta + +__all__ = ['User', 'IdentityURL'] + +class User(meta.TableBase): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Unicode(length=20), nullable=False) + +class IdentityURL(meta.TableBase): + __tablename__ = 'identity_urls' + url = Column(Unicode(length=255), primary_key=True) + user_id = Column(Integer, ForeignKey('users.id')) + +IdentityURL.user = relation(User, lazy=False, backref="identity_urls") diff --git a/floof/templates/base.mako b/floof/templates/base.mako new file mode 100644 index 0000000..340cdf9 --- /dev/null +++ b/floof/templates/base.mako @@ -0,0 +1,15 @@ +<%def name="title()">Untitled\ + + + +${title()} — Floof + + +% if c.user: +

sup ${c.user.name}

+% else: +

not logged in

+% endif +${next.body()} + + diff --git a/floof/templates/index.mako b/floof/templates/index.mako new file mode 100644 index 0000000..d3c64b9 --- /dev/null +++ b/floof/templates/index.mako @@ -0,0 +1 @@ +<%inherit file="base.mako" /> diff --git a/floof/templates/login.mako b/floof/templates/login.mako new file mode 100644 index 0000000..4497e98 --- /dev/null +++ b/floof/templates/login.mako @@ -0,0 +1,8 @@ +<%inherit file="base.mako" /> + +
+

+ Identity URL: + +

+
diff --git a/floof/tests/functional/test_account.py b/floof/tests/functional/test_account.py new file mode 100644 index 0000000..27eb4c3 --- /dev/null +++ b/floof/tests/functional/test_account.py @@ -0,0 +1,7 @@ +from floof.tests import * + +class TestAccountController(TestController): + + def test_index(self): + response = self.app.get(url(controller='account', action='index')) + # Test response... diff --git a/setup.py b/setup.py index 57f6605..2738b65 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ setup( install_requires=[ "Pylons>=0.9.7", "SQLAlchemy>=0.5", + 'openid', ], setup_requires=["PasteScript>=1.6.3"], packages=find_packages(exclude=['ez_setup']), -- 2.7.4