python-repoze.who-plugins-20090913/0000755000175000017500000000000011253246105015522 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/0000755000175000017500000000000011221406122022577 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/0000755000175000017500000000000011221406122030453 5ustar zackzack././@LongLink0000000000000000000000000000015400000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/requires.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/requi0000644000175000017500000000005011221406122031516 0ustar zackzackrepoze.who >= 1.0.14 sqlalchemy >= 0.5.0././@LongLink0000000000000000000000000000016600000000000011570 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/namespace_packages.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/names0000644000175000017500000000004511221406122031500 0ustar zackzackrepoze repoze.who repoze.who.plugins ././@LongLink0000000000000000000000000000016400000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/dependency_links.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/depen0000644000175000017500000000000111221406122031460 0ustar zackzack ././@LongLink0000000000000000000000000000015000000000000011561 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/PKG-INFOpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/PKG-I0000644000175000017500000000175211221406122031212 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who.plugins.sa Version: 1.0rc2 Summary: The repoze.who SQLAlchemy plugin Home-page: http://code.gustavonarea.net/repoze.who.plugins.sa/ Author: Gustavo Narea Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: ******************************** The repoze.who SQLAlchemy plugin ******************************** This plugin provides one repoze.who authenticator and one metadata provider which works with SQLAlchemy or Elixir-based models. Keywords: web application server wsgi sql sqlalchemy elixir authentication repoze Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Database Classifier: Topic :: Security ././@LongLink0000000000000000000000000000016000000000000011562 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/entry_points.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/entry0000644000175000017500000000000611221406122031533 0ustar zackzack ././@LongLink0000000000000000000000000000015500000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/top_level.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/top_l0000644000175000017500000000001511221406122031507 0ustar zackzackrepoze tests ././@LongLink0000000000000000000000000000015400000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/not-zip-safepython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/not-z0000644000175000017500000000000111122526062031442 0ustar zackzack ././@LongLink0000000000000000000000000000015300000000000011564 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/SOURCES.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze.who.plugins.sa.egg-info/SOURC0000644000175000017500000000171011221406122031270 0ustar zackzackREADME.txt VERSION.txt setup.cfg setup.py docs/Makefile docs/source/News.rst docs/source/conf.py docs/source/index.rst docs/source/_static/logo_hi.gif docs/source/_static/model_elixir_example.py docs/source/_static/model_sa_example.py docs/source/_static/repoze.css repoze/__init__.py repoze.who.plugins.sa.egg-info/PKG-INFO repoze.who.plugins.sa.egg-info/SOURCES.txt repoze.who.plugins.sa.egg-info/dependency_links.txt repoze.who.plugins.sa.egg-info/entry_points.txt repoze.who.plugins.sa.egg-info/namespace_packages.txt repoze.who.plugins.sa.egg-info/not-zip-safe repoze.who.plugins.sa.egg-info/requires.txt repoze.who.plugins.sa.egg-info/top_level.txt repoze/who/__init__.py repoze/who/plugins/__init__.py repoze/who/plugins/sa.py tests/__init__.py tests/databasesetup_elixir.py tests/databasesetup_sa.py tests/test_authenticator.py tests/test_mdprovider.py tests/test_userchecker.py tests/fixture/__init__.py tests/fixture/elixir_model.py tests/fixture/sa_model.pypython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/0000755000175000017500000000000011221406122023741 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/fixture/0000755000175000017500000000000011221406122025427 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/fixture/elixir_model.py0000644000175000017500000000575111137377031030501 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """Mock Elixir-powered model definition.""" from hashlib import sha1 from datetime import datetime from sqlalchemy.orm import scoped_session, sessionmaker import elixir from elixir import Entity, Field from elixir import DateTime, Unicode from elixir import using_options DBSession = scoped_session(sessionmaker(autoflush=True, autocommit=False)) metadata = elixir.metadata elixir.session = DBSession def init_model(engine): """Call me before using any of the tables or classes in the model.""" DBSession.configure(bind=engine) metadata.bind = engine class User(Entity): """Reasonably basic User definition. Probably would want additional attributes. """ using_options(tablename="user", auto_primarykey="user_id") user_name = Field(Unicode(16), required=True, unique=True) _password = Field(Unicode(40), colname="password", required=True) def _set_password(self, password): """encrypts password on the fly""" self._password = self.__encrypt_password(password) def _get_password(self): """returns password""" return self._password password = descriptor=property(_get_password, _set_password) def __encrypt_password(self, password): """Hash the given password with SHA1.""" if isinstance(password, unicode): password_8bit = password.encode('UTF-8') else: password_8bit = password hashed_password = sha1() hashed_password.update(password_8bit) hashed_password = hashed_password.hexdigest() # make sure the hased password is an UTF-8 object at the end of the # process because SQLAlchemy _wants_ a unicode object for Unicode columns if not isinstance(hashed_password, unicode): hashed_password = hashed_password.decode('UTF-8') return hashed_password def validate_password(self, password): """Check the password against existing credentials. this method _MUST_ return a boolean. @param password: the password that was provided by the user to try and authenticate. This is the clear text version that we will need to match against the (possibly) encrypted one in the database. @type password: unicode object """ return self.password == self.__encrypt_password(password) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/fixture/__init__.py0000644000175000017500000000133211137377031027553 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """Fixture package for the test suite.""" python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/fixture/sa_model.py0000644000175000017500000001160211137377031027600 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """Mock SQLAlchemy-powered model definition.""" from hashlib import sha1 from datetime import datetime from sqlalchemy import Table, ForeignKey, Column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.types import String, Unicode, UnicodeText, Integer, DateTime, \ Boolean, Float from sqlalchemy.orm import scoped_session, sessionmaker, relation, backref, \ synonym DBSession = scoped_session(sessionmaker(autoflush=True, autocommit=False)) DeclarativeBase = declarative_base() metadata = DeclarativeBase.metadata def init_model(engine): """Call me before using any of the tables or classes in the model.""" DBSession.configure(bind=engine) class User(DeclarativeBase): """Reasonably basic User definition. Probably would want additional attributes. """ __tablename__ = 'user' user_id = Column(Integer, autoincrement=True, primary_key=True) user_name = Column(Unicode(16), unique=True) _password = Column('password', Unicode(40)) def _set_password(self, password): """encrypts password on the fly.""" self._password = self.__encrypt_password(password) def _get_password(self): """returns password""" return self._password password = synonym('password', descriptor=property(_get_password, _set_password)) def __encrypt_password(self, password): """Hash the given password with SHA1.""" if isinstance(password, unicode): password_8bit = password.encode('UTF-8') else: password_8bit = password hashed_password = sha1() hashed_password.update(password_8bit) hashed_password = hashed_password.hexdigest() # make sure the hased password is an UTF-8 object at the end of the # process because SQLAlchemy _wants_ a unicode object for Unicode columns if not isinstance(hashed_password, unicode): hashed_password = hashed_password.decode('UTF-8') return hashed_password def validate_password(self, password): """Check the password against existing credentials. this method _MUST_ return a boolean. @param password: the password that was provided by the user to try and authenticate. This is the clear text version that we will need to match against the (possibly) encrypted one in the database. @type password: unicode object """ return self.password == self.__encrypt_password(password) class Member(DeclarativeBase): """Reasonably basic User definition. Probably would want additional attributes. It uses non-default attributes, so it'll have to be translated. """ __tablename__ = 'member' member_id = Column(Integer, autoincrement=True, primary_key=True) member_name = Column(Unicode(16), unique=True) _password = Column('password', Unicode(40)) def _set_password(self, password): """encrypts password on the fly.""" self._password = self.__encrypt_password(password) def _get_password(self): """returns password""" return self._password password = synonym('password', descriptor=property(_get_password, _set_password)) def __encrypt_password(self, password): """Hash the given password with SHA1.""" if isinstance(password, unicode): password_8bit = password.encode('UTF-8') else: password_8bit = password hashed_password = sha1() hashed_password.update(password_8bit) hashed_password = hashed_password.hexdigest() # make sure the hased password is an UTF-8 object at the end of the # process because SQLAlchemy _wants_ a unicode object for Unicode columns if not isinstance(hashed_password, unicode): hashed_password = hashed_password.decode('UTF-8') return hashed_password def verify_pass(self, password): """Check the password against existing credentials. this method _MUST_ return a boolean. """ return self.password == self.__encrypt_password(password) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/test_userchecker.py0000644000175000017500000000251311221403373027663 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """ Tests for the repoze.who SQLAlchemy MD provider. """ import unittest from repoze.who.plugins.sa import SQLAlchemyUserChecker import databasesetup_sa from fixture import sa_model class TestUserChecker(unittest.TestCase): """Tests for the user checker""" def setUp(self): databasesetup_sa.setup_database() self.plugin = SQLAlchemyUserChecker(sa_model.User, sa_model.DBSession) def tearDown(self): databasesetup_sa.teardownDatabase() def test_existing_user(self): self.assertTrue(self.plugin(u"guido")) def test_non_existing_user(self): self.assertFalse(self.plugin(u"gustavo")) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/__init__.py0000644000175000017500000000153211115557007026066 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2007, Agendaless Consulting and Contributors. # Copyright (c) 2008, Florent Aide and # Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """Test suite for the repoze.what SQL plugin.""" python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/test_authenticator.py0000644000175000017500000001260211137377031030241 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """ Tests for the repoze.who SQLAlchemy authenticator. """ import unittest from zope.interface.verify import verifyClass from repoze.who.interfaces import IAuthenticator from repoze.who.plugins.sa import SQLAlchemyAuthenticatorPlugin, \ make_sa_authenticator import databasesetup_sa, databasesetup_elixir from fixture import sa_model, elixir_model class TestAuthenticator(unittest.TestCase): """Tests for the authenticator function""" def setUp(self): databasesetup_sa.setup_database() self.plugin = SQLAlchemyAuthenticatorPlugin(sa_model.User, sa_model.DBSession) def tearDown(self): databasesetup_sa.teardownDatabase() def test_implements(self): verifyClass(IAuthenticator, SQLAlchemyAuthenticatorPlugin, tentative=True) def test_no_identity(self): identity = {} self.assertEqual(None, self.plugin.authenticate(None, identity)) def test_incomplete_credentials(self): identity = {'login': 'rms'} self.assertEqual(None, self.plugin.authenticate(None, identity)) identity = {'password': 'freedom'} self.assertEqual(None, self.plugin.authenticate(None, identity)) def test_no_match(self): identity = {'login': u'gustavo', 'password': u'narea'} self.assertEqual(None, self.plugin.authenticate(None, identity)) def test_match(self): identity = {'login': u'rms', 'password': u'freedom'} self.assertEqual(u'rms', self.plugin.authenticate(None, identity)) class TestAuthenticatorWithTranslations(unittest.TestCase): """Tests for the authenticator function""" def setUp(self): databasesetup_sa.setup_database_with_translations() def tearDown(self): databasesetup_sa.teardownDatabase() def test_it(self): self.plugin = SQLAlchemyAuthenticatorPlugin(sa_model.Member, sa_model.DBSession) # Updating the translations... self.plugin.translations['user_name'] = 'member_name' self.plugin.translations['validate_password'] = 'verify_pass' # Testing it... identity = {'login': u'rms', 'password': u'freedom'} self.assertEqual(u'rms', self.plugin.authenticate(None, identity)) class TestAuthenticatorWithElixir(TestAuthenticator): def setUp(self): databasesetup_elixir.setup_database() self.plugin = SQLAlchemyAuthenticatorPlugin(elixir_model.User, elixir_model.DBSession) def tearDown(self): databasesetup_elixir.teardownDatabase() class TestAuthenticatorMaker(unittest.TestCase): def setUp(self): databasesetup_sa.setup_database() def tearDown(self): databasesetup_sa.teardownDatabase() def test_simple_call(self): user_class = 'tests.fixture.sa_model:User' dbsession = 'tests.fixture.sa_model:DBSession' authenticator = make_sa_authenticator(user_class, dbsession) self.assertTrue(isinstance(authenticator, SQLAlchemyAuthenticatorPlugin)) def test_no_user_class(self): dbsession = 'tests.fixture.sa_model:DBSession' self.assertRaises(ValueError, make_sa_authenticator, None, dbsession) def test_no_dbsession(self): user_class = 'tests.fixture.sa_model:User' self.assertRaises(ValueError, make_sa_authenticator, user_class) def test_username_translation(self): user_class = 'tests.fixture.sa_model:User' dbsession = 'tests.fixture.sa_model:DBSession' username_translation = 'username' authenticator = make_sa_authenticator(user_class, dbsession, username_translation) self.assertTrue(isinstance(authenticator, SQLAlchemyAuthenticatorPlugin)) self.assertEqual(username_translation, authenticator.translations['user_name']) def test_passwd_validator_translation(self): user_class = 'tests.fixture.sa_model:User' dbsession = 'tests.fixture.sa_model:DBSession' password_validator_translation = 'verify_pass' authenticator = make_sa_authenticator( user_class, dbsession, validate_password_translation=password_validator_translation) self.assertTrue(isinstance(authenticator, SQLAlchemyAuthenticatorPlugin)) self.assertEqual(password_validator_translation, authenticator.translations['validate_password']) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/databasesetup_elixir.py0000644000175000017500000000340511137377031030532 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """Stuff required to setup the test database.""" import os from sqlalchemy import * from sqlalchemy.orm import * from cStringIO import StringIO from cgi import FieldStorage import elixir from fixture.elixir_model import init_model, DBSession, metadata, User engine = create_engine(os.environ.get('DBURL', 'sqlite://')) def setup_database(): init_model(engine) teardownDatabase() elixir.setup_all(True) # Creating users user = User() user.user_name = u'rms' user.password = u'freedom' DBSession.add(user) user = User() user.user_name = u'linus' user.password = u'linux' DBSession.add(user) user = User() user.user_name = u'sballmer' user.password = u'developers' DBSession.add(user) # Plus a couple of users without groups user = User() user.user_name = u'guido' user.password = u'phytonic' DBSession.add(user) user = User() user.user_name = u'rasmus' user.password = u'php' DBSession.add(user) DBSession.commit() def teardownDatabase(): DBSession.rollback() metadata.drop_all(engine) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/test_mdprovider.py0000644000175000017500000001006011221400007027516 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """ Tests for the repoze.who SQLAlchemy MD provider. """ import unittest from zope.interface.verify import verifyClass from repoze.who.interfaces import IMetadataProvider from repoze.who.plugins.sa import SQLAlchemyUserMDPlugin, \ make_sa_user_mdprovider import databasesetup_sa from fixture import sa_model class TestMDProvider(unittest.TestCase): """Tests for the authenticator function""" def setUp(self): databasesetup_sa.setup_database() self.plugin = SQLAlchemyUserMDPlugin(sa_model.User, sa_model.DBSession) def tearDown(self): databasesetup_sa.teardownDatabase() def test_implements(self): verifyClass(IMetadataProvider, SQLAlchemyUserMDPlugin, tentative=True) def test_it(self): user = sa_model.DBSession.query(sa_model.User).\ filter(sa_model.User.user_name==u'rms').one() identity = {'repoze.who.userid': user.user_name} expected_identity = { 'repoze.who.userid': user.user_name, 'user': user} self.plugin.add_metadata(None, identity) self.assertEqual(identity, expected_identity) class TestMDProviderWithTranslations(unittest.TestCase): """Tests for the translation functionality""" def setUp(self): databasesetup_sa.setup_database_with_translations() def tearDown(self): databasesetup_sa.teardownDatabase() def test_it(self): self.plugin = SQLAlchemyUserMDPlugin(sa_model.Member, sa_model.DBSession) # Updating the translations... self.plugin.translations['user_name'] = 'member_name' # Testing it... member = sa_model.DBSession.query(sa_model.Member).\ filter(sa_model.Member.member_name==u'rms').one() identity = {'repoze.who.userid': member.member_name} expected_identity = { 'repoze.who.userid': member.member_name, 'user': member} self.plugin.add_metadata(None, identity) self.assertEqual(expected_identity, identity) class TestMDProviderMaker(unittest.TestCase): def setUp(self): databasesetup_sa.setup_database() def tearDown(self): databasesetup_sa.teardownDatabase() def test_simple_call(self): user_class = 'tests.fixture.sa_model:User' dbsession = 'tests.fixture.sa_model:DBSession' mdprovider = make_sa_user_mdprovider(user_class, dbsession) self.assertTrue(isinstance(mdprovider, SQLAlchemyUserMDPlugin)) def test_no_user_class(self): dbsession = 'tests.fixture.sa_model:DBSession' self.assertRaises(ValueError, make_sa_user_mdprovider, None, dbsession) def test_no_dbsession(self): user_class = 'tests.fixture.sa_model:User' self.assertRaises(ValueError, make_sa_user_mdprovider, user_class) def test_username_translation(self): user_class = 'tests.fixture.sa_model:User' dbsession = 'tests.fixture.sa_model:DBSession' username_translation = 'username' mdprovider = make_sa_user_mdprovider(user_class, dbsession, username_translation) self.assertTrue(isinstance(mdprovider, SQLAlchemyUserMDPlugin)) self.assertEqual(username_translation, mdprovider.translations['user_name']) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/tests/databasesetup_sa.py0000644000175000017500000000504311137377031027641 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """Stuff required to setup the test database.""" import os from sqlalchemy import * from sqlalchemy.orm import * from cStringIO import StringIO from cgi import FieldStorage from fixture.sa_model import init_model, DBSession, metadata, User, Member engine = create_engine(os.environ.get('DBURL', 'sqlite://')) def setup_database(): init_model(engine) teardownDatabase() metadata.create_all(engine) # Creating users user = User() user.user_name = u'rms' user.password = u'freedom' DBSession.add(user) user = User() user.user_name = u'linus' user.password = u'linux' DBSession.add(user) user = User() user.user_name = u'sballmer' user.password = u'developers' DBSession.add(user) # Plus a couple of users without groups user = User() user.user_name = u'guido' user.password = u'phytonic' DBSession.add(user) user = User() user.user_name = u'rasmus' user.password = u'php' DBSession.add(user) DBSession.commit() def setup_database_with_translations(): init_model(engine) teardownDatabase() metadata.create_all(engine) # Creating members member = Member() member.member_name = u'rms' member.password = u'freedom' DBSession.add(member) member = Member() member.member_name = u'linus' member.password = u'linux' DBSession.add(member) member = Member() member.member_name = u'sballmer' member.password = u'developers' DBSession.add(member) # Plus a couple of members without groups member = Member() member.member_name = u'guido' member.password = u'phytonic' DBSession.add(member) member = Member() member.member_name = u'rasmus' member.password = u'php' DBSession.add(member) DBSession.commit() def teardownDatabase(): DBSession.rollback() metadata.drop_all(engine) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/README.txt0000644000175000017500000000034411137403076024311 0ustar zackzack******************************** The repoze.who SQLAlchemy plugin ******************************** This plugin provides one repoze.who authenticator and one metadata provider which works with SQLAlchemy or Elixir-based models. python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze/0000755000175000017500000000000011221406122024103 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze/__init__.py0000644000175000017500000000164511137377031026236 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze/who/0000755000175000017500000000000011221406122024700 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze/who/__init__.py0000644000175000017500000000164411137377031027032 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze/who/plugins/0000755000175000017500000000000011221406122026361 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze/who/plugins/sa.py0000644000175000017500000002356611221404200027345 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """ SQLAlchemy plugin for repoze.who. TODO: Write a function that configures the three plugins in one go. """ from zope.interface import implements from repoze.who.interfaces import IAuthenticator, IMetadataProvider from repoze.who.utils import resolveDotted from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound __all__ = ("SQLAlchemyAuthenticatorPlugin", "SQLAlchemyUserMDPlugin", "SQLAlchemyUserChecker", "make_sa_authenticator", "make_sa_user_mdprovider") class _BaseSQLAlchemyPlugin(object): default_translations = {'user_name': "user_name"} def __init__(self, user_class, dbsession): """ Setup the plugin. :param user_class: The SQLAlchemy/Elixir class for the users. :param session: The SQLAlchemy/Elixir session. """ self.user_class = user_class self.dbsession = dbsession self.translations = self.default_translations.copy() def get_user(self, username): # Getting a translation: username_attr = getattr(self.user_class, self.translations['user_name']) query = self.dbsession.query(self.user_class) query = query.filter(username_attr==username) try: return query.one() except (NoResultFound, MultipleResultsFound): # As recommended in the docs for repoze.who, it's important to # verify that there's only _one_ matching userid. return None class SQLAlchemyUserChecker(_BaseSQLAlchemyPlugin): """ User existence checker for :class:`repoze.who.plugins.auth_tkt.AuthTktCookiePlugin`. Example:: from repoze.who.plugins.sa import SQLAlchemyUserChecker from yourcoolproject.model import User, DBSession checker = SQLAlchemyUserChecker(User, DBSession) This plugin assumes that the user name is kept in the ``user_name`` attribute of the users' class. If you don't want to call it that way, then you have to "translate" it as in the sample below:: # You have User.username instead of User.user_name: checker.translations['user_name'] = 'username' """ def __call__(self, username): """ Check whether a user account identified by ``username`` exists. :param username: The user account's id to be verified. :type username: basestring :return: Whether it exists or not. :rtype: bool """ if self.get_user(username): return True return False #{ repoze.who plugins class SQLAlchemyAuthenticatorPlugin(_BaseSQLAlchemyPlugin): """ :mod:`repoze.who` authenticator for SQLAlchemy models. Example:: from repoze.who.plugins.sa import SQLAlchemyAuthenticatorPlugin from yourcoolproject.model import User, DBSession authenticator = SQLAlchemyAuthenticatorPlugin(User, DBSession) This plugin assumes that the user name is kept in the ``user_name`` attribute of the users' class, as well as that such a class has a method that verifies the user's password against the password provided through the login form (it receives the password to be verified as the only argument and such method is assumed to be called ``validate_password``). If you don't want to call the attributes above as ``user_name`` and/or ``validate_password``, respectively, then you have to "translate" them as in the sample below:: # You have User.username instead of User.user_name: authenticator.translations['user_name'] = 'username' # You have User.verify_password instead of User.validate_password: authenticator.translations['validate_password'] = 'verify_password' .. note:: If you want to configure this authenticator from an ``ini`` file, use :func:`make_sa_authenticator`. """ implements(IAuthenticator) default_translations = _BaseSQLAlchemyPlugin.default_translations.copy() default_translations['validate_password'] = "validate_password" # IAuthenticator def authenticate(self, environ, identity): if not ("login" in identity and "password" in identity): return None user = self.get_user(identity['login']) if user: validator = getattr(user, self.translations['validate_password']) if validator(identity['password']): return identity['login'] class SQLAlchemyUserMDPlugin(_BaseSQLAlchemyPlugin): """ :mod:`repoze.who` metadata provider that loads the SQLAlchemy-powered object for the current user. It loads the object into ``identity['user']``. Example:: from repoze.who.plugins.sa import SQLAlchemyUserMDPlugin from yourcoolproject.model import User, DBSession mdprovider = SQLAlchemyUserMDPlugin(User, DBSession) This plugin assumes that the user name is kept in the ``user_name`` attribute of the users' class. If you don't want to call the attribute above as ``user_name``, then you have to "translate" it as in the sample below:: # You have User.username instead of User.user_name: mdprovider.translations['user_name'] = 'username' .. note:: If you want to configure this plugin from an ``ini`` file, use :func:`make_sa_user_mdprovider`. """ implements(IMetadataProvider) def add_metadata(self, environ, identity): identity['user'] = self.get_user(identity['repoze.who.userid']) #{ Functions to instantiate the plugins from a Paste configuration def _base_plugin_maker(user_class=None, dbsession=None): """ Turn ``userclass`` and ``dbsession`` into Python objects. """ if user_class is None: raise ValueError("user_class must not be None") if dbsession is None: raise ValueError("dbsession must not be None") return resolveDotted(user_class), resolveDotted(dbsession) def make_sa_authenticator(user_class=None, dbsession=None, user_name_translation=None, validate_password_translation=None): """ Configure :class:`SQLAlchemyAuthenticatorPlugin`. :param user_class: The SQLAlchemy/Elixir class for the users. :type user_class: str :param dbsession: The SQLAlchemy/Elixir session. :type dbsession: str :param user_name_translation: The translation for ``user_name``, if any. :type user_name_translation: str :param validate_password_translation: The translation for ``validate_password``, if any. :type validate_password_translation: str :return: The authenticator. :rtype: SQLAlchemyAuthenticatorPlugin Example from an ``*.ini`` file:: # ... [plugin:sa_auth] use = repoze.who.plugins.sa:make_sa_authenticator user_class = yourcoolproject.model:User dbsession = yourcoolproject.model:DBSession # ... Or, if you need translations:: # ... [plugin:sa_auth] use = repoze.who.plugins.sa:make_sa_authenticator user_class = yourcoolproject.model:User dbsession = yourcoolproject.model:DBSession user_name_translation = username validate_password_translation = verify_password # ... """ user_model, dbsession_object = _base_plugin_maker(user_class, dbsession) authenticator = SQLAlchemyAuthenticatorPlugin(user_model, dbsession_object) if user_name_translation: authenticator.translations['user_name'] = user_name_translation if validate_password_translation: authenticator.translations['validate_password'] = \ validate_password_translation return authenticator def make_sa_user_mdprovider(user_class=None, dbsession=None, user_name_translation=None): """ Configure :class:`SQLAlchemyUserMDPlugin`. :param user_class: The SQLAlchemy/Elixir class for the users. :type user_class: str :param dbsession: The SQLAlchemy/Elixir session. :type dbsession: str :param user_name_translation: The translation for ``user_name``, if any. :type user_name_translation: str :return: The metadata provider. :rtype: SQLAlchemyUserMDPlugin Example from an ``*.ini`` file:: # ... [plugin:sa_md] use = repoze.who.plugins.sa:make_sa_user_mdprovider user_class = yourcoolproject.model:User dbsession = yourcoolproject.model:DBSession # ... Or, if you need translations:: # ... [plugin:sa_md] use = repoze.who.plugins.sa:make_sa_user_mdprovider user_class = yourcoolproject.model:User dbsession = yourcoolproject.model:DBSession user_name_translation = username # ... """ user_model, dbsession_object = _base_plugin_maker(user_class, dbsession) mdprovider = SQLAlchemyUserMDPlugin(user_model, dbsession_object) if user_name_translation: mdprovider.translations['user_name'] = user_name_translation return mdprovider #} python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/repoze/who/plugins/__init__.py0000644000175000017500000000172511137377030030512 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2008-2009, Gustavo Narea # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## """Special namespace for repoze.who plugins.""" # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/PKG-INFO0000644000175000017500000000175211221406122023701 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who.plugins.sa Version: 1.0rc2 Summary: The repoze.who SQLAlchemy plugin Home-page: http://code.gustavonarea.net/repoze.who.plugins.sa/ Author: Gustavo Narea Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: ******************************** The repoze.who SQLAlchemy plugin ******************************** This plugin provides one repoze.who authenticator and one metadata provider which works with SQLAlchemy or Elixir-based models. Keywords: web application server wsgi sql sqlalchemy elixir authentication repoze Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Database Classifier: Topic :: Security python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/VERSION.txt0000644000175000017500000000000611137404212024465 0ustar zackzack1.0rc2python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/0000755000175000017500000000000011221406122023527 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/0000755000175000017500000000000011221406122025027 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/News.rst0000644000175000017500000000252011221405355026503 0ustar zackzack************************************* :mod:`repoze.who.plugins.sa` releases ************************************* This document describes the releases of :mod:`repoze.who.plugins.sa`. .. _repoze.who.plugins.sa-1.0rc2: :mod:`repoze.who.plugins.sa` 1.0rc2 (2009-06-27) ================================================ * Added :class:`repoze.who.plugins.sa.SQLAlchemyUserChecker`, a user checker for :class:`repoze.who.plugins.auth_tkt.AuthTktCookiePlugin`. .. _repoze.who.plugins.sa-1.0rc1: :mod:`repoze.who.plugins.sa` 1.0rc1 (2009-01-26) ================================================ * Introduced the :class:`repoze.who.plugins.sa.SQLAlchemyUserMDPlugin` metadata provider. * Minor docstring fixes. .. _repoze.who.plugins.sa-1.0b3: :mod:`repoze.who.plugins.sa` 1.0b3 (2009-01-08) =============================================== Fixed `Bug #56 `_ (``User.user_name`` was not translatable). .. _repoze.who.plugins.sa-1.0b2: :mod:`repoze.who.plugins.sa` 1.0b2 (2008-12-18) =============================================== Renamed :mod:`repoze.who.plugins.sqlalchemy` to :mod:`repoze.who.plugins.sa` due to problems with the namespace. .. _repoze.who.plugins.sqlalchemy-1.0b1: :mod:`repoze.who.plugins.sqlalchemy` 1.0b1 (2008-12-18) ======================================================= Initial release. python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/_static/0000755000175000017500000000000011221406122026455 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/_static/repoze.css0000644000175000017500000000047411112057356030512 0ustar zackzack@import url('default.css'); body { background-color: #006339; } div.document { background-color: #dad3bd; } div.sphinxsidebar h3,h4,h5,li,a { color: #127c56 !important; } div.related { color: #dad3bd !important; background-color: #00744a; } div.related a { color: #dad3bd !important; } ././@LongLink0000000000000000000000000000015000000000000011561 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/_static/model_sa_example.pypython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/_static/model_sa_example0000644000175000017500000000757711113757077031740 0ustar zackzack""" Sample SQLAlchemy-powered model definition for the repoze.what SQL plugin. This model definition has been taken from a quickstarted TurboGears 2 project, but it's absolutely independent of TurboGears. """ import md5 import sha from datetime import datetime from sqlalchemy import Table, ForeignKey, Column from sqlalchemy.types import String, Unicode, UnicodeText, Integer, DateTime, \ Boolean, Float from sqlalchemy.orm import relation, backref, synonym from yourproject.model import DeclarativeBase, metadata, DBSession # This is the association table for the many-to-many relationship between # groups and permissions. group_permission_table = Table('group_permission', metadata, Column('group_id', Integer, ForeignKey('group.group_id', onupdate="CASCADE", ondelete="CASCADE")), Column('permission_id', Integer, ForeignKey('permission.permission_id', onupdate="CASCADE", ondelete="CASCADE")) ) # This is the association table for the many-to-many relationship between # groups and members - this is, the memberships. user_group_table = Table('user_group', metadata, Column('user_id', Integer, ForeignKey('user.user_id', onupdate="CASCADE", ondelete="CASCADE")), Column('group_id', Integer, ForeignKey('group.group_id', onupdate="CASCADE", ondelete="CASCADE")) ) # auth model class Group(DeclarativeBase): """An ultra-simple group definition. """ __tablename__ = 'group' group_id = Column(Integer, autoincrement=True, primary_key=True) group_name = Column(Unicode(16), unique=True) users = relation('User', secondary=user_group_table, backref='groups') class User(DeclarativeBase): """Reasonably basic User definition. Probably would want additional attributes. """ __tablename__ = 'user' user_id = Column(Integer, autoincrement=True, primary_key=True) user_name = Column(Unicode(16), unique=True) _password = Column('password', Unicode(40)) def _set_password(self, password): """encrypts password on the fly.""" self._password = self.__encrypt_password(password) def _get_password(self): """returns password""" return self._password password = synonym('password', descriptor=property(_get_password, _set_password)) def __encrypt_password(self, password): """Hash the given password with SHA1. Edit this method to implement your own algorithm. """ hashed_password = password if isinstance(password, unicode): password_8bit = password.encode('UTF-8') else: password_8bit = password hashed_password = sha.new(password_8bit).hexdigest() # make sure the hased password is an UTF-8 object at the end of the # process because SQLAlchemy _wants_ a unicode object for Unicode columns if not isinstance(hashed_password, unicode): hashed_password = hashed_password.decode('UTF-8') return hashed_password def validate_password(self, password): """Check the password against existing credentials. this method _MUST_ return a boolean. @param password: the password that was provided by the user to try and authenticate. This is the clear text version that we will need to match against the (possibly) encrypted one in the database. @type password: unicode object """ return self.password == self.__encrypt_password(password) class Permission(DeclarativeBase): """A relationship that determines what each Group can do""" __tablename__ = 'permission' permission_id = Column(Integer, autoincrement=True, primary_key=True) permission_name = Column(Unicode(16), unique=True) groups = relation(Group, secondary=group_permission_table, backref='permissions') python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/_static/logo_hi.gif0000644000175000017500000000777611076123133030613 0ustar zackzackGIF89a4polܵEEE999VVUŪ؉ǼʤჂ򠞙dcaRRQ]\\{zx???MLL444333!,4@pH,Ȥrl:Ш@Zجvzxl-z|~su2WfoTe`UXboef :7=Óʋ<Ϥ5)ݳ߼˿ڶܾՎ΢55=C!ѷm+M>o6dtu._ݘUn>؟3:XfC`[0/h uCP![ W d(]"Xr_ 9"#2dwHe.zآ0]Z}7L PeI8tO aiCà WI -*蠄ozXv@5 @d "^0e80ݪ&p"|Cnk, 06,w)>jjwiɂ< ɂ:p +[8th k(8,#h,,/U"CPCg Z6 滲˙2宐A:Ё|{J/ B ؁Pp ZP p MK C,( g l| !p'| T)Prª P t5:z|pBX*R3( D-,. A'G, @8 @A*H@>P ql+0 䯀t)T>P`&(0,=#@V`?-;h_S܃ŭqWuo @/S^&  gZUG >B^EZUmyUk-}T0]w~;ܙ9%pquxO0 h07]- K-[^`q< ]rCyPb6Gy$a>p(%%@$g- }ǀ;%y'?rgy<2Tk=>Ng~'@S2D1\4l#px(w\i׀eBxx42-%eT\?05Ph>(}`(&u$`x/"r(^@6†Vz#ehy)iVhKng(`NHz!wB.  gx%d؉mȆl\e-7p_}4z!<`P#K`h~v,0\e `74(;0H9Ȇ<8،<@1#@։T9l(؎8mȆ $ LC9'J(.`KP hs3`"#׎yS:Y: 92`X`Yh ن0i2i 0m20UTYX`FC3Xo:HXyLB7. QI׋ 0dYfy)B gpr9tYtȗ~)|Xr(RV؄ 2xMxu2yuWɈZə捔ق闫W)B8^hBy)iXyiZHz¨I py1|?P雿Iٙy Ti PTK#p:ZP`a)zzEɚ  p?2UĢâ-j-ꓮB2/:1:= pq)@9>)U.ZFKKʣ>:0ڢ,60%ʥ#$b1P3Хɥ]٦Ӷ>ijkm 0 `U+mJ  l*djjr*A7ॆj v DGiZ$b905- :J** jJ*KfK jm2x=ښrʬJšjZ ׮*:ʩk8گʮ#Gڊ{7P0hzʮ:zK = 4 y<2=7U, M9P1:˳> +@+;KE۳@;LۮO۴NS[L[P;\XO+W;efac[\K]kR Y;././@LongLink0000000000000000000000000000015400000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/_static/model_elixir_example.pypython-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/_static/model_elixir_exa0000644000175000017500000000651311113757071031732 0ustar zackzack""" Sample Elixir-powered model definition for the repoze.what SQL plugin. This model definition has been taken from a quickstarted TurboGears 2 project, but it's absolutely independent of TurboGears. """ import md5 import sha from datetime import datetime from sqlalchemy.orm import scoped_session, sessionmaker import elixir from elixir import Entity, Field from elixir import DateTime, Unicode from elixir import using_options from elixir import ManyToMany from tg import config DBSession = scoped_session(sessionmaker(autoflush=True, autocommit=False)) metadata = elixir.metadata elixir.session = DBSession class User(Entity): """Reasonably basic User definition. Probably would want additional attributes. """ using_options(tablename="user", auto_primarykey="user_id") user_name = Field(Unicode(16), required=True, unique=True) _password = Field(Unicode(40), colname="password", required=True) groups = ManyToMany( "Group", inverse="users", tablename="user_group", local_colname="group_id", remote_colname="user_id", ) def _set_password(self, password): """encrypts password on the fly""" self._password = self.__encrypt_password(password) def _get_password(self): """returns password""" return self._password password = descriptor=property(_get_password, _set_password) def __encrypt_password(self, password): """Hash the given password with SHA1. Edit this method to implement your own algorithm. """ hashed_password = password if isinstance(password, unicode): password_8bit = password.encode('UTF-8') else: password_8bit = password hashed_password = sha.new(password_8bit).hexdigest() # make sure the hased password is an UTF-8 object at the end of the # process because SQLAlchemy _wants_ a unicode object for Unicode columns if not isinstance(hashed_password, unicode): hashed_password = hashed_password.decode('UTF-8') return hashed_password def validate_password(self, password): """Check the password against existing credentials. this method _MUST_ return a boolean. @param password: the password that was provided by the user to try and authenticate. This is the clear text version that we will need to match against the (possibly) encrypted one in the database. @type password: unicode object """ return self.password == self.__encrypt_password(password) class Group(Entity): """An ultra-simple group definition.""" using_options(tablename="group", auto_primarykey="group_id") group_name = Field(Unicode(16), unique=True) display_name = Field(Unicode(255)) created = Field(DateTime, default=datetime.now) users = ManyToMany("User") permissions = ManyToMany( "Permission", inverse="groups", tablename="group_permission", local_colname="group_id", remote_colname="permission_id", ) class Permission(Entity): """A relationship that determines what each Group can do""" using_options(tablename="permission", auto_primarykey="permission_id") permission_name = Field(Unicode(16), unique=True) groups = ManyToMany("Group") python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/index.rst0000644000175000017500000000301411221405236026673 0ustar zackzack:mod:`repoze.who` SQLAlchemy plugin =================================== .. module:: repoze.who.plugins.sa :synopsis: SQLAlchemy/Elixir-based plugins for repoze.who .. moduleauthor:: Gustavo Narea :Author: Gustavo Narea. :Latest version: |release| .. topic:: Overview The :mod:`repoze.who` SQLAlchemy plugin provides an authenticator and a metadata provider plugins for SQLAlchemy or Elixir-based models. How to install ============== The minimum requirements :mod:`repoze.who` and SQLAlchemy and you can install it all by running:: easy_install repoze.who.plugins.sa The development mainline is available at the following Subversion repository:: http://svn.repoze.org/whoplugins/whoalchemy/trunk/ Authenticator ============= .. autoclass:: SQLAlchemyAuthenticatorPlugin .. autofunction:: make_sa_authenticator Metadata provider ================= .. autoclass:: SQLAlchemyUserMDPlugin .. autofunction:: make_sa_user_mdprovider Miscellaneous ============= .. autoclass:: SQLAlchemyUserChecker How to get help? ================ The prefered place to ask questions is the `Repoze mailing list `_ or the `#repoze `_ IRC channel. Bugs reports and feature requests should be sent to `the issue tracker of the Repoze project `_. Contents ======== .. toctree:: :maxdepth: 2 News Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/source/conf.py0000644000175000017500000001373411122514053026341 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who SA documentation build configuration file, created by # sphinx-quickstart on Mon Nov 10 20:27:30 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os here = os.path.dirname(os.path.abspath(__file__)) root = os.path.dirname(os.path.dirname(here)) # If setting up the auto(module|class) functionality: sys.path.append(os.path.abspath(root)) wd = os.getcwd() os.chdir(root) os.system('%s setup.py test -q' % sys.executable) os.chdir(wd) for item in os.listdir(root): if item.endswith('.egg'): sys.path.append(os.path.join(root, item)) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = u'repoze.who SQLAlchemy plugin' copyright = u'2008, The Repoze Project' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. release = open(os.path.join(root, 'VERSION.txt')).readline().rstrip() # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'repoze.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = '_static/logo_hi.gif' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'repozewhosadoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'repozewhopluginssa.tex', u'repoze.who SA Documentation', u'Gustavo Narea', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True intersphinx_mapping = {'http://static.repoze.org/whodocs/': None} python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/docs/Makefile0000644000175000017500000000431311106105442025173 0ustar zackzack# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html web pickle htmlhelp latex changes linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " pickle to make pickle files (usable by e.g. sphinx-web)" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf build/* html: mkdir -p build/html build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html @echo @echo "Build finished. The HTML pages are in build/html." pickle: mkdir -p build/pickle build/doctrees $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle @echo @echo "Build finished; now you can process the pickle files or run" @echo " sphinx-web build/pickle" @echo "to start the sphinx-web server." web: pickle htmlhelp: mkdir -p build/htmlhelp build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in build/htmlhelp." latex: mkdir -p build/latex build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex @echo @echo "Build finished; the LaTeX files are in build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p build/changes build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes @echo @echo "The overview file is in build/changes." linkcheck: mkdir -p build/linkcheck build/doctrees $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in build/linkcheck/output.txt." python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/setup.py0000644000175000017500000000441311221405553024322 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2007, Agendaless Consulting and Contributors. # Copyright (c) 2008, Florent Aide . # Copyright (c) 2008-2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.txt')).read() version = open(os.path.join(here, 'VERSION.txt')).readline().rstrip() setup(name='repoze.who.plugins.sa', version=version, description=('The repoze.who SQLAlchemy plugin'), long_description=README, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Database", "Topic :: Security", ], keywords='web application server wsgi sql sqlalchemy elixir ' \ 'authentication repoze', author="Gustavo Narea", author_email="repoze-dev@lists.repoze.org", namespace_packages=['repoze', 'repoze.who', 'repoze.who.plugins'], url="http://code.gustavonarea.net/repoze.who.plugins.sa/", license="BSD-derived (http://www.repoze.org/LICENSE.txt)", packages=find_packages(), include_package_data=True, zip_safe=False, tests_require=[ 'repoze.who >= 1.0.14', 'coverage', 'nose', 'sqlalchemy >= 0.5.0', 'elixir'], install_requires=['repoze.who >= 1.0.14', 'sqlalchemy >= 0.5.0'], test_suite="nose.collector", entry_points = """\ """ ) python-repoze.who-plugins-20090913/repoze.who.plugins.sa-1.0rc2/setup.cfg0000644000175000017500000000053011221406122024416 0ustar zackzack[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [nosetests] cover-erase = 1 verbose = 1 cover-package = repoze.who.plugins.sa verbosity = 1 with-coverage = 1 detailed-errors = 1 no-path-adjustment = 1 testmatch = ^(tests|test_.*)$ with-doctest = 1 where = tests [aliases] release = egg_info -rDb "" sdist bdist_egg register upload python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/0000755000175000017500000000000011154767027022412 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/README.txt0000644000175000017500000000122411153042576024101 0ustar zackzack************************************************** Test utilities for repoze.who-powered applications ************************************************** repoze.who-testutil is a repoze.who plugin which modifies repoze.who's original middleware to make it easier to forge authentication, without bypassing identification (this is, running the metadata providers). It's been created to ease testing of repoze.who-powered applications, in a way independent of the identifiers, authenticators and challengers used originally by your application, so that you won't have to update your test suite as your application grows and the authentication method changes. python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze/0000755000175000017500000000000011154767027023716 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze/__init__.py0000644000175000017500000000170111146557515026026 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """The ``repoze`` namespace""" # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze/who/0000755000175000017500000000000011154767027024513 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze/who/__init__.py0000644000175000017500000000170511146557515026627 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """The ``repoze.who`` namespace""" # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze/who/plugins/0000755000175000017500000000000011154767027026174 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze/who/plugins/testutil.py0000644000175000017500000002157111154765677030442 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """Test utilities for repoze.who-powered applications.""" import sys from logging import INFO from re import compile as compile_regex from zope.interface import implements from paste.httpexceptions import HTTPUnauthorized from paste.deploy.converters import asbool from repoze.who.middleware import PluggableAuthenticationMiddleware from repoze.who.config import WhoConfig, \ make_middleware_with_config as mk_mw_cfg from repoze.who.interfaces import IIdentifier, IAuthenticator, IChallenger __all__ = ['AuthenticationForgerPlugin', 'AuthenticationForgerMiddleware', 'make_middleware', 'make_middleware_with_config'] _HTTP_STATUS_PATTERN = compile_regex(r'^(?P[0-9]{3}) (?P.*)$') class AuthenticationForgerPlugin(object): """ :mod:`repoze.who` plugin to forge authentication easily and bypass :mod:`repoze.who` challenges. This plugin enables you to write identifier and challenger-independent tests. As a result, your protected areas will be easier to test: #. To forge authentication, without bypassing identification (i.e., running MD providers), you can use the following WebTest-powered test:: def test_authorization_granted(self): '''The right subject must get what she requested''' environ = {'REMOTE_USER': 'manager'} resp = self.app.get('/admin/', extra_environ=environ, status=200) assert 'some text' in resp.body As you can see, this is an identifier-independent way to forge authentication. #. To check that authorization was denied, in a challenger-independent way, you can use:: def test_authorization_denied_anonymous(self): '''Anonymous users must get a 401 page''' self.app.get('/admin/', status=401) def test_authorization_denied_authenticated(self): '''Authenticated users must get a 403 page''' environ = {'REMOTE_USER': 'editor'} self.app.get('/admin/', extra_environ=environ, status=403) """ implements(IIdentifier, IAuthenticator, IChallenger) def __init__(self, fake_user_key='REMOTE_USER', remote_user_key='repoze.who.testutil.userid'): """ :param fake_user_key: The key for the item in the ``environ`` which will contain the forged user Id. :type fake_user_key: str :param remote_user_key: The actual "external" ``remote_user_key`` used by :mod:`repoze.who`. :type remote_user_key: str """ self.fake_user_key = fake_user_key self.remote_user_key = remote_user_key # IIdentifier def identify(self, environ): """ Pre-authenticate using the user Id found in the relevant ``environ`` item, if any. The user Id. found will be put into ``identity['fake-userid']``, for :meth:`authenticate`. """ if self.fake_user_key in environ: identity = {'fake-userid': environ[self.fake_user_key]} return identity # IIdentifier def remember(self, environ, identity): """Do nothing""" pass # IIdentifier def forget(self, environ, identity): """Do nothing""" pass # IAuthenticator def authenticate(self, environ, identity): """ Turn the value in ``identity['fake-userid']`` into the remote user's name. Finally, it removes ``identity['fake-userid']`` so that it won't reach the WSGI application. """ if 'fake-userid' in identity: environ[self.remote_user_key] = identity.pop('fake-userid') return environ[self.remote_user_key] # IChallenger def challenge(self, environ, status, app_headers, forget_headers): """Return a 401 page unconditionally.""" headers = app_headers + forget_headers # The HTTP status code and reason may not be the default ones: status_parts = _HTTP_STATUS_PATTERN.search(status) if status_parts: reason = status_parts.group('reason') code = int(status_parts.group('code')) else: reason = 'HTTP Unauthorized' code = 401 # Building the response: response = HTTPUnauthorized(headers=headers) response.title = reason response.code = code return response class AuthenticationForgerMiddleware(PluggableAuthenticationMiddleware): """ :class:`PluggableAuthenticationMiddleware ` proxy to forge authentication, without bypassing identification. """ def __init__(self, app, identifiers, authenticators, challengers, mdproviders, classifier, challenge_decider, log_stream=None, log_level=INFO, remote_user_key='REMOTE_USER'): """ Setup authentication in an easy to forge way. All the arguments received will be passed as is to :class:`repoze.who.middleware.PluggableAuthenticationMiddleware`, with one instance of :class:`AuthenticationForgerPlugin` in: * ``identifiers``. This instance will be inserted in the first position of the list. * ``authenticators``. Any authenticator passed will be ignored; such an instance will be the only authenticator defined. * ``challengers``. Any challenger passed will be ignored; such an instance will be the only challenger defined. Internally, it will also set ``remote_user_key`` to ``'repoze.who.testutil.userid'``, so that you can use the standard ``'REMOTE_USER'`` in your tests. The metadata providers won't be modified. """ self.actual_remote_user_key = remote_user_key forger = AuthenticationForgerPlugin(fake_user_key=remote_user_key) forger = ('auth_forger', forger) identifiers.insert(0, forger) authenticators = [forger] challengers = [forger] # Calling the parent's constructor: init = super(AuthenticationForgerMiddleware, self).__init__ init(app, identifiers, authenticators, challengers, mdproviders, classifier, challenge_decider, log_stream, log_level, 'repoze.who.testutil.userid') #{ Middleware makers: def make_middleware(skip_authentication=False, *args, **kwargs): """ Return the requested authentication middleware. :param skip_authentication: If ``True``, an instance of :class:`AuthenticationForgerMiddleware` will be returned instead of :class:`repoze.who.middleware.PluggableAuthenticationMiddleware` :type skip_authentication: bool ``args`` and ``kwargs`` are the positional and named arguments, respectively, to be passed to the relevant authentication middleware. """ if asbool(skip_authentication): # We must replace the middleware: return AuthenticationForgerMiddleware(*args, **kwargs) else: return PluggableAuthenticationMiddleware(*args, **kwargs) def make_middleware_with_config(app, global_conf, config_file, log_file=None, log_level=None, skip_authentication=False): """ Proxy :func:`repoze.who.config.make_middleware_with_config` to skip authentication when required. If ``skip_authentication`` evaluates to ``True``, then the returned middleware will be an instance of :class:`AuthenticationForgerMiddleware`. """ if not asbool(skip_authentication): # We must not replace the middleware return mk_mw_cfg(app, global_conf, config_file, log_file, log_level) # We must replace the middleware: parser = WhoConfig(global_conf['here']) parser.parse(open(config_file)) return AuthenticationForgerMiddleware( app, parser.identifiers, parser.authenticators, parser.challengers, parser.mdproviders, parser.request_classifier, parser.challenge_decider, remote_user_key=parser.remote_user_key, ) #} python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze/who/plugins/__init__.py0000644000175000017500000000171511146557515030311 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """The ``repoze.who.plugins`` namespace""" # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/PKG-INFO0000644000175000017500000000273311154767027023514 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who-testutil Version: 1.0rc1 Summary: Test utilities for repoze.who-powered applications Home-page: http://code.gustavonarea.net/repoze.who-testutil/ Author: Gustavo Narea Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: ************************************************** Test utilities for repoze.who-powered applications ************************************************** repoze.who-testutil is a repoze.who plugin which modifies repoze.who's original middleware to make it easier to forge authentication, without bypassing identification (this is, running the metadata providers). It's been created to ease testing of repoze.who-powered applications, in a way independent of the identifiers, authenticators and challengers used originally by your application, so that you won't have to update your test suite as your application grows and the authentication method changes. Keywords: web application wsgi authentication testing tests repoze Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Security python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/VERSION.txt0000644000175000017500000000000611154766546024301 0ustar zackzack1.0rc1python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/ez_setup.py0000644000175000017500000002231311146557515024623 0ustar zackzack#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c8" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', } import sys, os def _validate_md5(egg_name, data): if egg_name in md5_data: from md5 import md5 digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re from md5 import md5 for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/0000755000175000017500000000000011154767027030141 5ustar zackzack././@LongLink0000000000000000000000000000015000000000000011561 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/requires.txtpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/requires.0000644000175000017500000000010111154767027031771 0ustar zackzackrepoze.who >= 1.0 zope.interface Paste > 1.7 PasteDeploy >= 1.3.3././@LongLink0000000000000000000000000000016200000000000011564 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/namespace_packages.txtpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/namespace0000644000175000017500000000004511154767027032017 0ustar zackzackrepoze repoze.who repoze.who.plugins ././@LongLink0000000000000000000000000000016000000000000011562 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/dependency_links.txtpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/dependenc0000644000175000017500000000000111154767027032000 0ustar zackzack python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/PKG-INFO0000644000175000017500000000273311154767027031243 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who-testutil Version: 1.0rc1 Summary: Test utilities for repoze.who-powered applications Home-page: http://code.gustavonarea.net/repoze.who-testutil/ Author: Gustavo Narea Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: ************************************************** Test utilities for repoze.who-powered applications ************************************************** repoze.who-testutil is a repoze.who plugin which modifies repoze.who's original middleware to make it easier to forge authentication, without bypassing identification (this is, running the metadata providers). It's been created to ease testing of repoze.who-powered applications, in a way independent of the identifiers, authenticators and challengers used originally by your application, so that you won't have to update your test suite as your application grows and the authentication method changes. Keywords: web application wsgi authentication testing tests repoze Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Security ././@LongLink0000000000000000000000000000015400000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/entry_points.txtpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/entry_poi0000644000175000017500000000000611154767027032070 0ustar zackzack ././@LongLink0000000000000000000000000000015100000000000011562 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/top_level.txtpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/top_level0000644000175000017500000000001511154767027032051 0ustar zackzackrepoze tests ././@LongLink0000000000000000000000000000015000000000000011561 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/not-zip-safepython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/not-zip-s0000644000175000017500000000000111151053625031700 0ustar zackzack ././@LongLink0000000000000000000000000000014700000000000011567 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/SOURCES.txtpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/repoze.who_testutil.egg-info/SOURCES.t0000644000175000017500000000154611154767027031457 0ustar zackzackMANIFEST.in README.txt VERSION.txt ez_setup.py setup.cfg setup.py docs/Makefile docs/source/API.rst docs/source/News.rst docs/source/conf.py docs/source/index.rst docs/source/HowTo/Reconfiguring.rst docs/source/HowTo/TestingAuthentication.rst docs/source/HowTo/TestingProtectedAreas.rst docs/source/HowTo/index.rst docs/source/_static/logo_hi.gif docs/source/_static/repoze.css docs/source/_static/sample-who.ini repoze/__init__.py repoze.who_testutil.egg-info/PKG-INFO repoze.who_testutil.egg-info/SOURCES.txt repoze.who_testutil.egg-info/dependency_links.txt repoze.who_testutil.egg-info/entry_points.txt repoze.who_testutil.egg-info/namespace_packages.txt repoze.who_testutil.egg-info/not-zip-safe repoze.who_testutil.egg-info/requires.txt repoze.who_testutil.egg-info/top_level.txt repoze/who/__init__.py repoze/who/plugins/__init__.py repoze/who/plugins/testutil.pypython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/0000755000175000017500000000000011154767027023342 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/0000755000175000017500000000000011154767027024642 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/News.rst0000644000175000017500000000234311154766343026312 0ustar zackzack******************************** **repoze.who-testutil** releases ******************************** This document describes the releases of :mod:`repoze.who-testutil`. .. _1.0rc1: **repoze.who-testutil** 1.0rc1 (2009-03-08) =========================================== * :class:`repoze.who.plugins.testutil.AuthenticationForgerPlugin` ignored the original response headers on challenge. .. _1.0b2: **repoze.who-testutil** 1.0b2 (2009-03-02) ========================================== * Specified the required version of :mod:`repoze.who`, otherwise the buggy setuptools won't install it. .. _1.0b1: **repoze.who-testutil** 1.0b1 (2009-02-27) ========================================== This is the first release of **repoze.who-testutil**, which ships the following components: * :class:`repoze.who.plugins.testutil.AuthenticationForgerPlugin`, a :mod:`repoze.who` plugin which acts as identifier, authenticator and challenger. * :class:`repoze.who.plugins.testutil.AuthenticationForgerMiddleware`, a proxy to :class:`repoze.who.middleware.PluggableAuthenticationMiddleware` to forge authentication easily. * :func:`repoze.who.plugins.testutil.make_middleware`. * :func:`repoze.who.plugins.testutil.make_middleware_with_config`. python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/HowTo/0000755000175000017500000000000011154767027025702 5ustar zackzack././@LongLink0000000000000000000000000000015200000000000011563 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/HowTo/TestingAuthentication.rstpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/HowTo/TestingAuthenticatio0000644000175000017500000001260411152035707031756 0ustar zackzack********************************* Test authentication independently ********************************* You may want to test authentication in order to make sure :mod:`repoze.who` is working properly within your application, which is also known as "integration tests". This section will help you test authentication *only*, based on the ``test.ini`` file defined in the previous section. The test case ============= In most situations, a single test case will be enough to test authentication exhaustively. While testing authentication, you have to check the behavior of your application in the following situations: * When authorization is denied with the 401 HTTP status code: :mod:`repoze.who` must catch it and run the challenger(s) you specified. This is, when the user is forced to log in. * When the user logs in voluntarily. * When the user logs out. So, your test case for authentication should be made up of at least three tests. .setUp() -------- The ``.setUp()`` method of this test case should perform the traditional procedure to test applications with WebTest: .. code-block:: python :linenos: from unittest import TestCase from paste.deploy import loadapp from webtest import TestApp # Set the path to your configuration directory here: conf_dir = '/path/to/configuration/dir' class TestAuthentication(TestCase): """Test case for the authentication sub-system in the application""" def setUp(self): appconfig = loadapp('config:test.ini', relative_to=conf_dir) self.app = TestApp(appconfig) # (...) Note that we're using the ``main`` application, which doesn't skip authentication. This way, authentication will behave the same way as in a production environment. Sample test case ================ Say we have the following :mod:`repoze.who` configuration file: .. literalinclude:: ../_static/sample-who.ini :language: ini The following test case illustrates how you could test authentication for that setup:: from unittest import TestCase from paste.deploy import loadapp from webtest import TestApp # Set the path to your configuration directory here: conf_dir = '/path/to/configuration/dir' class TestAuthentication(TestCase): """Test case for the authentication sub-system in the application""" def setUp(self): wsgiapp = loadapp('config:test.ini', relative_to=conf_dir) self.app = TestApp(wsgiapp) def test_forced_login(self): """ Anonymous users should be redirected to the login form when they request a protected area. """ # Requesting a protected area as anonymous: resp = self.app.get('/panel/', status=302) assert resp.location.startswith('http://localhost/login?') # Being redirected to the login page: login_page = resp.follow(status=200) login_form = login_page.form login_form['login'] = 'gustavo' login_form['password'] = 'hola' # Submitting the login form: login_handler = login_form.submit(status=302) # We should be redirected to the initially requested page: assert login_handler.location == 'http://localhost/panel/' # Checking that the user was correctly authenticated: initial_page = login_handler.follow(status=200) assert 'auth_tkt' in initial_page.request.cookies, \ "Session cookie wasn't defined: %s" % initial_page.request.cookies def test_voluntary_login(self): """Voluntary logins should work perfectly""" # Requesting the login form: login_page = self.app.get('/login', status=200) login_form = login_page.form login_form['login'] = 'gustavo' login_form['password'] = 'hola' # Submitting the login form: login_handler = login_form.submit(status=302) assert login_handler.location == 'http://localhost/' # Checking that the user was correctly authenticated: initial_page = login_handler.follow(status=200) assert 'auth_tkt' in initial_page.request.cookies, \ "Session cookie wasn't defined: %s" % initial_page.request.cookies def test_logout(self): """Users should be logged out correctly""" # Logging in: self.app.get('/login_handler?login=gustavo&password=hola', status=302) # Checking that the user was correctly authenticated: home_page = self.app.get('/', status=200) assert 'auth_tkt' in home_page.request.cookies, \ "Session cookie wasn't defined: %s" % home_page.request.cookies # Now let's log out: self.app.get('/logout_handler', status=302) # Finally, let's check that the session cookie was destroyed after logout: home_page = self.app.get('/', status=200) assert home_page.request.cookies.get('auth_tkt') == '', \ "Session cookie wasn't deleted: %s" % home_page.request.cookies Depending on your :mod:`repoze.who` plugins and the way you use them, you may need more tests. In our example, it also makes sense to test what happens when the user tries to log in with the wrong credentials. python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/HowTo/index.rst0000644000175000017500000000203411151535505027531 0ustar zackzack**************************************** How to test protected areas in Web sites **************************************** .. topic:: Overview This guide will show you how to test protected areas and authentication, separately, using `PasteDeploy `_, `WebTest `_ and :mod:`repoze.who.plugins.testutil`. **I assume that you already have repoze.who working the way you want**, configured via an Ini file or Python code. If not, please set it up first using the :mod:`repoze.who` manual and then come back to this guide. Likewise, I also assume that you already have a `PasteDeploy configuration file `_ for your application. It's not strictly necessary, but we're going to use it in this HOWTO. Three steps are required to test protected areas and authentication separately: .. toctree:: :maxdepth: 2 Reconfiguring TestingProtectedAreas TestingAuthentication python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/HowTo/Reconfiguring.rst0000644000175000017500000000561711152034510031224 0ustar zackzack***************************** Reconfigure :mod:`repoze.who` ***************************** Now that you have :mod:`repoze.who` working, it's time to change its setup slightly in order for us to skip authentication while testing protected areas. The way to change it depends on the way you configure :mod:`repoze.who`: Via Python code =============== If the PasteDeploy factory for your application looks like this:: from repoze.who.middleware import PluggableAuthenticationMiddleware def make_application(global_config, **local_conf): # (...) app = PluggableAuthenticationMiddleware( app, identifiers, authenticators, challengers, mdproviders, classifier, challenge_decider) # (...) return app You should replace :class:`repoze.who.middleware.PluggableAuthenticationMiddleware` with :func:`repoze.who.plugins.testutil.make_middleware`:: from repoze.who.plugins.testutil import make_middleware def make_application(global_config, **local_conf): # (...) app = make_middleware( local_conf.get('skip_authentication'), app, identifiers, authenticators, challengers, mdproviders, classifier, challenge_decider) # (...) return app .. attention:: Note that :func:`repoze.who.plugins.testutil.make_middleware` receives one more argument, before ``app``. Via :mod:`repoze.what` ---------------------- TODO Via .ini file ============= If the PasteDeploy factory for your application looks like this:: from repoze.who.config import make_middleware_with_config def make_application(global_config, **local_conf): # (...) app = make_middleware_with_config( app, global_conf, local_conf['who.config_file'], local_conf['who.log_file'], local_conf['who.log_level']) # (...) return app You should replace :class:`repoze.who.config.make_middleware_with_config` with :func:`repoze.who.plugins.testutil.make_middleware_with_config`:: from repoze.who.plugins.testutil import make_middleware_with_config def make_application(global_config, **local_conf): # (...) app = make_middleware_with_config( app, global_conf, local_conf['who.config_file'], local_conf['who.log_file'], local_conf['who.log_level'], skip_authentication=local_conf.get('skip_authentication')) # (...) return app .. attention:: Note that :func:`repoze.who.plugins.testutil.make_middleware_with_config` receives one more argument: ``skip_authentication``. -------------- .. tip:: You may want to run your application now to check that, so far, nothing seems to have changed. ././@LongLink0000000000000000000000000000015200000000000011563 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/HowTo/TestingProtectedAreas.rstpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/HowTo/TestingProtectedArea0000644000175000017500000001010311152035245031664 0ustar zackzack******************************************* Test protected areas without authentication ******************************************* Once your authentication middleware is prepared to skip authentication when explicitly requested, as described in the previous section, we'll be ready to test the protected areas independently of the :mod:`repoze.who` identifiers, authenticators and challengers used. The test configuration ====================== If you don't already have a test config file (often called ``test.ini``), then you should create one. It's not strictly necessary, but we're going to use it in this HOWTO. In your ``test.ini`` file (or whatever you call it), let's define a new application called ``main_without_authn``, which will be the same ``main`` application but without authentication: .. code-block:: ini [DEFAULT] # (...) # Your main application: [app:main] use = config:main_configuration.ini # Your main application without authentication: [app:main_without_authn] use = main skip_authentication = True The base test case ================== Next, it's time to create a base class for your test cases, which will set your application up without authentication (using the test configuration file we created above). If you've ever created one, you'll notice this one is a little special (hint: see line #12): .. code-block:: python :linenos: from unittest import TestCase from paste.deploy import loadapp from webtest import TestApp # Set the path to your configuration directory here: conf_dir = '/path/to/configuration/dir' class TestProtectedAreas(TestCase): def setUp(self): wsgiapp = loadapp('config:test.ini#main_without_authn', relative_to=conf_dir) self.app = TestApp(wsgiapp) Say it's defined in ``yourapplication.tests.base``. A sample test case ================== Finally we're ready to create our first test case for a protected area! And all you have to do is extend ``TestProtectedAreas`` and test your application as you'd expect: .. code-block:: python :linenos: from yourapplication.tests.base import TestProtectedAreas class TestControlPanel(TestProtectedAreas): """Test case for the control panel at ``/panel``""" def test_index_as_admin(self): """Administrators can access the panel""" environ = {'REMOTE_USER': 'admin'} resp = self.app.get('/panel/', extra_environ=environ, status=200) assert "some text" in resp.body def test_index_as_normal_user(self): """Regular users shouldn't access the panel""" environ = {'REMOTE_USER': 'foobar'} self.app.get('/panel/', extra_environ=environ, status=403) def test_index_as_anonymous(self): """Anonymous users must not access the panel""" self.app.get('/panel/', status=401) Now some comments about the test case above: #. Every time you need to forge authentication, you should do it the standard way: Setting the user name in ``environ['REMOTE_USER']`` (or whatever you use) and then pass the fake environment to ``webtest.TestApp`` instance when you make a request. See lines 8-9 and 14-15. #. If you want to act as an anonymous user, don't set ``environ['REMOTE_USER']``. See line 19. #. It's highly recommended to set the HTTP status code you expect to get when you make a request. See lines 9, 15 and 19. Keep in mind the meaning of the 200, 401 and 403 HTTP status codes: * 200: Authorization was granted and the request was processed with no problems at all. * 401: Authorization was denied, but authenticating *could* help to gain access. This is used when the user is anonymous. * 403: Authorization was denied and authentication *won't* help to gain access. This is mostly used when the user is *not* anonymous. * When authorization to a given resource is denied to *everybody* (anonymous or authenticated), the 403 HTTP status code must be used. python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/API.rst0000644000175000017500000000122511153042575025776 0ustar zackzack*************************** **repoze.who-testutil** API *************************** .. module:: repoze.who.plugins.testutil :synopsis: Test utilities for repoze.who-powered applications .. moduleauthor:: Gustavo Narea Authentication middleware ========================= .. autoclass:: AuthenticationForgerMiddleware :members: __init__ Middleware makers ----------------- .. autofunction:: make_middleware .. autofunction:: make_middleware_with_config :mod:`repoze.who` plugins ========================= .. autoclass:: AuthenticationForgerPlugin :members: __init__, identify, remember, forget, authenticate, challenge python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/_static/0000755000175000017500000000000011154767027026270 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/_static/sample-who.ini0000644000175000017500000000135511151564674031051 0ustar zackzack[plugin:form] use = repoze.who.plugins.form:make_redirecting_plugin login_form_url = /login login_handler_path = /login_handler logout_handler_path = /logout_handler rememberer_name = auth_tkt [plugin:auth_tkt] use = repoze.who.plugins.auth_tkt:make_plugin secret = something [plugin:htpasswd_authenticator] use = repoze.who.plugins.htpasswd:make_plugin filename = %(here)s/users.htpasswd check_fn = repoze.who.plugins.htpasswd:crypt_check [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider [identifiers] plugins = form;browser auth_tkt [authenticators] plugins = htpasswd_authenticator [challengers] plugins = form;browser python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/_static/repoze.css0000644000175000017500000000047411146562512030304 0ustar zackzack@import url('default.css'); body { background-color: #006339; } div.document { background-color: #dad3bd; } div.sphinxsidebar h3,h4,h5,li,a { color: #127c56 !important; } div.related { color: #dad3bd !important; background-color: #00744a; } div.related a { color: #dad3bd !important; } python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/_static/logo_hi.gif0000644000175000017500000000777611146562512030411 0ustar zackzackGIF89a4polܵEEE999VVUŪ؉ǼʤჂ򠞙dcaRRQ]\\{zx???MLL444333!,4@pH,Ȥrl:Ш@Zجvzxl-z|~su2WfoTe`UXboef :7=Óʋ<Ϥ5)ݳ߼˿ڶܾՎ΢55=C!ѷm+M>o6dtu._ݘUn>؟3:XfC`[0/h uCP![ W d(]"Xr_ 9"#2dwHe.zآ0]Z}7L PeI8tO aiCà WI -*蠄ozXv@5 @d "^0e80ݪ&p"|Cnk, 06,w)>jjwiɂ< ɂ:p +[8th k(8,#h,,/U"CPCg Z6 滲˙2宐A:Ё|{J/ B ؁Pp ZP p MK C,( g l| !p'| T)Prª P t5:z|pBX*R3( D-,. A'G, @8 @A*H@>P ql+0 䯀t)T>P`&(0,=#@V`?-;h_S܃ŭqWuo @/S^&  gZUG >B^EZUmyUk-}T0]w~;ܙ9%pquxO0 h07]- K-[^`q< ]rCyPb6Gy$a>p(%%@$g- }ǀ;%y'?rgy<2Tk=>Ng~'@S2D1\4l#px(w\i׀eBxx42-%eT\?05Ph>(}`(&u$`x/"r(^@6†Vz#ehy)iVhKng(`NHz!wB.  gx%d؉mȆl\e-7p_}4z!<`P#K`h~v,0\e `74(;0H9Ȇ<8،<@1#@։T9l(؎8mȆ $ LC9'J(.`KP hs3`"#׎yS:Y: 92`X`Yh ن0i2i 0m20UTYX`FC3Xo:HXyLB7. QI׋ 0dYfy)B gpr9tYtȗ~)|Xr(RV؄ 2xMxu2yuWɈZə捔ق闫W)B8^hBy)iXyiZHz¨I py1|?P雿Iٙy Ti PTK#p:ZP`a)zzEɚ  p?2UĢâ-j-ꓮB2/:1:= pq)@9>)U.ZFKKʣ>:0ڢ,60%ʥ#$b1P3Хɥ]٦Ӷ>ijkm 0 `U+mJ  l*djjr*A7ॆj v DGiZ$b905- :J** jJ*KfK jm2x=ښrʬJšjZ ׮*:ʩk8گʮ#Gڊ{7P0hzʮ:zK = 4 y<2=7U, M9P1:˳> +@+;KE۳@;LۮO۴NS[L[P;\XO+W;efac[\K]kR Y;python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/index.rst0000644000175000017500000001557411153042576026511 0ustar zackzack********************************************************* Test utilities for :mod:`repoze.who`-powered applications ********************************************************* :Author: Gustavo Narea. :Latest release: |release| .. topic:: Overview **repoze.who-testutil** is a :mod:`repoze.who` plugin which modifies :mod:`repoze.who`'s original middleware to make it easier to forge authentication, without bypassing identification (this is, running the metadata providers). It's been created in order to ease testing of :mod:`repoze.who`-powered applications, in a way independent of the identifiers, authenticators and challengers used originally by your application, so that you won't have to update your test suite as your application grows and the authentication method changes. The problems ============ While testing protected areas, you have to authenticate first ------------------------------------------------------------- And that's absolutely specific to the identifiers/challengers you're using. For example, if you're using `WebTest `_ and the :class:`repoze.who.plugins.form.RedirectingFormPlugin` plugin, you have to get the login handler at the beginning of each test that covers situations where the user is authenticated:: class TestControlPanel(TestCase): def setUp(self): from paste.deploy import loadapp from webtest import TestApp wsgiapp = loadapp('config:test.ini') self.app = TestApp(wsgiapp) def test_index_as_admin(self): # First of all, let's log in the RedirectingFormPlugin way: self.app.get('/login_handler?login=admin&password=somepass') # Now that we're authenticated, let's request the control panel: resp = self.app.get('/panel/', status=200) assert "some text" in resp.body def test_index_as_normal_user(self): # First of all, let's log in the RedirectingFormPlugin way: self.app.get('/login_handler?login=foo&password=bar') # Now that we're authenticated, let's request the control panel: self.app.get('/panel/', status=302) # We got a 302 redirection. This is the RedirectingFormPlugin way # to let us know that authorization was denied. def test_index_as_anonymous(self): # Let's request the control panel as an anonymous user: self.app.get('/panel/', status=302) # We got a 302 redirection. This is the RedirectingFormPlugin way # to let us know that authorization was denied. This is, we end up testing protected areas in a way that is totally tied to the :mod:`repoze.who` identifiers and challengers you intend to use initially. If the are replaced later, you will have to update many of your tests (*most* of them, possibly). Or, while testing protected areas, you have to forge authentication ------------------------------------------------------------------- But that will bypass identification: The metadata providers won't get run, so this is not an option if your application relies on them:: class TestControlPanel(TestCase): def setUp(self): from paste.deploy import loadapp from webtest import TestApp wsgiapp = loadapp('config:test.ini') self.app = TestApp(wsgiapp) def test_index_as_admin(self): # Let's forge authentication: environ = {'REMOTE_USER': 'admin'} resp = self.app.get('/panel/', extra_environ=environ, status=200) assert "some text" in resp.body This seems like the best way to test a protected area, and you may expect it to work, but unfortunately it won't: If the controller action for ``'/panel/'`` assumes that if the user is authenticated, her full name will be available in ``identity['full_name']``, then your test will be broken because such an item won't be defined in the ``identity`` dict -- even worse: the ``identity`` dict won't even be defined because :mod:`repoze.who` will assume that, because ``environ['REMOTE_USER']`` is set, it won't be necessary to run its middleware. The solution ============ It's absolutely unnecessary to test authentication every time you test a protected area in your Web site; authentication should be tested *once* and *separately*. This is, to test a protected area in a Web site, only *identification* and *authorization* are required, not authentication. With **repoze.who-testutil**, you'll be able to write tests for protected areas the way you'd expect:: class TestControlPanel(TestCase): def setUp(self): from paste.deploy import loadapp from webtest import TestApp wsgiapp = loadapp('config:test.ini') self.app = TestApp(wsgiapp) def test_index_as_admin(self): environ = {'REMOTE_USER': 'admin'} resp = self.app.get('/panel/', extra_environ=environ, status=200) assert "some text" in resp.body def test_index_as_normal_user(self): environ = {'REMOTE_USER': 'foobar'} # The 403 HTTP status code means that authorization has been # denied, while we are aware of who the user is: self.app.get('/panel/', status=403) def test_index_as_anonymous(self): # Let's request the control panel as an anonymous user. # The 401 HTTP status code means that authorization has been # denied, although it may be granted if the user logs in: self.app.get('/panel/', status=401) As you may have noticed, these tests are absolutely independent of the :mod:`repoze.who` plugins used. And the best of all: The :mod:`repoze.who` middleware won't be skipped, so the ``identity`` dict will be defined as usual! Then if you want to test authentication, you can do it once and separately -- and if the authentication method changes over time, you'd just have to update a few tests, instead of all the tests that cover protected areas. How to install ============== It requires :mod:`repoze.who` only, and you can install them with ``easy_install``:: easy_install repoze.who-testutil Documentation ============= .. toctree:: :maxdepth: 2 HowTo/index API Support and development ======================= The prefered place to ask questions is the `Repoze mailing list `_ or the `#repoze `_ IRC channel. Bugs reports and feature requests should be sent to `the issue tracker of the Repoze project `_. The development mainline is available at the following Subversion repository:: http://svn.repoze.org/whoplugins/whotestutil/trunk/ Releases -------- .. toctree:: :maxdepth: 2 News python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/source/conf.py0000644000175000017500000001355511151540730026135 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.what documentation build configuration file, created by # sphinx-quickstart on Mon Nov 10 20:27:30 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os here = os.path.dirname(os.path.abspath(__file__)) root = os.path.dirname(os.path.dirname(here)) # If setting up the auto(module|class) functionality: sys.path.append(os.path.abspath(root)) wd = os.getcwd() os.chdir(root) os.system('%s setup.py test -q' % sys.executable) os.chdir(wd) for item in os.listdir(root): if item.endswith('.egg'): sys.path.append(os.path.join(root, item)) # General configuration # --------------------- extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = u'repoze.who Test Utilities' copyright = u'2009, The Repoze Project' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. release = open(os.path.join(root, 'VERSION.txt')).readline().rstrip() # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'repoze.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = '_static/logo_hi.gif' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'repozewhotestutildoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'repozewhotestutil.tex', u'Test utilities for repoze.who-powered applications', u'Gustavo Narea', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True intersphinx_mapping = { 'http://static.repoze.org/whodocs/': None, } python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/docs/Makefile0000644000175000017500000000431311146562512024774 0ustar zackzack# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html web pickle htmlhelp latex changes linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " pickle to make pickle files (usable by e.g. sphinx-web)" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf build/* html: mkdir -p build/html build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html @echo @echo "Build finished. The HTML pages are in build/html." pickle: mkdir -p build/pickle build/doctrees $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle @echo @echo "Build finished; now you can process the pickle files or run" @echo " sphinx-web build/pickle" @echo "to start the sphinx-web server." web: pickle htmlhelp: mkdir -p build/htmlhelp build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in build/htmlhelp." latex: mkdir -p build/latex build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex @echo @echo "Build finished; the LaTeX files are in build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p build/changes build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes @echo @echo "The overview file is in build/changes." linkcheck: mkdir -p build/linkcheck build/doctrees $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in build/linkcheck/output.txt." python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/MANIFEST.in0000644000175000017500000000030711146557515024150 0ustar zackzackinclude README.txt include VERSION.txt include MANIFEST.in recursive-include docs * prune docs/build recursive-include repoze * recursive-exclude tests * global-exclude *~ *.pyc *.egg .directory python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/setup.py0000644000175000017500000000431711153044402024111 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## import os from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.txt')).read() version = open(os.path.join(here, 'VERSION.txt')).readline().rstrip() setup(name='repoze.who-testutil', version=version, description=('Test utilities for repoze.who-powered applications'), long_description=README, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Security" ], keywords='web application wsgi authentication testing tests repoze', author='Gustavo Narea', author_email='repoze-dev@lists.repoze.org', namespace_packages = ['repoze', 'repoze.who', 'repoze.who.plugins'], url='http://code.gustavonarea.net/repoze.who-testutil/', license='BSD-derived (http://www.repoze.org/LICENSE.txt)', packages=find_packages(), include_package_data=True, zip_safe=False, tests_require=['repoze.who >= 1.0', 'coverage', 'nose'], install_requires=[ 'repoze.who >= 1.0', 'zope.interface', 'Paste > 1.7', # Workaround for the buggy setuptools 'PasteDeploy >= 1.3.3'], test_suite='nose.collector', entry_points = """\ """ ) python-repoze.who-plugins-20090913/repoze.who-testutil-1.0rc1/setup.cfg0000644000175000017500000000052011154767027024230 0ustar zackzack[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [nosetests] cover-erase = 1 verbose = 1 cover-package = repoze.who.plugins.testutil verbosity = 1 with-coverage = 1 detailed-errors = 1 no-path-adjustment = 1 testmatch = ^(tests|test_.*)$ with-doctest = 1 [aliases] release = egg_info -rDb "" sdist bdist_egg register upload python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/0000755000175000017500000000000011157260664023070 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/README.txt0000644000175000017500000000055511157260621024564 0ustar zackzackIntroduction ============ ``repoze.who.plugins.openid`` is a plugin for the `repoze.who framework `_ enabling `OpenID `_ logins. For more information read the `documentation `_ or check out the `source code `_. ././@LongLink0000000000000000000000000000014500000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000755000175000017500000000000011157260664031617 5ustar zackzack././@LongLink0000000000000000000000000000016100000000000011563 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/requires.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000644000175000017500000000007611157260664031624 0ustar zackzackrepoze.who>=1.0.6 python-openid>=2.0 setuptools zope.interface././@LongLink0000000000000000000000000000017300000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/namespace_packages.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000644000175000017500000000004511157260664031620 0ustar zackzackrepoze repoze.who repoze.who.plugins ././@LongLink0000000000000000000000000000017100000000000011564 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/dependency_links.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000644000175000017500000000000111157260664031610 0ustar zackzack ././@LongLink0000000000000000000000000000015500000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/PKG-INFOpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000644000175000017500000000277411157260664031633 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who.plugins.openid Version: 0.5 Summary: An OpenID plugin for repoze.who Home-page: http://quantumcore.org/docs/repoze.who.plugins.openid Author: Christian Scholz Author-email: cs@comlounge.net License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: Introduction ============ ``repoze.who.plugins.openid`` is a plugin for the `repoze.who framework `_ enabling `OpenID `_ logins. For more information read the `documentation `_ or check out the `source code `_. Changelog ========= 0.5 - March 15th 2009 --------------------- * Initial release Keywords: openid repoze who identification authentication plugin Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP ././@LongLink0000000000000000000000000000016500000000000011567 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/entry_points.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000644000175000017500000000004511157260664031620 0ustar zackzack # -*- Entry points: -*- ././@LongLink0000000000000000000000000000016200000000000011564 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/top_level.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000644000175000017500000000000711157260664031616 0ustar zackzackrepoze ././@LongLink0000000000000000000000000000016100000000000011563 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/not-zip-safepython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000644000175000017500000000000111157257725031614 0ustar zackzack ././@LongLink0000000000000000000000000000016000000000000011562 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/SOURCES.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze.who.plugins.openid.egg-info/0000644000175000017500000000177711157260664031635 0ustar zackzackREADME.txt TODO.txt setup.cfg setup.py docs/HISTORY.txt docs/Makefile docs/basicflow.rst docs/conf.py docs/index.rst docs/installation.rst docs/usage.rst docs/who.ini docs/.static/quantumcore-small.png docs/.static/quantumstyle.css docs/.templates/layout.html docs/api/identification.rst repoze/__init__.py repoze.who.plugins.openid.egg-info/PKG-INFO repoze.who.plugins.openid.egg-info/SOURCES.txt repoze.who.plugins.openid.egg-info/dependency_links.txt repoze.who.plugins.openid.egg-info/entry_points.txt repoze.who.plugins.openid.egg-info/namespace_packages.txt repoze.who.plugins.openid.egg-info/not-zip-safe repoze.who.plugins.openid.egg-info/requires.txt repoze.who.plugins.openid.egg-info/top_level.txt repoze/who/__init__.py repoze/who/plugins/__init__.py repoze/who/plugins/openid/__init__.py repoze/who/plugins/openid/classifiers.py repoze/who/plugins/openid/identification.py repoze/who/plugins/openid/tests/__init__.py repoze/who/plugins/openid/tests/consumer.py repoze/who/plugins/openid/tests/test_challenge.pypython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/0000755000175000017500000000000011157260664024374 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/__init__.py0000644000175000017500000000036411157257401026503 0ustar zackzack# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/0000755000175000017500000000000011157260664025171 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/__init__.py0000644000175000017500000000036411157257401027300 0ustar zackzack# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/0000755000175000017500000000000011157260664026652 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/__init__.py0000644000175000017500000000036411157257401030761 0ustar zackzack# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/0000755000175000017500000000000011157260664030130 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/tests/0000755000175000017500000000000011157260664031272 5ustar zackzack././@LongLink0000000000000000000000000000016300000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/tests/test_challenge.pypython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/tests/tes0000644000175000017500000001241511157257401032006 0ustar zackzackimport unittest import StringIO from repoze.who.tests import encode_multipart_formdata from StringIO import StringIO from repoze.who.plugins.openid.classifiers import openid_challenge_decider from repoze.who.plugins.openid.identification import OpenIdIdentificationPlugin from consumer import patch_plugin class ChallengeTest(unittest.TestCase): """test the challenge plugin""" def setUp(self): self.server_response={ "openid.mode" : "id_res", "nonce" : "nonce", "openid.identity" : "http://repoze.myopenid.com", "openid.assoc_handle" : "assoc_handle", "openid.return_to" : "return_to", "openid.signed" : "signed", "openid.sig" : "sig", "openid.invalidate_handle" : "invalidate_handle", } self.plugin = patch_plugin(OpenIdIdentificationPlugin( store = None, openid_field = 'repoze.whoplugins.openid.openid', error_field = '', store_file_path='', session_name = '', login_handler_path = '/login', logout_handler_path = '', login_form_url = '/login_form', logged_in_url = '', logged_out_url = '', came_from_field = 'came_from', rememberer_name = '' ) ) environ = {'wsgi.input':'', 'wsgi.url_scheme': 'http', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '8080', 'CONTENT_TYPE':'text/html', 'CONTENT_LENGTH':0, 'REQUEST_METHOD':'POST', 'PATH_INFO': '/protected', 'QUERY_STRING':'', } class DummyLogger: warnings = [] debugs = [] infos = [] def warn(self, msg): self.warnings.append(msg) def debug(self, msg): self.debugs.append(msg) def info(self, msg): self.infos.append(msg) logger = environ['repoze.who.logger'] = DummyLogger() self.environ=environ def tearDown(self): pass def test_challenge_decider(self): """test challenge decider""" environ = self.environ environ['repoze.whoplugins.openid.openid'] = 'foobar.com' # decider takes environ, status, headers self.assertEqual(openid_challenge_decider(environ, '200 Ok', {}), True) self.assertEqual(openid_challenge_decider({}, '401 Unauthorized', {}), True) self.assertEqual(openid_challenge_decider({}, '200 Ok', {}), False) def test_challenge_redirect(self): """check if the challenge plugin works if given an openid""" # create a form POST response as if we would post the openid fields = [('repoze.whoplugins.openid.openid','foobar.com')] content_type, body = encode_multipart_formdata(fields) environ = self.environ environ['wsgi.input'] = StringIO(body) environ['REQUEST_METHOD'] = 'POST' environ['CONTENT_LENGTH'] = len(body) environ['CONTENT_TYPE'] = content_type # in this case the plugin has to redirect to the openid provider # faked by MockConsumer in this case res = self.plugin.challenge(environ, '200 Ok', {}, {}) self.assertEqual(res.location,'http://someopenidprovider.com/somewhere') self.assertEqual(res.status,'302 Found') def test_challenge_show_login_form(self): """test if the challenge plugin redirects to the login form""" res = self.plugin.challenge(self.environ, '200 Ok', {}, {}) self.assertEqual(res.location,'/login_form?came_from=http://localhost:8080/protected') self.assertEqual(res.status,'302 Found') def test_login_form_send(self): """test if the login form data is received and the environment set correctly""" fields = [('repoze.whoplugins.openid.openid','foobar.com')] content_type, body = encode_multipart_formdata(fields) environ = self.environ environ['wsgi.input'] = StringIO(body) environ['REQUEST_METHOD'] = 'POST' environ['CONTENT_LENGTH'] = len(body) environ['CONTENT_TYPE'] = content_type environ['PATH_INFO'] = '/login' identity = self.plugin.identify(environ) self.assertEqual(environ.get('repoze.whoplugins.openid.openid',None), 'foobar.com') def test_complete_openid_request(self): """test if the openid request completes""" environ = self.environ environ['PATH_INFO'] = '/login' fields = self.server_response.items() content_type, body = encode_multipart_formdata(fields) environ['wsgi.input'] = StringIO(body) environ['REQUEST_METHOD'] = 'POST' environ['CONTENT_LENGTH'] = len(body) environ['CONTENT_TYPE'] = content_type identity = self.plugin.identify(environ) self.assertEqual(identity['repoze.who.plugins.openid.userid'],'http://repoze.myopenid.com') def test_incomplete_openid_request(self): """test if the openid request fails with a wrong identity""" environ = self.environ environ['PATH_INFO'] = '/login' sresp = self.server_response sresp['openid.identity'] = '' fields = sresp.items() content_type, body = encode_multipart_formdata(fields) environ['wsgi.input'] = StringIO(body) environ['REQUEST_METHOD'] = 'POST' environ['CONTENT_LENGTH'] = len(body) environ['CONTENT_TYPE'] = content_type identity = self.plugin.identify(environ) self.assertEqual(identity.get('repoze.who.plugins.openid.userid',None),None) def test_authenticate(self): """test if the authentication plugin works as well""" environ = self.environ identity = {'repoze.who.plugins.openid.userid' : 'http://foobar.com'} res = self.plugin.authenticate(environ, identity) self.assertEqual(res, 'http://foobar.com') ././@LongLink0000000000000000000000000000015500000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/tests/consumer.pypython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/tests/con0000644000175000017500000000421311157257401031767 0ustar zackzack""" This code is copied from plone.openid http://svn.plone.org/svn/plone/plone.openid/trunk/plone/openid/tests/consumer.py """ from openid.consumer.consumer import FAILURE, SUCCESS class MockAuthRequest: """Amock OpenID AuthRequest. """ def __init__(self, status=None, identity_url=None, message=None): self.status=status self.identity_url=identity_url self.message=message def redirectURL(self, trust_root, return_to): return "http://someopenidprovider.com/somewhere" def getDisplayIdentifier(self): return "http://foobar.com" class MockConsumer: """A mock OpenID consumerclass. """ def begin(self, identity): self.identity=identity return MockAuthRequest() def complete(self, credentials, current_url): status=SUCCESS message="authentication completed succesfully" if credentials.has_key("openid.identity") and credentials["openid.identity"] == "": # if the python openid is passed an identity of an empty string # an IndexError is raised in the depths of its XRI identification # see: http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=xri # an empty string is common when the submit button of the # openid login is clicked prior to providing an identity url # we simulate openid's response here in our mock object message="invalid identity" status=FAILURE else: for field in [ "nonce", "openid.identity", "openid.assoc_handle", "openid.return_to", "openid.signed", "openid.sig", "openid.invalidate_handle", "openid.mode"]: if field not in credentials: message="field missing" status=FAILURE return MockAuthRequest(status=status, message=message, identity_url=credentials["openid.identity"]) def get_consumer(environ): return MockConsumer() def patch_plugin(plugin): plugin.get_consumer = get_consumer return plugin ././@LongLink0000000000000000000000000000015500000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/tests/__init__.pypython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/tests/__i0000644000175000017500000000000011157257401031724 0ustar zackzack././@LongLink0000000000000000000000000000014700000000000011567 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/__init__.pypython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/__init__.0000644000175000017500000000410311157257401031661 0ustar zackzackfrom identification import OpenIdIdentificationPlugin def make_identification_plugin(store='mem', openid_field = "openid", session_name = None, login_handler_path = None, logout_handler_path = None, login_form_url = None, error_field = 'error', logged_in_url = None, logged_out_url = None, came_from_field = 'came_from', store_file_path='', rememberer_name = None, sql_associations_table = '', sql_nonces_table = '', sql_connstring = ''): if store not in (u'mem',u'file',u'sql'): raise ValueError("store needs to be 'mem', 'sql' or 'file'") if login_form_url is None: raise ValueError("login_form_url needs to be given") if rememberer_name is None: raise ValueError("rememberer_name needs to be given") if login_handler_path is None: raise ValueError("login_handler_path needs to be given") if logout_handler_path is None: raise ValueError("logout_handler_path needs to be given") if session_name is None: raise ValueError("session_name needs to be given") if logged_in_url is None: raise ValueError("logged_in_url needs to be given") if logged_out_url is None: raise ValueError("logged_out_url needs to be given") plugin = OpenIdIdentificationPlugin(store, openid_field = openid_field, error_field = error_field, session_name = session_name, login_form_url = login_form_url, login_handler_path = login_handler_path, logout_handler_path = logout_handler_path, store_file_path = store_file_path, logged_in_url = logged_in_url, logged_out_url = logged_out_url, came_from_field = came_from_field, rememberer_name = rememberer_name, sql_associations_table = sql_associations_table, sql_nonces_table = sql_nonces_table, sql_connstring = sql_connstring ) return plugin ././@LongLink0000000000000000000000000000015200000000000011563 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/classifiers.pypython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/classifie0000644000175000017500000000104411157257401032007 0ustar zackzackimport zope.interface from repoze.who.interfaces import IChallengeDecider def openid_challenge_decider(environ, status, headers): # we do the default if it's a 401, probably we show a form then if status.startswith('401 '): return True elif environ.has_key('repoze.whoplugins.openid.openid'): # in case IIdentification found an openid it should be in the environ # and we do the challenge return True return False zope.interface.directlyProvides(openid_challenge_decider, IChallengeDecider) ././@LongLink0000000000000000000000000000015500000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/identification.pypython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/repoze/who/plugins/openid/identific0000644000175000017500000003104411157257401032006 0ustar zackzackimport cgi import urlparse import cgitb import sys from zope.interface import implements from repoze.who.interfaces import IChallenger from repoze.who.interfaces import IIdentifier from repoze.who.interfaces import IAuthenticator from webob import Request, Response import openid from openid.store import memstore, filestore, sqlstore from openid.consumer import consumer from openid.oidutil import appendArgs from openid.cryptutil import randomString from openid.fetchers import setDefaultFetcher, Urllib2Fetcher from openid.extensions import pape, sreg class OpenIdIdentificationPlugin(object): """The repoze.who OpenID plugin This class contains 3 plugin types and is thus implementing IIdentifier, IChallenger and IAuthenticator. (check the `repoze.who documentation `_ for what all these plugin types do.) """ implements(IChallenger, IIdentifier, IAuthenticator) def __init__(self, store, openid_field, error_field = '', store_file_path='', session_name = '', login_handler_path = '', logout_handler_path = '', login_form_url = '', logged_in_url = '', logged_out_url = '', came_from_field = '', rememberer_name = '', sql_associations_table = '', sql_nonces_table = '', sql_connstring = ''): self.rememberer_name = rememberer_name self.login_handler_path = login_handler_path self.logout_handler_path = logout_handler_path self.login_form_url = login_form_url self.session_name = session_name self.error_field = error_field self.came_from_field = came_from_field self.logged_out_url = logged_out_url self.logged_in_url = logged_in_url # for the SQL store self.sql_associations_table = sql_associations_table self.sql_nonces_table = sql_nonces_table self.sql_connstring = sql_connstring # set up the store if store==u"file": self.store = filestore.FileOpenIDStore(store_file_path) elif store==u"mem": self.store = memstore.MemoryStore() elif store==u"sql": # TODO: This does not work as we need a connection, not a string self.store = sqlstore.SQLStore(sql_connstring, sql_associations_table, sql_connstring) self.openid_field = openid_field def _get_rememberer(self, environ): rememberer = environ['repoze.who.plugins'][self.rememberer_name] return rememberer def get_consumer(self,environ): session = environ.get(self.session_name,{}) return consumer.Consumer(session,self.store) def redirect_to_logged_in(self, environ): """redirect to came_from or standard page if login was successful""" request = Request(environ) came_from = request.params.get(self.came_from_field,'') if came_from!='': url = came_from else: url = self.logged_in_url res = Response() res.status = 302 res.location = url environ['repoze.who.application'] = res # IIdentifier def identify(self, environ): """this method is called when a request is incoming. After the challenge has been called we might get here a response from an openid provider. """ request = Request(environ) # first test for logout as we then don't need the rest if request.path == self.logout_handler_path: res = Response() # set forget headers for a,v in self.forget(environ,{}): res.headers.add(a,v) res.status = 302 res.location = self.logged_out_url environ['repoze.who.application'] = res return {} identity = {} # first we check we are actually on the URL which is supposed to be the # url to return to (login_handler_path in configuration) # this URL is used for both: the answer for the login form and # when the openid provider redirects the user back. if request.path == self.login_handler_path: # in the case we are coming from the login form we should have # an openid in here the user entered open_id = request.params.get(self.openid_field, None) environ['repoze.who.logger'].debug('checking openid results for : %s ' %open_id) if open_id is not None: open_id = open_id.strip() # we don't do anything with the openid we found ourselves but we put it in here # to tell the challenge plugin to initiate the challenge identity['repoze.whoplugins.openid.openid'] = environ['repoze.whoplugins.openid.openid'] = open_id # this part now is for the case when the openid provider redirects # the user back. We should find some openid specific fields in the request. mode=request.params.get("openid.mode", None) if mode=="id_res": oidconsumer = self.get_consumer(environ) info = oidconsumer.complete(request.params, request.url) if info.status == consumer.SUCCESS: environ['repoze.who.logger'].info('openid request successful for : %s ' %open_id) display_identifier = info.identity_url # remove this so that the challenger is not triggered again del environ['repoze.whoplugins.openid.openid'] # store the id for the authenticator identity['repoze.who.plugins.openid.userid'] = display_identifier # now redirect to came_from or the success page self.redirect_to_logged_in(environ) return identity # TODO: Do we have to check for more failures and such? # elif mode=="cancel": # cancel is a negative assertion in the OpenID protocol, # which means the user did not authorize correctly. environ['repoze.whoplugins.openid.error'] = 'OpenID authentication failed.' pass return identity # IIdentifier def remember(self, environ, identity): """remember the openid in the session we have anyway""" rememberer = self._get_rememberer(environ) r = rememberer.remember(environ, identity) return r # IIdentifier def forget(self, environ, identity): """forget about the authentication again""" rememberer = self._get_rememberer(environ) return rememberer.forget(environ, identity) # IChallenge def challenge(self, environ, status, app_headers, forget_headers): """the challenge method is called when the ``IChallengeDecider`` in ``classifiers.py`` returns ``True``. This is the case for either a ``401`` response from the client or if the key ``repoze.whoplugins.openid.openidrepoze.whoplugins.openid.openid`` is present in the WSGI environment. The name of this key can be adjusted via the ``openid_field`` configuration directive. The latter is the case when we are coming from the login page where the user entered the openid to use. ``401`` can come back in any case and then we simply redirect to the login form which is configured in the who configuration as ``login_form_url``. TODO: make the environment key to check also configurable in the challenge_decider. For the OpenID flow check `the OpenID library documentation `_ """ request = Request(environ) # check for the field present, if not redirect to login_form if not request.params.has_key(self.openid_field): # redirect to login_form res = Response() res.status = 302 res.location = self.login_form_url+"?%s=%s" %(self.came_from_field, request.url) return res # now we have an openid from the user in the request openid_url = request.params[self.openid_field] environ['repoze.who.logger'].debug('starting openid request for : %s ' %openid_url) try: # we create a new Consumer and start the discovery process for the URL given # in the library openid_request is called auth_req btw. openid_request = self.get_consumer(environ).begin(openid_url) except consumer.DiscoveryFailure, exc: # eventually no openid server could be found environ[self.error_field] = 'Error in discovery: %s' %exc[0] environ['repoze.who.logger'].info('Error in discovery: %s ' %exc[0]) return self._redirect_to_loginform(environ) except KeyError, exc: # TODO: when does that happen, why does plone.openid use "pass" here? environ[self.error_field] = 'Error in discovery: %s' %exc[0] environ['repoze.who.logger'].info('Error in discovery: %s ' %exc[0]) return self._redirect_to_loginform(environ) return None # not sure this can still happen but we are making sure. # should actually been handled by the DiscoveryFailure exception above if openid_request is None: environ[self.error_field] = 'No OpenID services found for %s' %openid_url environ['repoze.who.logger'].info('No OpenID services found for: %s ' %openid_url) return self._redirect_to_loginform(environ) # we have to tell the openid provider where to send the user after login # so we need to compute this from our path and application URL # we simply use the URL we are at right now (which is the form) # this will be captured by the repoze.who identification plugin later on # it will check if some valid openid response is coming back # trust_root is the URL (realm) which will be presented to the user # in the login process and should be your applications url # TODO: make this configurable? # return_to is the actual URL to be used for returning to this app return_to = request.path_url # we return to this URL here trust_root = request.application_url environ['repoze.who.logger'].debug('setting return_to URL to : %s ' %return_to) # TODO: usually you should check openid_request.shouldSendRedirect() # but this might say you have to use a form redirect and I don't get why # so we do the same as plone.openid and ignore it. # TODO: we might also want to give the application some way of adding # extensions to this message. redirect_url = openid_request.redirectURL(trust_root, return_to) # # , immediate=False) res = Response() res.status = 302 res.location = redirect_url environ['repoze.who.logger'].debug('redirecting to : %s ' %redirect_url) # now it's redirecting and might come back via the identify() method # from the openid provider once the user logged in there. return res def _redirect_to_loginform(self, environ={}): """redirect the user to the login form""" res = Response() res.status = 302 q='' ef = environ.get(self.error_field, None) if ef is not None: q='?%s=%s' %(self.error_field, ef) res.location = self.login_form_url+q return res # IAuthenticator def authenticate(self, environ, identity): """dummy authenticator This takes the openid found and uses it as the userid. Normally you would want to take the openid and search a user for it to map maybe multiple openids to a user. This means for you to simply implement something similar to this. """ if identity.has_key("repoze.who.plugins.openid.userid"): environ['repoze.who.logger'].info('authenticated : %s ' %identity['repoze.who.plugins.openid.userid']) return identity.get('repoze.who.plugins.openid.userid') def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/PKG-INFO0000644000175000017500000000277411157260664024177 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who.plugins.openid Version: 0.5 Summary: An OpenID plugin for repoze.who Home-page: http://quantumcore.org/docs/repoze.who.plugins.openid Author: Christian Scholz Author-email: cs@comlounge.net License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: Introduction ============ ``repoze.who.plugins.openid`` is a plugin for the `repoze.who framework `_ enabling `OpenID `_ logins. For more information read the `documentation `_ or check out the `source code `_. Changelog ========= 0.5 - March 15th 2009 --------------------- * Initial release Keywords: openid repoze who identification authentication plugin Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/TODO.txt0000644000175000017500000000063011157257641024376 0ustar zackzack- fix the SQLStore (make the connstring a connection, add tests) - add tests for mem, file store - fix the session problem, use cookie fallback if no session is given - add SREG/AX/PAPE support - make more configuration optional and provide default values - rename sql_* to store_sql_* config options - is came_from actually working? - document logging - document which errors can occur in self.error_field python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/0000755000175000017500000000000011157260664024020 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/.static/0000755000175000017500000000000011157260664025365 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/.static/quantumcore-small.png0000644000175000017500000006213311157257402031545 0ustar zackzackPNG  IHDRsvg gAMA7tEXtSoftwareAdobe ImageReadyqe<cIDATx}|lM]dݦ4C' @: R@B~:7rսmfNgtE(}s۝{=p69}l lR"ր6@*R*RWaUAm4 q]SJ},,» Lb3f=on띩Nh՘Z W|TJT-3(@݂;nuK7yM5ΰ eS,b) UR)_ 9(1Ը[6L8/2`|yy9mC|l Q,K'T2R) ޔƢmq,Sh pUaR)!wsK9dg g*ai3 ض=Ja6%T{TJ|=́Ǣ`F˂ ]35J;0g$Ox>w ͶynW{TJ|;[QDW+2SӜJ>G;m[ϗ46*קּ~TJT]̝QU-Yҙ[aIAb}Os$(AZR)WF(#is%^4m4ԳP[EDPR)2n|>E7p~?ce_ɼWjW\-<4_tv>7eg|e)U5S _]ɵ%suUN7ycG.Œ93f6tQ^xB3T\u+^֮-ϯ@2=*-mmmfZd ~8pٲz]vܹ'hoothP8¡Wȭ2($2{Ȉ_{ th[oW42CFWc9? ye>e^zܢ+'*yI$I_ڮYhs5E+Y^$p9s&pzۣTUQeYd?rG:LV#N]!ɷ*[5eeBZ,cG{iK^;S K6z=%l4nroMN@c+#ux%1|./_ṽWOn)l99oA[WW  60d'{ry|>Qȕ[0H,]Be ڎ G%.UP|qñ6[`f0\;;t~k|aH)h kKlKLbVU˔E/x"4xj7pK/л~qBȜwG7lkknnvE]:Csx[IG_1Mn3(wڊKUq-?2a[8?&wԋ=NtznƖY N׭[GF >Gt0@fqlـEd mC5_Xj=}l@ 2,E}? v='(AM+ّ:zslk8uzpLG%'\=*TY~5>\EѬ ]aiӧ$AHY Lyꮧo~Ǐ(:7-8s?Z2@U&|w?}mT̸$ q䙕>)2hY__?ydIcI{oK/<1 ?S,YrUzrpw,!à-OF "(;@pܿI#ߜg)дͦTl%=e{L&>=It/Ts6lbq=*G--V!v Qی1Xf=)yCp hf@Y3. RNoXnUد6#PzI`Dv1¬oΘWY-x}.?ۿq .k nyuR3{{>?S'|4F_ҍ#yc|4g0=Kapv@v$&/G ; #c"?M̓'p",58yt+Am_5סZr #a%-Kk}ި6lFb@_y pܒX?Pfд[c[ړZ]4vcKgM.'sxr='}T>jVxutʜTlqjTz羹{=1Rw^C|+ +;:1)uoyHm,>Ǎ8KEA|>C=+ *Dp!xAp.56Ӡ@ $Jc. {@v×PIH61&v Ɏ+vk=I33'w%vÍ526gbz٠\ s^wr\K;*X̜wM{%Ie 6yWʜW < JQvڔƆPx ο38Ѧiy> @?Fj]A9>чIB,.:)oɂm hBHt`4e8jtO m Z41̌-8 #pVHep;o֮LOɑ|b\ʡeq8^H C2`9c|~笈'7t[&[!²wڲʜW1-'DGNLR10h}^o^[}/61> ܙ^(|1'H,D3Q!HPJ1eu@ 3m^ !S4^sGFzD7\G?`:ϣ\ЌM*)=E]Q!!mpHCߕ Lj2U=8"rrIe+¿y者 Ih8nU /3dYyk˪1?L_|vӎMhaocc㬶Y``ܹRA``/?㱾X9ZPV?ksG1eUUgtΔ%sf(V\r+2LtMw3f@@@8y&m䬱~㄃?R U*#Z̎p$eV=b c{6Ȱz'{@ 癀--gRܗ=Z3tzQisZ=@A}=i$X`H5z+ N,|I(l'8g[ըH cْơKUAqćU\N5ި%+Jwkjj_ܯz{iơ |KO:7xQlk *TjrB5u[gmk9yDUyif0uM'+a!DM򕟎pmCuP͡'P]Lk^_]8{8SO=>#n.NzfWq~38w9WORW_Y`u*7:eQnX|婟%_Ӫ"_"F>so~\r 9CsM5f H؛/OP78. steqo\ϝ+7Yf,cna_nGR`Y '/h' ^ @B&4zX ?>ۜ> 239יl i2&A)Am:L'nCַMҤWh۩[d0c2 % 0N/#}(5 ě)5@}@39j`LW^bE4(Yk3>wwj5ec!um֮.38,aeYW,h.Ydįߺ[]|Yc&97g:VV<5k^OƺPk=I"bm3Á77|(VyM5 J\c W1Gad39'g,c4 v$ꝱL6*^"Cg-/ݽU Tj3 31ym˜5# Bh0zޗ2lFc~v{$h!;Gx/Eeah7 nKDBAfمFq0aeeeR!BxX+յehk}+FpitWM, :ax~=+71]v4.sSf^}DkߘR]/8'1䇷& = _LtRfYE|OX|ɓ'5N=xdžY ְPkWL pΎ=StP} _\x%3zb_٫h*Kڨъwgѕ@h:g1{.:hwۿgο*ׅd:yu>>k\;{Z)!|p=s-pD-q ZE!{yg,) RypKNwiO\tƶƍ8P](3յ+s@r4EA݊ haӕ4Ժk4ʡ`43 9Q *.G12L!߇߁Hm)pm <^+s22Mx0s*G G4 @7p0MɾL?F$f_ϰ Sy.T[t #&/ L$Hb2Lxq ~s@L]E"V"/[f&vfؤ@jPLY:GH4P'13ik/X h4e! /#g4;^ ]Q;3ڙO˝$/W b}ac)'gH&4c8Noag<$E'(?eFu??1O%Sun8AEj=hk󔤞Ćg #,djp sOi#9P0tu؝%"J`B}.:J@R)V 2C\0{_h0W0UʅF./#yRKQ?uvhJ~uy@qXK}&f٭OcИG-Ƽ(!y[ʺ:L%bo)*șp6fegw>Dc ܇ Q4jӈhV̠*4qF={)`c4'ڝ3wQȳeh{,tRYM I6-TYBU^YKqj6їE"feJ^Md6vM$5#`)㜻0r~[yUt<|JA)CiR hhwpY 'zy̝IɋȀ,Ή0>5lΡnj䬵aʤ&ij=]Ήtg,V4_l#@WNy9.tIfUcCU9oWE(r{497?y.4Dr-fjȩ9NTQY,u-OQ# c x[vgMԄKQ4131=c~7c>_p}*I?K2Dcp,ȍzv?@ _d-;xv9i~&Ylm t) p_Xmt/yuf ۈUAcAlwLP:%:VkKX7崪Bv8_f{05hOhwH!H.>CvEI&F$P=~ g<mU򩣱Dinˏ98jZ?1K39/Hs^4;̉QNWTecltZ etwQ`}Nx^Wc0sCFtP̈TPPS\0.ީ XQd:Ave$%GqW%A.+gl˵Վ1=ve]F_ysveFF&;1:[ Jwh0Tv2šX ( >X$Ʃ u ڠS,Gf͐%sp{ ?0|&4*N u[LGu-`"-\f۱]L03KfFK]:@ UT4}NR}r=wU*!B8&a%:HGSwvHgڦԎk%.p& [pJ\cZHKp$"* M.#i8yS!2ye>5T252 X:J7>a<~UQ8#y%^NqU&mY#Zc[cΨ0w懮8|,REYRy-`j%șG\nF|z3m18qZ`vPGx@.5l#2/Rx *dDv&;0~ SKrUAz/"4pKTuzT25jM·*F <6ui'sr@{$OĤ" 5._hѹm:+L6Z2?H/}OLl%_ '|5 OWe MӵXkosӋVr0RX!d\XԤdͰZ|'>0i%#6 Z(ջgg{<`M,X#ܢ; cDtdc f*mZ={+nߑ]l@v䥡ĐZ,|On);%Nk@tvTqĐCIFM•'>H4~YWܔP;"y l WT9W.55{1:߉<ӦN&L^N khIU,.MCv Į-iHseul #;vQʵB/e9l^+j+ATǑ+ t\[2wԨ@ǘ=~pH 'y.AHsK^tSG{ÕղXRyW}O~080XX H>Π#z%-݀4)Nur \vZPEN\D_Zԓjn,~C)ؖOx Om6L [HAMgKDU>k/JZە;Xf얼5J ʲLLKYCU@JDrF-~_MLv`5 {:¸:/َY\bN&_+[{Z܍ 6JPT;HYe@"l;Cm6<!q|B*s)1w2YVoYg,jPeoȗ$),CE@s %&hߦ2 V?HdwA*Mtd8vղ32= s ?JzO1 PH0wL X }'H'Z傕cH6o6Xb g^.:NRA$^-#J_d.âȫmmՉ"'j\U/#}gbi.AU%0,{xQp٣*o3;9@@QڔAEP7z^@~ThͱOS㪽SOu%4K5URS|S#juGbWGS="!(kHI nH2oP)},P aU:SNI Mz@` f2,'Ae"呼q{ؚ|M 'sca@Ȉ Ø15A}0i$z}?,i#6dKjˌex}_w1TMVB"+<^Ҏ MXT;2)oVe ;5Ղcoo}Aduͯv~jm e}N1p%>'9(leKOm7{>RfU>IzTk|p*;#0"b!2d(^Hh 9oٳҸ,̒dax9b]5uјB,7CvfjksFM9x&FdIN ɒ֊WwRVJ>;^Րl6OMVy5 u'F DL2/`%HT;|ʁZsԅU ᎠpWժ~. !0رHMT^dn &JD^#RWlܰn8F.+՜Q`GјK҉So1sN֪xK;^m*'I%iQ:E fqn˱peE G(asǥAژ.DɂtܙhuYZ'1(: i#ϼʬk)WQkg-$t_Gp.B2ė,x942ZYN1Tk h=B9ax^w y>2[=δnOom]0c4@l11 w%v5y}#C@˂Tk ̂1?EzZ=?m 3ЕBObkH U~CØP.%uc@MjrIП p#X@od 7Ƕu7 maTY,2«PHEPVY>D2CCOx{qxMYs7%_YeMajgɑ!u/ef њcd}1kLbC/ TZ5P O2]ɳl.7{M< qDjӈ=N-E󡘽E8̺)pbcIF8]EWOw'j)k{ ]m6鬖Yly i/J#劬Epc-da˾N0cj=4ft?%A'rB0~ā&33 m֭ G~Bt- /w7tl4loNwC>2-^u<%FW^U"ܢOPVWxPg{h:J r̆c"0 4^;Ht`0a3RB=ƎĎj"dAS|S{^i3FLwD !b8tNFw#!ls8' i=:~I =<ݶ@HPAv ],Փotwi=78a;ө)4Iwph{{ԩQ+v( )oAZF tU n8w.kq&x)(q+X^)u%*1BGiU)a_F:OʲC/ Fƚ )C'{^ע+q|cG&m@ŽKt۶L}JMWͻ#{NT{\ݟoR#$%K@ D01}hgbkTw0174.UPBVM}nNHpTg4pA&a0N ` Lp68Jj4MZU(ݝl 4>Bo ћ0;2}QbμAEaC[pjNڞTOwgKl3%pŔ2q\vGS|[,s˯w%>i3E翟C@-58,w.op?ULIS',f7`(hM87 _?t9`lg$j_M d-6YvĹOtSNG$/ɦK`X؛ak֯o'bM;Hr0p9;2e1 ܶ&&qF g~k\5ݩ\V=bF[VJO}tω'mo~}l7 nx]]Py2G(PZӧʜ\4"@CR/A (jw#Ձ>2 4>3aS1ћ4Sa'8rnpUk4 AU 0߰583mX 2![e6Ƿӏ j" 1`u&{fzWrWg3fSz"mVNW_ @K760H:z:Ty_e7~tvהOuu?n*So>l0^ty71_7áģl 9}FÌ ư9~t;;bVjW._u{& $8AY2}Rcmy\P3'Nb㸟3|7*ܡ$l`r1!Uש7]J >oH7>:!ʺfϝؔ e?+Kr$#ll2jak:ti8S+̋=5طg,9롗:fEQ6"1;@ ˿cIzQ&dqƴ΄.rv~Hw8 TrF3mmU[$OU{zMSh^VKh YL&ySSxe^xgh bfi ӿ(_G;ss6. -ڞ6EฑJA<,[ite9hwbOAcJ!2mD71"Nغf2f`M{u1 i@vLS$cgؚ 0 t{7ގE?Kܪ.X1}魗nur {l[;,vʎZM]ѧ4 M:56'SDG^w5&p5"n"5=ikaX)$rU,j*|^<[l. [ɱBW#ܸ |gjkj 2:亃Jk0 h^H9G~ {Ea[7ZSm_M1=ŧ=9@x0oeJpv_|ɵĠndCfμ:Gu޵4,"wOKD9?{*tJnTEv;<ƾkS$ƓTN0ZY2N)Gk)T?&;4,g^c<3?㺏~*T]@ l!rǗoj}"M?x?0:o@lB`fU2hljgj8U*ڗ,̕aB42(]@jr^ `pvNuy$u:4+֝qx70ӄ2)2#0cq;ݡ&kJLAuf4yIQL0M}?$:X[muj8IsRf+57YWB=g(7]wMW408޻w^L\,ǥt}UO@ hf* rG?wG>æI+.?\I-qҤÏ8\UT ؼ?ytxf&`hFV]:e ţj<~-Ԅ{pVxmQx$t])+a^n^a h,ϙR cxwl9_w}pͬ'to~^RK{5Չ* O9At?pkkU&QjGQ mO-3Ǝ{ ^([[>:8$YZ}Փ:?6飱px3S^po=9Უ?oѼჩapJ;St;q?)'plnyG[2QNy䏏81d'^|pYk@P馐)S&O_ ܋1A\ʙK;;zX-jF7>}aJd hm\x+VGW7{Zzrk|_bBUTOݩnM`s[P;'8nm2 S2NϘz~'N.B icDi b ڎ.P'iV*J @[ݗ UVmmHvRˆ>wPDqHoDt@GQΊ8|Xbkץt@+kb':gΜ?t#d&VCLe0<D%]~>ȶf[υ_T7$eٙ@XN~٤\ۋO%ۅ@MBߞNChB-[od ~5:bLP 8iW>sו O齜 MMA0  0(F(=s^8=FGε)NwF#1K2 h.Le[MkFJ%ԫ+{_}PIF7ޕբhRr$ ٘v[Qm|2G`tDŽ]fY3N>c̩7|^"M .3,l(Ajs,?|~DžBx:kɵ+[1ڤ[C#ǔg):t;2Icw(o>y-1rVi$;ÝMdb9Y__v>}FԅϮJYT$>{~1N,u0#{0C5KjS /K*I/<%ݺp3; eB1k@ls˧AC]N4Cb:n4xBg3FeuשQ>cww }@dx_ٻrgM>#yh}ѵ;}Z_=ۅKZsrX@i(x$+"%:@JYKIp/pU Ϋ}{ 'Ňv؜9mGwmܝϿie'%_wusN?|k庵ƛ#'_1KӾ^}啗Wv9N4ןSFE>眿g5':ԣ3<˫z=e2u=wmR|aF|mU Bs]lx/_cafM-zܡGGmglѯ>7,Ȫw-xt֘?Yt93XƑ{h9si1G53-܄l9/b@glW+|rj> ;;޵Pq(.ۂ\W4#/>u}R9HN.3\|В+ׯ_v/_C$eR2& th4mIJTgOf7 ` =13V6Cئ݉ u>78/ @*tR]tI=0@p{Gޏ_`0ڔ\1~p d"R)a)΂j2 lh/\da<@,'y=62_a6;A9V[ujªEn[<-6&?jNqjGZА40¢wC -oji[{Mf'pP8lnl!¡LJ;l4 0Xm3<d b$7̰V7MUqHŵlܸ{jzoJ"=75"ahCX Z{hROա@;N8$2tni+2RH<&2MݲcC=R*R>ZeUD91ut bD093w0PvU5bOٰE׭Y-I#jlsv>D2V[5+-qJO?`~_NCN XbE9p?d!6ṁJ$ Ѭ#EZr0e畼W%:@%O6x؆jW5[:ɾP.VtjL/-e$w[iOD];wưt]BR)Rs*eh.$BFf~x%cbֳ+!Rpc]%} o;DLqqE^\箅{gbgHtJpoZ'A1? ~z,Hv'wkωɾ* ocK>nkdw>WJTʇ;QiTj$aaxFN3cPuMgĭ80h&ʶ`=^*pnL( /_ 4)rp_h{=+RM=e`v͏ ce~B2˗H?56|o|?fBO>deTJ|a.,3$/`;bMSVD(md>SɀYjLiq=֙jmRE>tsLner;QCJH355^߻l2!/-ʷVs̉D".KO^ɧx2U*R>xCsjcl3 H^+Hc6Kac Ȏt n~vh_t8dwb'®$ 2'r h9L}ߝڑfP#yJ8 2ff}s?|0kdYfccū+>TJ`anwgZ8@$%tVIE^z/EK)u+"%+.@c039h#(`1x{X:uiX nށE j*KR6:;|dɳ'r L%8JKFe ~Ź˵tRʅp~ŭ/?[!. iLʿ~><߹~/_Br'3W"]/y*N6C:#< YI *[tLv;rckČݴj}ܞlE:z?(>h)O+9=OôH`:~oi߾>a?~* JeN ժcP^'RJa(K{Sy,Cިu.zٔ{?x|jxL\_XaTDZv#DO^ɻWN۷oG}OP0ܦrgۉ;l=Y1|X<td|r=:pݹ'ٍІ[+Kv#Ey}?]Bqi4(m߱r4d9 c -% uٖ4m,M)=Bos^*ᝍC*3,jP(ӽe=rSe eԉt&JB37Q'뮮!_"vHj_:[Gmhj22Fwe*S;כؤcKD(d;/V,+W(=n樛z@!MBPN Bqܽlv~$C_0ytvMr:qg'}o}F4򸙅þNj-nAmq lmcIٖׄ۵f BqQ~'t"Cvc渆^cT\$Fa8a7I~z^ş}$p~X %{;y/agJO(G!CC3y>{Xo淋rr-[Yt2u@ŷ7Q?;4eq&JDZ쵍CL'ww,q~ B}-I2{˥`IJ[VMR ~TI119i&]5ջrېKUE2K;; P(-: &-}䍂nyM%[c^?)k[=^Lw"U?"yK1SwWP\:],K!VO,^ς EEZ)p=]Bq$sz,,. c+ !=帹EcxgB\eukm&lf@Y}Lp܍  {;~U4[FP\ _u5ݪlh!; bq@c!/ MOfDŽዡi_b5aZkޑMP(N䊳"0C23۝ٞF%U3րyI *{p"~Kk=Fqv1+ʘ1)8ODbi)S1{VP(r?Äܐ؁wvrKmhXFP/~JjG< a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { visibility: visible; } a.headerlink:hover { background-color: #c60f0f; color: white; } div.body p, div.body dd, div.body li { text-align: justify; line-height: 130%; } div.body p.caption { text-align: inherit; } div.body td { text-align: left; } ul.fakelist { list-style: none; margin: 10px 0 10px 20px; padding: 0; } .field-list ul { padding-left: 1em; } .first { margin-top: 0 !important; } /* "Footnotes" heading */ p.rubric { margin-top: 30px; font-weight: bold; } /* "Topics" */ div.topic { background-color: #eee; border: 1px solid #ccc; padding: 0 7px 0 7px; margin: 10px 0 10px 0; } p.topic-title { font-size: 1.1em; font-weight: bold; margin-top: 10px; } /* Admonitions */ div.admonition { margin-top: 10px; margin-bottom: 10px; padding: 7px; } div.admonition dt { font-weight: bold; } div.admonition dl { margin-bottom: 0; } div.admonition p { display: inline; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } div.note { background-color: #eee; border: 1px solid #ccc; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; display: inline; } p.admonition-title:after { content: ":"; } div.body p.centered { text-align: center; margin-top: 25px; } table.docutils { border: 0; } table.docutils td, table.docutils th { padding: 1px 8px 1px 0; border-top: 0; border-left: 0; border-right: 0; border-bottom: 1px solid #aaa; } table.field-list td, table.field-list th { border: 0 !important; } table.footnote td, table.footnote th { border: 0 !important; } .field-list ul { margin: 0; padding-left: 1em; } .field-list p { margin: 0; } dl { margin-bottom: 15px; clear: both; } dd p { margin-top: 0px; } dd ul, dd table { margin-bottom: 10px; } dd { margin-top: 3px; margin-bottom: 10px; margin-left: 30px; } .refcount { color: #060; } dt:target, .highlight { background-color: #fbe54e; } dl.glossary dt { font-weight: bold; font-size: 1.1em; } th { text-align: left; padding-right: 5px; } pre { padding: 5px; background-color: #efc; color: #333; border: 1px solid #ac9; border-left: none; border-right: none; overflow: auto; } td.linenos pre { padding: 5px 0px; border: 0; background-color: transparent; color: #aaa; } table.highlighttable { margin-left: 0.5em; } table.highlighttable td { padding: 0 0.5em 0 0.5em; } tt { background-color: #ecf0f3; padding: 0 1px 0 1px; font-size: 0.95em; } tt.descname { background-color: transparent; font-weight: bold; font-size: 1.2em; } tt.descclassname { background-color: transparent; } tt.xref, a tt { background-color: transparent; font-weight: bold; } .footnote:target { background-color: #ffa } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { background-color: transparent; } .optional { font-size: 1.3em; } .versionmodified { font-style: italic; } form.comment { margin: 0; padding: 10px 30px 10px 30px; background-color: #eee; } form.comment h3 { background-color: #326591; color: white; margin: -10px -30px 10px -30px; padding: 5px; font-size: 1.4em; } form.comment input, form.comment textarea { border: 1px solid #ccc; padding: 2px; font-family: sans-serif; font-size: 100%; } form.comment input[type="text"] { width: 240px; } form.comment textarea { width: 100%; height: 200px; margin-bottom: 10px; } .system-message { background-color: #fda; padding: 5px; border: 3px solid red; } /* :::: PRINT :::: */ @media print { div.document, div.documentwrapper, div.bodywrapper { margin: 0; width : 100%; } div.sphinxsidebar, div.related, div.footer, div#comments div.new-comment-box, #top-link { display: none; } } python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/who.ini0000644000175000017500000000251311157257402025313 0ustar zackzack[plugin:basicauth] # identification and challenge use = repoze.who.plugins.basicauth:make_plugin realm = 'sample' [plugin:openid] # identification and challenge use = repoze.who.plugins.openid:make_identification_plugin # sql and file are possible here with different configurations store = file store_file_path = %(here)s/sstore openid_field = openid came_from_field = came_from error_field = error session_name = beaker.session login_form_url = /login_form login_handler_path = /do_login logout_handler_path = /logout logged_in_url = /success logged_out_url = /logout_success rememberer_name = auth_tkt [plugin:auth_tkt] # identification use = repoze.who.plugins.auth_tkt:make_plugin secret = s33kr1t cookie_name = oatmeal secure = False include_ip = False [plugin:htpasswd] # authentication use = repoze.who.plugins.htpasswd:make_plugin filename = %(here)s/passwd check_fn = repoze.who.plugins.htpasswd:crypt_check [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.plugins.openid.classifiers:openid_challenge_decider [identifiers] plugins = openid auth_tkt [authenticators] # plugin_name;classifier_name.. or just plugin_name (good for any) plugins = openid [challengers] # plugin_name;classifier_name:.. or just plugin_name (good for any) plugins = openid python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/Makefile0000644000175000017500000000434311157257402025460 0ustar zackzack# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -a PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html web pickle htmlhelp latex changes linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " pickle to make pickle files (usable by e.g. sphinx-web)" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf .build/* html: mkdir -p .build/html .build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html @echo @echo "Build finished. The HTML pages are in .build/html." pickle: mkdir -p .build/pickle .build/doctrees $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle @echo @echo "Build finished; now you can process the pickle files or run" @echo " sphinx-web .build/pickle" @echo "to start the sphinx-web server." web: pickle htmlhelp: mkdir -p .build/htmlhelp .build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in .build/htmlhelp." latex: mkdir -p .build/latex .build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex @echo @echo "Build finished; the LaTeX files are in .build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p .build/changes .build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes @echo @echo "The overview file is in .build/changes." linkcheck: mkdir -p .build/linkcheck .build/doctrees $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in .build/linkcheck/output.txt." python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/basicflow.rst0000644000175000017500000000516011157257402026521 0ustar zackzackSequence of an OpenID authentication process -------------------------------------------- ``repoze.who`` consists of several plugins which work together during the OpenID authentication sequence in the following way: 1. The user enters a page which needs authentication 2. The application raises an "401 Unauthorized" exception 3. The ``IChallengeDecider`` plugin decides that the ``IChallenge`` plugin needs to be called 4. The ``IChallenge`` plugin checks for the 401 and redirects the user to the URL defined in ``login_form_url`` 5. The user enters an OpenID into the login form and submits it. The URL given in the configuration option ``login_handler_url`` is POSTed to. 6. The ``IIdentification`` plugin detects the URL given and checks if an openid is present in the POST data. If this is the case it copies the openid into the WSGI environment so that it's read later by the ``IChallenge`` plugin (which is called after the application has done it's part, which in this case is probably returning a ``404`` error because you don't need to implement the login handler as it's handled then by the challenge plugin) 7. On egress with that ``404`` error the ``IChallengeDecider`` checks this time if an OpenID is present in the WSGI environment. If this is the case it will allow the ``IChallenge`` plugin to run 8. The ``IChallenge`` plugin checks if the URL given in ``login_handler_path`` is active and if an OpenID is present in the environment. If this is the case it will start the OpenID discovery process using the Python OpenID library. It will return a WSGI application which will redirect the user to the OpenID provider. 9. Coming back from the OpenID provider the user calls the URL given in ``login_handler_path`` again because this was the URL the plugin gave to the provider to redirect back to. The ``IIdentification`` plugin is called again on ingress and it checks again the URL to be correct and the result of the OpenID authentication (using the library). If everything was ok it stores the authenticated OpenID in the identity dict as ``repoze.who.plugins.openid.userid``. This is additionally remembered via the plugin given in the configuration option ``rememberer_name`` (usually this is ``auth_tkt``) 10. The ``IAuthenticate`` plugin is called next and converts the found openid into a userid which is returned (``None`` means that no authentication took place). The dummy authenticator shipped with this plugin will simply copy the openid over as userid. Usually you should write your own plugin which might do some database lookup to find the correct user. And this finishes the OpenID process. python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/index.rst0000644000175000017500000000102511157257402025653 0ustar zackzack.. repoze.who openid plugin documentation master file, created by sphinx-quickstart on Sat Nov 1 11:43:55 2008. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to repoze.who openid plugin's documentation! ==================================================== Contents: .. toctree:: :maxdepth: 2 installation basicflow usage api/identification Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/api/0000755000175000017500000000000011157260664024571 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/api/identification.rst0000644000175000017500000000036311157257402030312 0ustar zackzack.. _identification_module: :mod:`repoze.who.plugins.openid.identification` ----------------------------------------------- .. automodule:: repoze.who.plugins.openid.identification .. autoclass:: OpenIdIdentificationPlugin :members: python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/installation.rst0000644000175000017500000000055611157257402027255 0ustar zackzackInstallation of the plugin ========================== To install the plugin all you have to do is to use ``easy_install``:: easy_install repoze.who.plugins.openid This will install the plugin with all it's dependencies (like the OpenID library). If you use buildout you can simply add ``repoze.who.plugins.openid`` to the list of Python eggs to install. python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/HISTORY.txt0000644000175000017500000000012711157260346025717 0ustar zackzackChangelog ========= 0.5 - March 15th 2009 --------------------- * Initial release python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/usage.rst0000644000175000017500000001510511157257402025654 0ustar zackzackConfiguration ============= .. Module: repoze.who.plugins.openid.identitification The OpenID plugin is configured like all the other ``repoze.who`` plugins via the ``who.ini`` file (or what the name of it happens to be according to your main ``.ini`` file. Here is an example of the openid-plugin-section:: [plugin:openid] use = repoze.who.plugins.openid:make_identification_plugin store = file store_file_path = %(here)s/sstore openid_field = openid came_from_field = came_from error_field = error session_name = beaker.session login_form_url = /login_form login_handler_path = /do_login logout_handler_path = /logout logged_in_url = /success logged_out_url = /logout_success rememberer_name = auth_tkt A more complete example will be given below. Configuration options --------------------- Here is a list of all possible configuration options and there possible values: .. describe:: store Defines which OpenID store implementation to use. Possible values are ``mem``, ``file`` and ``sql``. Depending on what you choose here you need to give additional values such as a file path or sql connection string. ``mem`` means to use a RAM based store for OpenID associations. No further configuration is possible here. ``file`` means to use a file based store for OpenID associations. You need to give the path to the file being used as ``store_file_path`` option. ``sql`` means to use an SQL database for storing OpenID associations. You need at least give a connection string as ``store_sql_connstring`` configuration option. Additionally you can choose which tables to use. The defaults are ``oid_associations`` and ``oid_nonces`` but they can be configured by the configuration options ``store_sql_associations_table`` and ``store_sql_nonces_table`` respectively. Check the OpenID library documentation for more info on these tables and how to use the SQL store. .. warning:: The SQL implementation is not working at the moment. .. describe:: openid_field You define here in which field in the request coming from a login form the OpenID of the user is stored. Default is ``openid``. .. describe:: came_from_field ``came_from_field`` defines in which field in a request coming from the login form the URL is stored to which to redirect after successful authentication. The default is ``came_from``. .. warning:: This is not really tested and might actually not work due to the redirections of the OpenID process itself. Better use ``logged_in_url`` for this. .. describe:: error_field This directive defines in which field in the WSGI environment OpenID errors will be written should they occur. The default is ``error``. .. describe:: session_name OpenIDs requires a session for the whole login process (basically from sending the user to the OpenID provider to the provider redirecting back and checking the result). The default is to use a cookie internally. If you are using your own session middleware anyway and it's providing a dictionary interface in an WSGI environment variable then you can configure this to be used by providing the name of this variable as ``session_name``. Example:: session_name = beaker.session .. describe:: login_form_url This directive defines under which path the login page is to be found. This needs to be configured so the challenge plugin can redirect to it. The login page is supposed to ask the user for the OpenID to be used to login which then is supposed to be stored in a field named as configured with ``openid_field``. .. describe:: login_handler_path This configuration defines the URL the login form POSTs it's data to. You need to define this because the OpenID process will also use this URL to know when an OpenID authentication process is active as the OpenID provider will redirect back to this URL. The plugin will then intercept this redirect and parse the results. You don't really have to write a view for this URL as it's just there to be intercepted by this plugin. In case of a login success the user will be redirected to the URL defined in ``logged_in_url``. In case of an error the login form will be displayed again. .. describe:: logout_handler_path In order to be able to log a user out again you have to give a path which you send the user to. This URL again does not need to be implemented as a view but only serves as marker for this plugin to know. After the logout has happened the user will be redirect to the URL defined in ``logged_out_url``. .. describe:: logged_in_url Store the URL in here to which the user should be redirected after a successful login. You need to define this and you need to implement the view for it. .. describe:: logged_out_url Store the URL in here to which the user should be redirected after a successful logout. You need to define this and you need to implement the view for it. .. describe:: rememberer_name Place the name of the identification plugin here which is used to remember a successful authentication. You can e.g. configure the ``auth_tkt`` plugin as done in the ``repoze.who`` example and just put this in here:: rememberer_name = auth_tkt The result is that the openid of the user will be stored as cookie via the ``auth_tkt`` plugin. This plugin also makes it somewhat sure that the value is not plain text in the cookie but encrypted at least somewhat. Other possibilities are here to e.g. use a session for this you have anyway. Check the ``repoze.who`` documentation on how to write a rememberer plugin. The challenge decider --------------------- In order to trigger the OpenID process the challenge plugin needs to know when to really start it. Usually this needs to be done if an OpenID is present in the request. Per default the challenger is only called for ``Unauthorized`` responses from the application. In order to also trigger it for requests containing the field defined in ``openid_field`` you also have to configure a different Challengde Decider in your ``who.ini``:: [general] challenge_decider = repoze.who.plugins.openid.classifiers:openid_challenge_decider Complete example ---------------- In order to show how all the plugins work together here is a complete ``who.ini``: .. literalinclude:: who.ini :language: none python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/conf.py0000644000175000017500000001401411157257402025313 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who openid plugin documentation build configuration file, created by # sphinx-quickstart on Sat Nov 1 11:43:55 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. #sys.path.append(os.path.abspath('some/directory')) # parent = os.path.dirname(os.path.dirname(__file__)) sys.path.append(os.path.abspath(parent)) wd = os.getcwd() os.chdir(parent) os.system('%s setup.py develop -qN' % sys.executable) os.chdir(wd) print sys.executable print parent for item in os.listdir(parent): if item.endswith('.egg'): sys.path.append(os.path.join(parent, item)) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = 'repoze.who openid plugin' copyright = '2008, Christian Scholz' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. release = '1.0b1' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. #exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'quantumstyle.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. #html_logo = 'small.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['.static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'repozewhoopenidplugindoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'repozewhoopenidplugin.tex', 'repoze.who openid plugin Documentation', 'Christian Scholz', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True autoclass_content="class" python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/.templates/0000755000175000017500000000000011157260664026074 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/docs/.templates/layout.html0000644000175000017500000000176411157257402030303 0ustar zackzack{# Filename: .templates/layout.html #} {% extends '!layout.html' %} {% block relbar1 %} {% endblock %} python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/setup.py0000644000175000017500000000333411157260037024577 0ustar zackzackfrom setuptools import setup, find_packages import os version = '0.5' setup(name='repoze.who.plugins.openid', version=version, description="An OpenID plugin for repoze.who", long_description=open("README.txt").read() + "\n" + open(os.path.join("docs", "HISTORY.txt")).read(), # Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers classifiers=[ "Programming Language :: Python", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License (GPL)", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP" ], keywords='openid repoze who identification authentication plugin', author='Christian Scholz', author_email='cs@comlounge.net', url='http://quantumcore.org/docs/repoze.who.plugins.openid', license="BSD-derived (http://www.repoze.org/LICENSE.txt)", packages=find_packages(exclude=['ez_setup']), namespace_packages=['repoze', 'repoze.who', 'repoze.who.plugins'], include_package_data=True, zip_safe=False, install_requires=[ 'repoze.who>=1.0.6', 'python-openid>=2.0', 'setuptools', 'zope.interface' ], test_requires=[ ], test_suite="repoze.who.plugins.openid", entry_points=""" # -*- Entry points: -*- """, test_suite='repoze.who.plugins.openid.tests' ) python-repoze.who-plugins-20090913/repoze.who.plugins.openid-0.5/setup.cfg0000644000175000017500000000007311157260664024711 0ustar zackzack[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/0000755000175000017500000000000011153047263023044 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/README.txt0000644000175000017500000000047611146610701024544 0ustar zackzack********************************************** Collection of repoze.who friendly form plugins ********************************************** repoze.who-friendlyform is a repoze.who plugin which provides a collection of developer-friendly form plugins, although for the time being such a collection has only one item. python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/0000755000175000017500000000000011153047263031416 5ustar zackzack././@LongLink0000000000000000000000000000015700000000000011570 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/requires.txtpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/re0000644000175000017500000000004011153047263031741 0ustar zackzackrepoze.who >= 1.0 zope.interface././@LongLink0000000000000000000000000000017100000000000011564 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/namespace_packages.txtpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/na0000644000175000017500000000004511153047263031736 0ustar zackzackrepoze repoze.who repoze.who.plugins ././@LongLink0000000000000000000000000000016700000000000011571 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/dependency_links.txtpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/de0000644000175000017500000000000111153047263031720 0ustar zackzack ././@LongLink0000000000000000000000000000015300000000000011564 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/PKG-INFOpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/PK0000644000175000017500000000214111153047263031651 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who-friendlyform Version: 1.0b3 Summary: Collection of repoze.who friendly form plugins Home-page: http://code.gustavonarea.net/repoze.who-friendlyform/ Author: Gustavo Narea Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: ********************************************** Collection of repoze.who friendly form plugins ********************************************** repoze.who-friendlyform is a repoze.who plugin which provides a collection of developer-friendly form plugins, although for the time being such a collection has only one item. Keywords: web application wsgi server authentication forms repoze Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Security ././@LongLink0000000000000000000000000000016300000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/entry_points.txtpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/en0000644000175000017500000000000611153047263031737 0ustar zackzack ././@LongLink0000000000000000000000000000016000000000000011562 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/top_level.txtpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/to0000644000175000017500000000000711153047263031760 0ustar zackzackrepoze ././@LongLink0000000000000000000000000000015700000000000011570 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/not-zip-safepython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/no0000644000175000017500000000000111146557151031750 0ustar zackzack ././@LongLink0000000000000000000000000000015600000000000011567 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/SOURCES.txtpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze.who_friendlyform.egg-info/SO0000644000175000017500000000131511153047263031662 0ustar zackzackMANIFEST.in README.txt TODO.txt VERSION.txt ez_setup.py setup.cfg setup.py tests.py docs/Makefile docs/source/News.rst docs/source/conf.py docs/source/index.rst docs/source/_static/logo_hi.gif docs/source/_static/repoze.css repoze/__init__.py repoze.who_friendlyform.egg-info/PKG-INFO repoze.who_friendlyform.egg-info/SOURCES.txt repoze.who_friendlyform.egg-info/dependency_links.txt repoze.who_friendlyform.egg-info/entry_points.txt repoze.who_friendlyform.egg-info/namespace_packages.txt repoze.who_friendlyform.egg-info/not-zip-safe repoze.who_friendlyform.egg-info/requires.txt repoze.who_friendlyform.egg-info/top_level.txt repoze/who/__init__.py repoze/who/plugins/__init__.py repoze/who/plugins/friendlyform.pypython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze/0000755000175000017500000000000011153047263024350 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze/__init__.py0000644000175000017500000000170111146554207026463 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """The ``repoze`` namespace""" # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze/who/0000755000175000017500000000000011153047263025145 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze/who/__init__.py0000644000175000017500000000170511146554226027265 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """The ``repoze.who`` namespace""" # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze/who/plugins/0000755000175000017500000000000011153047263026626 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze/who/plugins/__init__.py0000644000175000017500000000171511146554240030743 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """The ``repoze.who.plugins`` namespace""" # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/repoze/who/plugins/friendlyform.py0000644000175000017500000002727011151047045031704 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """Collection of :mod:`repoze.who` friendly forms""" from urlparse import urlparse, urlunparse from urllib import urlencode try: from urlparse import parse_qs except ImportError: from cgi import parse_qs from paste.httpexceptions import HTTPFound, HTTPUnauthorized from paste.request import construct_url, parse_dict_querystring, parse_formvars from paste.response import replace_header, header_value from zope.interface import implements from repoze.who.interfaces import IChallenger, IIdentifier __all__ = ['FriendlyFormPlugin'] class FriendlyFormPlugin(object): """ :class:`RedirectingFormPlugin `-like form plugin with more features. It is like ``RedirectingFormPlugin``, but provides us with the following features: * Users are not challenged on logout, unless the referrer URL is a private one (but that's up to the application). * Developers may define post-login and/or post-logout pages. * In the login URL, the amount of failed logins is available in the environ. It's also increased by one on every login try. This counter will allow developers not using a post-login page to handle logins that fail/succeed. You should keep in mind that if you're using a post-login or a post-logout page, that page will receive the referrer URL as a query string variable whose name is "came_from". """ implements(IChallenger, IIdentifier) def __init__(self, login_form_url, login_handler_path, post_login_url, logout_handler_path, post_logout_url, rememberer_name, login_counter_name=None): """ :param login_form_url: The URL/path where the login form is located. :type login_form_url: str :param login_handler_path: The URL/path where the login form is submitted to (where it is processed by this plugin). :type login_handler_path: str :param post_login_url: The URL/path where the user should be redirected to after login (even if wrong credentials were provided). :type post_login_url: str :param logout_handler_path: The URL/path where the user is logged out. :type logout_handler_path: str :param post_logout_url: The URL/path where the user should be redirected to after logout. :type post_logout_url: str :param rememberer_name: The name of the repoze.who identifier which acts as rememberer. :type rememberer_name: str :param login_counter_name: The name of the query string variable which will represent the login counter. :type login_counter_name: str The login counter variable's name will be set to ``__logins`` if ``login_counter_name`` equals None. """ self.login_form_url = login_form_url self.login_handler_path = login_handler_path self.post_login_url = post_login_url self.logout_handler_path = logout_handler_path self.post_logout_url = post_logout_url self.rememberer_name = rememberer_name self.login_counter_name = login_counter_name if not login_counter_name: self.login_counter_name = '__logins' # IIdentifier def identify(self, environ): """ Override the parent's identifier to introduce a login counter (possibly along with a post-login page) and load the login counter into the ``environ``. """ path_info = environ['PATH_INFO'] script_name = environ.get('SCRIPT_NAME') or '/' query = parse_dict_querystring(environ) if path_info == self.login_handler_path: ## We are on the URL where repoze.who processes authentication. ## # Let's append the login counter to the query string of the # "came_from" URL. It will be used by the challenge below if # authorization is denied for this request. form = parse_formvars(environ) form.update(query) try: login = form['login'] password = form['password'] credentials = { 'login': form['login'], 'password': form['password'] } except KeyError: credentials = None referer = environ.get('HTTP_REFERER', script_name) destination = form.get('came_from', referer) if self.post_login_url: # There's a post-login page, so we have to replace the # destination with it. destination = self._get_full_path(self.post_login_url, environ) if 'came_from' in query: # There's a referrer URL defined, so we have to pass it to # the post-login page as a GET variable. destination = self._insert_qs_variable(destination, 'came_from', query['came_from']) failed_logins = self._get_logins(environ, True) new_dest = self._set_logins_in_url(destination, failed_logins) environ['repoze.who.application'] = HTTPFound(new_dest) return credentials elif path_info == self.logout_handler_path: ## We are on the URL where repoze.who logs the user out. ## form = parse_formvars(environ) form.update(query) referer = environ.get('HTTP_REFERER', script_name) came_from = form.get('came_from', referer) # set in environ for self.challenge() to find later environ['came_from'] = came_from environ['repoze.who.application'] = HTTPUnauthorized() return None elif path_info == self.login_form_url or self._get_logins(environ): ## We are on the URL that displays the from OR any other page ## ## where the login counter is included in the query string. ## # So let's load the counter into the environ and then hide it from # the query string (it will cause problems in frameworks like TG2, # where this unexpected variable would be passed to the controller) environ['repoze.who.logins'] = self._get_logins(environ, True) # Hiding the GET variable in the environ: if self.login_counter_name in query: del query[self.login_counter_name] environ['QUERY_STRING'] = urlencode(query, doseq=True) # IChallenger def challenge(self, environ, status, app_headers, forget_headers): """ Override the parent's challenge to avoid challenging the user on logout, introduce a post-logout page and/or pass the login counter to the login form. """ url_parts = list(urlparse(self.login_form_url)) query = url_parts[4] query_elements = parse_qs(query) came_from = environ.get('came_from', construct_url(environ)) query_elements['came_from'] = came_from url_parts[4] = urlencode(query_elements, doseq=True) login_form_url = urlunparse(url_parts) login_form_url = self._get_full_path(login_form_url, environ) destination = login_form_url # Configuring the headers to be set: cookies = [(h,v) for (h,v) in app_headers if h.lower() == 'set-cookie'] headers = forget_headers + cookies if environ['PATH_INFO'] == self.logout_handler_path: # Let's log the user out without challenging. came_from = environ.get('came_from') if self.post_logout_url: # Redirect to a predefined "post logout" URL. destination = self._get_full_path(self.post_logout_url, environ) if came_from: destination = self._insert_qs_variable( destination, 'came_from', came_from) else: # Redirect to the referrer URL. script_name = environ.get('SCRIPT_NAME', '') destination = came_from or script_name or '/' elif 'repoze.who.logins' in environ: # Login failed! Let's redirect to the login form and include # the login counter in the query string environ['repoze.who.logins'] += 1 # Re-building the URL: destination = self._set_logins_in_url(destination, environ['repoze.who.logins']) return HTTPFound(destination, headers=headers) # IIdentifier def remember(self, environ, identity): rememberer = self._get_rememberer(environ) return rememberer.remember(environ, identity) # IIdentifier def forget(self, environ, identity): rememberer = self._get_rememberer(environ) return rememberer.forget(environ, identity) def _get_rememberer(self, environ): rememberer = environ['repoze.who.plugins'][self.rememberer_name] return rememberer def _get_full_path(self, path, environ): """ Return the full path to ``path`` by prepending the SCRIPT_NAME. If ``path`` is a URL, do nothing. """ if path.startswith('/'): path = environ.get('SCRIPT_NAME', '') + path return path def _get_logins(self, environ, force_typecast=False): """ Return the login counter from the query string in the ``environ``. If it's not possible to convert it into an integer and ``force_typecast`` is ``True``, it will be set to zero (int(0)). Otherwise, it will be ``None`` or an string. """ variables = parse_dict_querystring(environ) failed_logins = variables.get(self.login_counter_name) if force_typecast: try: failed_logins = int(failed_logins) except (ValueError, TypeError): failed_logins = 0 return failed_logins def _set_logins_in_url(self, url, logins): """ Insert the login counter variable with the ``logins`` value into ``url`` and return the new URL. """ return self._insert_qs_variable(url, self.login_counter_name, logins) def _insert_qs_variable(self, url, var_name, var_value): """ Insert the variable ``var_name`` with value ``var_value`` in the query string of ``url`` and return the new URL. """ url_parts = list(urlparse(url)) query_parts = parse_qs(url_parts[4]) query_parts[var_name] = var_value url_parts[4] = urlencode(query_parts, doseq=True) return urlunparse(url_parts) def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/PKG-INFO0000644000175000017500000000214111153047263024137 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who-friendlyform Version: 1.0b3 Summary: Collection of repoze.who friendly form plugins Home-page: http://code.gustavonarea.net/repoze.who-friendlyform/ Author: Gustavo Narea Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: ********************************************** Collection of repoze.who friendly form plugins ********************************************** repoze.who-friendlyform is a repoze.who plugin which provides a collection of developer-friendly form plugins, although for the time being such a collection has only one item. Keywords: web application wsgi server authentication forms repoze Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Security python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/VERSION.txt0000644000175000017500000000000511153045776024734 0ustar zackzack1.0b3python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/ez_setup.py0000644000175000017500000002231311143723211025246 0ustar zackzack#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c8" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', } import sys, os def _validate_md5(egg_name, data): if egg_name in md5_data: from md5 import md5 digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re from md5 import md5 for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/TODO.txt0000644000175000017500000000057011146605501024351 0ustar zackzack* Write a function to instantiate FriendlyFormPlugin from a Paste Deploy config file. * Write a FriendlyFormPlugin-based form which only uses post-login and post-logout pages, and doesn't use the __logins query string argument. * Add the ability for FriendlyFormPlugin to use cookies instead of the "came_from" and "__logins" arguments (http://bugs.repoze.org/issue59). python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/tests.py0000644000175000017500000007172511147606267024604 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## """Test suite for the collection of :mod:`repoze.who` friendly forms.""" from unittest import TestCase from urllib import quote as original_quoter from zope.interface.verify import verifyClass from paste.httpexceptions import HTTPFound from repoze.who.interfaces import IIdentifier, IChallenger from repoze.who.plugins.friendlyform import FriendlyFormPlugin # Let's prevent the original quote() from leaving slashes: quote = lambda txt: original_quoter(txt, '') class TestFriendlyFormPlugin(TestCase): def test_implements(self): verifyClass(IIdentifier, FriendlyFormPlugin) verifyClass(IChallenger, FriendlyFormPlugin) def test_constructor(self): p = self._make_one() self.assertEqual(p.login_counter_name, '__logins') self.assertEqual(p.post_login_url, None) self.assertEqual(p.post_logout_url, None) def test_constructor_with_loging_counter_as_None(self): p = self._make_one(login_counter_name=None) self.assertEqual(p.login_counter_name, '__logins') def test_repr(self): p = self._make_one() self.assertEqual(repr(p), '' % id(p)) def test_login_without_postlogin_page(self): """ The page to be redirected to after login must include the login counter. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: came_from = '/some_path' environ = self._make_environ('/login_handler', 'came_from=%s' % quote(came_from)) # --- Testing it: p.identify(environ) app = environ['repoze.who.application'] new_redirect = came_from + '?__logins=0' self.assertEqual(app.location(), new_redirect) def test_post_login_page_as_url(self): """Post-logout pages can also be defined as URLs, not only paths""" # --- Configuring the plugin: login_url = 'http://example.org/welcome' p = self._make_one(post_login_url=login_url) # --- Configuring the mock environ: environ = self._make_environ('/login_handler') # --- Testing it: p.identify(environ) app = environ['repoze.who.application'] self.assertEqual(app.location(), login_url + '?__logins=0') def test_post_login_page_with_SCRIPT_NAME(self): """ While redirecting to the post-login page, the SCRIPT_NAME must be taken into account. """ # --- Configuring the plugin: p = self._make_one(post_login_url='/welcome_back') # --- Configuring the mock environ: environ = self._make_environ('/login_handler', SCRIPT_NAME='/my-app') # --- Testing it: p.identify(environ) app = environ['repoze.who.application'] self.assertEqual(app.location(), '/my-app/welcome_back?__logins=0') def test_post_login_page_with_SCRIPT_NAME_and_came_from(self): """ While redirecting to the post-login page with the came_from variable, the SCRIPT_NAME must be taken into account. """ # --- Configuring the plugin: p = self._make_one(post_login_url='/welcome_back') # --- Configuring the mock environ: came_from = '/something' environ = self._make_environ('/login_handler', 'came_from=%s' % quote(came_from), SCRIPT_NAME='/my-app') # --- Testing it: p.identify(environ) app = environ['repoze.who.application'] redirect = '/my-app/welcome_back?__logins=0&came_from=%s' self.assertEqual(app.location(), redirect % quote(came_from)) def test_post_login_page_without_login_counter(self): """ If there's no login counter defined, the post-login page should receive the counter at zero. """ # --- Configuring the plugin: p = self._make_one(post_login_url='/welcome_back') # --- Configuring the mock environ: environ = self._make_environ('/login_handler') # --- Testing it: p.identify(environ) app = environ['repoze.who.application'] self.assertEqual(app.location(), '/welcome_back?__logins=0') def test_post_login_page_with_login_counter(self): """ If the login counter is defined, the post-login page should receive it as is. """ # --- Configuring the plugin: p = self._make_one(post_login_url='/welcome_back') # --- Configuring the mock environ: environ = self._make_environ('/login_handler', '__logins=2', redirect='/some_path') # --- Testing it: p.identify(environ) app = environ['repoze.who.application'] self.assertEqual(app.location(), '/welcome_back?__logins=2') def test_post_login_page_with_invalid_login_counter(self): """ If the login counter is defined with an invalid value, the post-login page should receive the counter at zero. """ # --- Configuring the plugin: p = self._make_one(post_login_url='/welcome_back') # --- Configuring the mock environ: environ = self._make_environ('/login_handler', '__logins=non_integer', redirect='/some_path') # --- Testing it: p.identify(environ) app = environ['repoze.who.application'] self.assertEqual(app.location(), '/welcome_back?__logins=0') def test_post_login_page_with_referrer(self): """ If the referrer is defined, it should be passed along with the login counter to the post-login page. """ # --- Configuring the plugin: p = self._make_one(post_login_url='/welcome_back') # --- Configuring the mock environ: orig_redirect = '/some_path' came_from = quote('http://example.org') environ = self._make_environ( '/login_handler', '__logins=3&came_from=%s' % came_from, redirect=orig_redirect, ) # --- Testing it: p.identify(environ) app = environ['repoze.who.application'] new_url = '/welcome_back?__logins=3&came_from=%s' % came_from self.assertEqual(app.location(), new_url) def test_login_page_with_login_counter(self): """ In the page where the login form is displayed, the login counter must be defined in the WSGI environment variable 'repoze.who.logins'. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: environ = self._make_environ('/login', '__logins=2') # --- Testing it: p.identify(environ) self.assertEqual(environ['repoze.who.logins'], 2) self.assertEqual(environ['QUERY_STRING'], '') def test_login_page_without_login_counter(self): """ In the page where the login form is displayed, the login counter must be defined in the WSGI environment variable 'repoze.who.logins' and if it's not defined in the query string, set it to zero in the environ. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: environ = self._make_environ('/login') # --- Testing it: p.identify(environ) self.assertEqual(environ['repoze.who.logins'], 0) self.assertEqual(environ['QUERY_STRING'], '') def test_login_page_with_camefrom(self): """ In the page where the login form is displayed, the login counter must be defined in the WSGI environment variable 'repoze.who.logins' and hidden in the query string available in the environ. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: came_from = 'http://example.com' environ = self._make_environ('/login', 'came_from=%s' % quote(came_from)) # --- Testing it: p.identify(environ) self.assertEqual(environ['repoze.who.logins'], 0) self.assertEqual(environ['QUERY_STRING'], 'came_from=%s' % quote(came_from)) def test_logout_without_post_logout_page(self): """ Users must be redirected to '/' on logout if there's no referrer page and no post-logout page defined. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: environ = self._make_environ('/logout_handler') # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) self.assertEqual(app.location(), '/') def test_logout_with_SCRIPT_NAME_and_without_post_logout_page(self): """ Users must be redirected to SCRIPT_NAME on logout if there's no referrer page and no post-logout page defined. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: environ = self._make_environ('/logout_handler', SCRIPT_NAME='/my-app') # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) self.assertEqual(app.location(), '/my-app') def test_logout_with_camefrom_and_without_post_logout_page(self): """ Users must be redirected to the referrer page on logout if there's no post-logout page defined. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: environ = self._make_environ('/logout_handler') environ['came_from'] = '/somewhere' # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) self.assertEqual(app.location(), '/somewhere') def test_logout_with_post_logout_page(self): """Users must be redirected to the post-logout page, if defined""" # --- Configuring the plugin: p = self._make_one(post_logout_url='/see_you_later') # --- Configuring the mock environ: environ = self._make_environ('/logout_handler') # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) self.assertEqual(app.location(), '/see_you_later') def test_logout_with_post_logout_page_as_url(self): """Post-logout pages can also be defined as URLs, not only paths""" # --- Configuring the plugin: logout_url = 'http://example.org/see_you_later' p = self._make_one(post_logout_url=logout_url) # --- Configuring the mock environ: environ = self._make_environ('/logout_handler') # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) self.assertEqual(app.location(), logout_url) def test_logout_with_post_logout_page_and_SCRIPT_NAME(self): """ Users must be redirected to the post-logout page, if defined, taking the SCRIPT_NAME into account. """ # --- Configuring the plugin: p = self._make_one(post_logout_url='/see_you_later') # --- Configuring the mock environ: environ = self._make_environ('/logout_handler', SCRIPT_NAME='/my-app') # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) self.assertEqual(app.location(), '/my-app/see_you_later') def test_logout_with_post_logout_page_and_came_from(self): """ Users must be redirected to the post-logout page, if defined, and also pass the came_from variable. """ # --- Configuring the plugin: p = self._make_one(post_logout_url='/see_you_later') # --- Configuring the mock environ: came_from = '/the-path' environ = self._make_environ('/logout_handler') environ['came_from'] = came_from # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) redirect = '/see_you_later?came_from=%s' self.assertEqual(app.location(), redirect % quote(came_from)) def test_failed_login(self): """ Users must be redirected to the login form if the tried to log in with the wrong credentials. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: environ = self._make_environ('/somewhere') environ['repoze.who.logins'] = 1 # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) came_from = 'http://example.org/somewhere' redirect = '/login?__logins=2&came_from=%s' % quote(came_from) self.assertEqual(app.location(), redirect) def test_not_logout_and_not_failed_logins(self): """ Do not modify the challenger unless it's handling a logout or a failed login. """ # --- Configuring the plugin: p = self._make_one() # --- Configuring the mock environ: environ = self._make_environ('/somewhere') # --- Testing it: app = p.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) came_from = 'http://example.org/somewhere' redirect = '/login?came_from=%s' % quote(came_from) self.assertEqual(app.location(), redirect) def test_identify_pathinfo_miss(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/not_login_handler') result = plugin.identify(environ) self.assertEqual(result, None) self.failIf(environ.get('repoze.who.application')) def test_identify_via_login_handler(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/login_handler', login='chris', password='password', came_from='http://example.com/') result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password'}) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, 'http://example.com/?__logins=0') self.assertEqual(app.code, 302) def test_identify_via_login_handler_no_username_pass(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/login_handler') result = plugin.identify(environ) self.assertEqual(result, None) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, '/?__logins=0') self.assertEqual(app.code, 302) def test_identify_via_login_handler_no_came_from_no_http_referer(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/login_handler', login='chris', password='password') result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password'}) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, '/?__logins=0') self.assertEqual(app.code, 302) def test_identify_via_login_handler_no_came_from(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/login_handler', login='chris', password='password') environ['HTTP_REFERER'] = 'http://foo.bar/' result = plugin.identify(environ) self.assertEqual(result, {'login':'chris', 'password':'password'}) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 1) name, value = app.headers[0] self.assertEqual(name, 'location') self.assertEqual(value, 'http://foo.bar/?__logins=0') self.assertEqual(app.code, 302) def test_identify_via_login_handler_no_came_from_no_referer_sname(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/login_handler', script_name='/my-app', login='chris', password='password') plugin.identify(environ) app = environ['repoze.who.application'] self.assertEqual(app.location(), '/my-app?__logins=0') def test_identify_via_logout_handler(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/logout_handler', login='chris', password='password', came_from='http://example.com') result = plugin.identify(environ) self.assertEqual(result, None) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 0) self.assertEqual(app.code, 401) self.assertEqual(environ['came_from'], 'http://example.com') def test_identify_via_logout_handler_no_came_from_no_http_referer(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/logout_handler', login='chris', password='password') result = plugin.identify(environ) self.assertEqual(result, None) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 0) self.assertEqual(app.code, 401) self.assertEqual(environ['came_from'], '/') def test_identify_via_logout_handler_no_came_from_no_referer_spath(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/logout_handler', script_name='/my-app', login='chris', password='password') plugin.identify(environ) app = environ['repoze.who.application'] self.assertEqual(environ['came_from'], '/my-app') def test_identify_via_logout_handler_no_came_from(self): plugin = self._makeOne() environ = self._makeFormEnviron(path_info='/logout_handler', login='chris', password='password') environ['HTTP_REFERER'] = 'http://example.com/referer' result = plugin.identify(environ) self.assertEqual(result, None) app = environ['repoze.who.application'] self.assertEqual(len(app.headers), 0) self.assertEqual(app.code, 401) self.assertEqual(environ['came_from'], 'http://example.com/referer') def test_remember(self): plugin = self._makeOne() environ = self._makeFormEnviron() identity = {} result = plugin.remember(environ, identity) self.assertEqual(result, None) self.assertEqual(environ['repoze.who.plugins']['cookie'].remembered, identity) def test_forget(self): plugin = self._makeOne() environ = self._makeFormEnviron() identity = {} result = plugin.forget(environ, identity) self.assertEqual(result, None) self.assertEqual(environ['repoze.who.plugins']['cookie'].forgotten, identity ) def test_challenge(self): plugin = self._makeOne() environ = self._makeFormEnviron() app = plugin.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) sr = DummyStartResponse() result = ''.join(app(environ, sr)) self.failUnless(result.startswith('302 Found')) self.assertEqual(len(sr.headers), 3) self.assertEqual(sr.headers[1][0], 'location') url = sr.headers[1][1] import urlparse import cgi parts = urlparse.urlparse(url) parts_qsl = cgi.parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 1) came_from_key, came_from_value = parts_qsl[0] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://www.example.com/?default=1') headers = sr.headers self.assertEqual(len(headers), 3) self.assertEqual(sr.headers[0][0], 'forget') self.assertEqual(sr.headers[0][1], '1') self.assertEqual(sr.headers[2][0], 'content-type') self.assertEqual(sr.headers[2][1], 'text/plain; charset=utf8') self.assertEqual(sr.status, '302 Found') def test_challenge_came_from_in_environ(self): plugin = self._makeOne() environ = self._makeFormEnviron() environ['came_from'] = 'http://example.com/came_from' app = plugin.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) sr = DummyStartResponse() result = ''.join(app(environ, sr)) self.failUnless(result.startswith('302 Found')) self.assertEqual(len(sr.headers), 3) self.assertEqual(sr.headers[1][0], 'location') url = sr.headers[1][1] import urlparse import cgi parts = urlparse.urlparse(url) parts_qsl = cgi.parse_qsl(parts[4]) self.assertEqual(len(parts_qsl), 1) came_from_key, came_from_value = parts_qsl[0] self.assertEqual(parts[0], 'http') self.assertEqual(parts[1], 'example.com') self.assertEqual(parts[2], '/login.html') self.assertEqual(parts[3], '') self.assertEqual(came_from_key, 'came_from') self.assertEqual(came_from_value, 'http://example.com/came_from') def test_challenge_with_setcookie_from_app(self): plugin = self._makeOne() environ = self._makeFormEnviron() app = plugin.challenge( environ, '401 Unauthorized', [('app', '1'), ('set-cookie','a'), ('set-cookie','b')], []) sr = DummyStartResponse() result = ''.join(app(environ, sr)) self.failUnless(result.startswith('302 Found')) self.assertEqual(sr.headers[0][0], 'set-cookie') self.assertEqual(sr.headers[0][1], 'a') self.assertEqual(sr.headers[1][0], 'set-cookie') self.assertEqual(sr.headers[1][1], 'b') self.assertEqual(sr.headers[2][0], 'location') def test_challenge_with_non_root_script_name(self): """The script name must be taken into account while redirecting.""" plugin = self._makeOne(login_form_url='/login') environ = self._makeFormEnviron(script_name='/app', path_info='/admin') came_from = 'http://www.example.com/app/admin?default=1' environ['came_from'] = came_from app = plugin.challenge(environ, '401 Unauthorized', [('app', '1')], [('forget', '1')]) from urllib import quote login_url = '/app/login?came_from=%s' % quote(came_from, '') self.assertEqual(app.location(), login_url) def _make_one(self, login_counter_name='__logins', post_login_url=None, post_logout_url=None): p = FriendlyFormPlugin('/login', '/login_handler', post_login_url, '/logout_handler', post_logout_url, 'whatever', login_counter_name=login_counter_name) return p def _makeOne(self, login_form_url='http://example.com/login.html', login_handler_path = '/login_handler', logout_handler_path = '/logout_handler', rememberer_name='cookie'): # TODO: Merge this into _make_one() plugin = FriendlyFormPlugin(login_form_url, login_handler_path, None, logout_handler_path, None, rememberer_name) return plugin def _make_redirection(self, url): # TODO: Remove this method app = HTTPFound(url) return app def _make_environ(self, path_info, qs='', SCRIPT_NAME='', redirect=None): environ = { 'PATH_INFO': path_info, 'SCRIPT_NAME': SCRIPT_NAME, 'QUERY_STRING': qs, 'SERVER_NAME': 'example.org', 'SERVER_PORT': '80', 'wsgi.input': '', 'wsgi.url_scheme': 'http', } # TODO: Remove the ``redirect`` param if redirect: environ['repoze.who.application'] = self._make_redirection(redirect) return environ def _makeEnviron(self, kw=None): # TODO: Merge this into _make_environ environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ def _makeFormEnviron(self, login=None, password=None, came_from=None, path_info='/', identifier=None, script_name=''): # TODO: Merge this into _make_environ from StringIO import StringIO fields = [] if login: fields.append(('login', login)) if password: fields.append(('password', password)) if came_from: fields.append(('came_from', came_from)) if identifier is None: credentials = {'login':'chris', 'password':'password'} identifier = DummyIdentifier(credentials) content_type, body = encode_multipart_formdata(fields) extra = {'wsgi.input':StringIO(body), 'wsgi.url_scheme':'http', 'SERVER_NAME':'www.example.com', 'SERVER_PORT':'80', 'CONTENT_TYPE':content_type, 'CONTENT_LENGTH':len(body), 'REQUEST_METHOD':'POST', 'repoze.who.plugins': {'cookie':identifier}, 'QUERY_STRING':'default=1', 'PATH_INFO':path_info, 'SCRIPT_NAME':script_name } environ = self._makeEnviron(extra) return environ #{ Utilities def encode_multipart_formdata(fields): BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' CRLF = '\r\n' L = [] for (key, value) in fields: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"' % key) L.append('') L.append(value) L.append('--' + BOUNDARY + '--') L.append('') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body #{ Mock objects class DummyStartResponse: def __call__(self, status, headers, exc_info=None): self.status = status self.headers = headers self.exc_info = exc_info return [] class DummyIdentifier: forgotten = False remembered = False def __init__(self, credentials=None, remember_headers=None, forget_headers=None, replace_app=None): self.credentials = credentials self.remember_headers = remember_headers self.forget_headers = forget_headers self.replace_app = replace_app def identify(self, environ): if self.replace_app: environ['repoze.who.application'] = self.replace_app return self.credentials def forget(self, environ, identity): self.forgotten = identity return self.forget_headers def remember(self, environ, identity): self.remembered = identity return self.remember_headers #} python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/0000755000175000017500000000000011153047263023774 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/source/0000755000175000017500000000000011153047263025274 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/source/News.rst0000644000175000017500000000300511153046501026732 0ustar zackzack************************************ **repoze.who-friendlyform** releases ************************************ This document describes the releases of :mod:`repoze.who.plugins.friendlyform`. .. _1.0b3: **repoze.who-friendlyform** 1.0b3 (2009-03-02) ============================================== * Specified the required version of :mod:`repoze.who`, otherwise the buggy setuptools won't install it. .. _1.0b2: **repoze.who-friendlyform** 1.0b2 (2009-02-20) ============================================== * Forced the login counter name in the query string to be ``'__logins'`` even when ``login_counter_name`` is passed as ``None`` to :meth:`repoze.who.plugins.friendlyform.FriendlyFormPlugin.__init__`. The previous behavior was causing some weird problems on TG2 applications. .. _1.0b1: **repoze.who-friendlyform** 1.0b1 (2009-02-17) ============================================== This is the first release of **repoze.who-friendlyform** as an independent project. The initial form plugin, :class:`repoze.who.plugins.friendlyform.FriendlyFormPlugin`, has been moved from :class:`repoze.what.plugins.quickstart.FriendlyRedirectingFormPlugin`. This new version of ``FriendlyRedirectingFormPlugin`` doesn't extends :class:`RedirectingFormPlugin ` anymore. Instead, the relevant bits from the ``RedirectingFormPlugin`` have been copied over, as recommended by Chris McDonough. This new version of ``FriendlyRedirectingFormPlugin`` behaves exactly as the original one. python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/source/_static/0000755000175000017500000000000011153047263026722 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/source/_static/repoze.css0000644000175000017500000000047411143723232030741 0ustar zackzack@import url('default.css'); body { background-color: #006339; } div.document { background-color: #dad3bd; } div.sphinxsidebar h3,h4,h5,li,a { color: #127c56 !important; } div.related { color: #dad3bd !important; background-color: #00744a; } div.related a { color: #dad3bd !important; } python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/source/_static/logo_hi.gif0000644000175000017500000000777611143723232031046 0ustar zackzackGIF89a4polܵEEE999VVUŪ؉ǼʤჂ򠞙dcaRRQ]\\{zx???MLL444333!,4@pH,Ȥrl:Ш@Zجvzxl-z|~su2WfoTe`UXboef :7=Óʋ<Ϥ5)ݳ߼˿ڶܾՎ΢55=C!ѷm+M>o6dtu._ݘUn>؟3:XfC`[0/h uCP![ W d(]"Xr_ 9"#2dwHe.zآ0]Z}7L PeI8tO aiCà WI -*蠄ozXv@5 @d "^0e80ݪ&p"|Cnk, 06,w)>jjwiɂ< ɂ:p +[8th k(8,#h,,/U"CPCg Z6 滲˙2宐A:Ё|{J/ B ؁Pp ZP p MK C,( g l| !p'| T)Prª P t5:z|pBX*R3( D-,. A'G, @8 @A*H@>P ql+0 䯀t)T>P`&(0,=#@V`?-;h_S܃ŭqWuo @/S^&  gZUG >B^EZUmyUk-}T0]w~;ܙ9%pquxO0 h07]- K-[^`q< ]rCyPb6Gy$a>p(%%@$g- }ǀ;%y'?rgy<2Tk=>Ng~'@S2D1\4l#px(w\i׀eBxx42-%eT\?05Ph>(}`(&u$`x/"r(^@6†Vz#ehy)iVhKng(`NHz!wB.  gx%d؉mȆl\e-7p_}4z!<`P#K`h~v,0\e `74(;0H9Ȇ<8،<@1#@։T9l(؎8mȆ $ LC9'J(.`KP hs3`"#׎yS:Y: 92`X`Yh ن0i2i 0m20UTYX`FC3Xo:HXyLB7. QI׋ 0dYfy)B gpr9tYtȗ~)|Xr(RV؄ 2xMxu2yuWɈZə捔ق闫W)B8^hBy)iXyiZHz¨I py1|?P雿Iٙy Ti PTK#p:ZP`a)zzEɚ  p?2UĢâ-j-ꓮB2/:1:= pq)@9>)U.ZFKKʣ>:0ڢ,60%ʥ#$b1P3Хɥ]٦Ӷ>ijkm 0 `U+mJ  l*djjr*A7ॆj v DGiZ$b905- :J** jJ*KfK jm2x=ښrʬJšjZ ׮*:ʩk8گʮ#Gڊ{7P0hzʮ:zK = 4 y<2=7U, M9P1:˳> +@+;KE۳@;LۮO۴NS[L[P;\XO+W;efac[\K]kR Y;python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/source/index.rst0000644000175000017500000000263011153046000027122 0ustar zackzack***************************************************** Collection of :mod:`repoze.who` friendly form plugins ***************************************************** :Author: Gustavo Narea. :Latest release: |release| .. module:: repoze.who.plugins.friendlyform :synopsis: Developer-friendly repoze.who form plugins .. moduleauthor:: Gustavo Narea .. topic:: Overview **repoze.who-friendlyform** is a :mod:`repoze.who` plugin which provides a collection of developer-friendly form plugins, although for the time being such a collection has only one item. How to install ============== The minimum requirement is :mod:`repoze.who`, and you can install both with ``easy_install``:: easy_install repoze.who-friendlyform Available form plugins ====================== .. autoclass:: FriendlyFormPlugin :members: __init__ Support and development ======================= The prefered place to ask questions is the `Repoze mailing list `_ or the `#repoze `_ IRC channel. Bugs reports and feature requests should be sent to `the issue tracker of the Repoze project `_. The development mainline is available at the following Subversion repository:: http://svn.repoze.org/whoplugins/whofriendlyforms/trunk/ Releases -------- .. toctree:: :maxdepth: 2 News python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/source/conf.py0000644000175000017500000001360611146561571026606 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.what documentation build configuration file, created by # sphinx-quickstart on Mon Nov 10 20:27:30 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os here = os.path.dirname(os.path.abspath(__file__)) root = os.path.dirname(os.path.dirname(here)) # If setting up the auto(module|class) functionality: sys.path.append(os.path.abspath(root)) wd = os.getcwd() os.chdir(root) os.system('%s setup.py test -q' % sys.executable) os.chdir(wd) for item in os.listdir(root): if item.endswith('.egg'): sys.path.append(os.path.join(root, item)) # General configuration # --------------------- extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = u'Collection of repoze.who friendly form plugins' copyright = u'2009, The Repoze Project' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. release = open(os.path.join(root, 'VERSION.txt')).readline().rstrip() # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'repoze.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = '_static/logo_hi.gif' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'repozewhofriendlyformdoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'repozewhofriendlyform.tex', u'Collection of repoze.who friendly form plugins', u'Gustavo Narea', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True intersphinx_mapping = { 'http://static.repoze.org/whodocs/': None, } python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/docs/Makefile0000644000175000017500000000431311143723232025431 0ustar zackzack# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html web pickle htmlhelp latex changes linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " pickle to make pickle files (usable by e.g. sphinx-web)" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf build/* html: mkdir -p build/html build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html @echo @echo "Build finished. The HTML pages are in build/html." pickle: mkdir -p build/pickle build/doctrees $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle @echo @echo "Build finished; now you can process the pickle files or run" @echo " sphinx-web build/pickle" @echo "to start the sphinx-web server." web: pickle htmlhelp: mkdir -p build/htmlhelp build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in build/htmlhelp." latex: mkdir -p build/latex build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex @echo @echo "Build finished; the LaTeX files are in build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p build/changes build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes @echo @echo "The overview file is in build/changes." linkcheck: mkdir -p build/linkcheck build/doctrees $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in build/linkcheck/output.txt." python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/MANIFEST.in0000644000175000017500000000030711143723211024573 0ustar zackzackinclude README.txt include VERSION.txt include MANIFEST.in recursive-include docs * prune docs/build recursive-include repoze * recursive-exclude tests * global-exclude *~ *.pyc *.egg .directory python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/setup.py0000644000175000017500000000414411153046113024552 0ustar zackzack# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2009, Gustavo Narea . # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE. # ############################################################################## import os from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.txt')).read() version = open(os.path.join(here, 'VERSION.txt')).readline().rstrip() setup(name='repoze.who-friendlyform', version=version, description=('Collection of repoze.who friendly form plugins'), long_description=README, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Security" ], keywords='web application wsgi server authentication forms repoze', author='Gustavo Narea', author_email='repoze-dev@lists.repoze.org', namespace_packages = ['repoze', 'repoze.who', 'repoze.who.plugins'], url='http://code.gustavonarea.net/repoze.who-friendlyform/', license='BSD-derived (http://www.repoze.org/LICENSE.txt)', packages=find_packages(), include_package_data=True, zip_safe=False, tests_require=['repoze.who >= 1.0', 'coverage', 'nose'], install_requires=['repoze.who >= 1.0', 'zope.interface'], test_suite='nose.collector', entry_points = """\ """ ) python-repoze.who-plugins-20090913/repoze.who-friendlyform-1.0b3/setup.cfg0000644000175000017500000000052411153047263024666 0ustar zackzack[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [nosetests] cover-erase = 1 verbose = 1 cover-package = repoze.who.plugins.friendlyform verbosity = 1 with-coverage = 1 detailed-errors = 1 no-path-adjustment = 1 testmatch = ^(tests|test_.*)$ with-doctest = 1 [aliases] release = egg_info -rDb "" sdist bdist_egg register upload python-repoze.who-plugins-20090913/repoze.who-friendlyform0000777000175000017500000000000011253246076027570 2repoze.who-friendlyform-1.0b3/ustar zackzackpython-repoze.who-plugins-20090913/repoze.who-testutil0000777000175000017500000000000011253246105026274 2repoze.who-testutil-1.0rc1/ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.openid0000777000175000017500000000000011253246103030034 2repoze.who.plugins.openid-0.5/ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/0000755000175000017500000000000011061527367022526 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/VERSION0000644000175000017500000000000311061473033023555 0ustar zackzack1.0python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/0000755000175000017500000000000011061527367024032 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/__init__.py0000644000175000017500000000221211056237271026134 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who.plugins.ldap, LDAP authentication for WSGI applications. # Copyright (C) 2008 by Gustavo Narea # # This file is part of repoze.who.plugins.ldap # # # repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or any later # version. # # repoze.who.plugins.ldap 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 General # Public License for more details. # # You should have received a copy of the GNU General Public License along with # repoze.who.plugins.ldap. If not, see . # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/who/0000755000175000017500000000000011061527367024627 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/who/__init__.py0000644000175000017500000000221211056237271026731 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who.plugins.ldap, LDAP authentication for WSGI applications. # Copyright (C) 2008 by Gustavo Narea # # This file is part of repoze.who.plugins.ldap # # # repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or any later # version. # # repoze.who.plugins.ldap 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 General # Public License for more details. # # You should have received a copy of the GNU General Public License along with # repoze.who.plugins.ldap. If not, see . # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/who/plugins/0000755000175000017500000000000011061527367026310 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/who/plugins/ldap/0000755000175000017500000000000011061527367027230 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/who/plugins/ldap/plugins.py0000644000175000017500000001773111061314722031261 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who.plugins.ldap, LDAP authentication for WSGI applications. # Copyright (C) 2008 by Gustavo Narea # # This file is part of repoze.who.plugins.ldap # # # repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or any later # version. # # repoze.who.plugins.ldap 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 General # Public License for more details. # # You should have received a copy of the GNU General Public License along with # repoze.who.plugins.ldap. If not, see . """LDAP plugins for repoze.who.""" __all__ = ['LDAPAuthenticatorPlugin', 'LDAPAttributesPlugin'] from zope.interface import implements import ldap from repoze.who.interfaces import IAuthenticator, IMetadataProvider #{ Authenticators class LDAPAuthenticatorPlugin(object): implements(IAuthenticator) def __init__(self, ldap_connection, base_dn): """Create an LDAP authentication plugin. By passing an existing LDAPObject, you're free to use the LDAP authentication method you want, the way you want. If the default way to find the DN is not suitable for you, you may want to override L{_get_dn}. This plugin is compatible with any identifier plugin that defines the C{login} and C{password} items in the I{identity} dictionary. @param ldap_connection: An initialized LDAP connection. @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject} @param base_dn: The base for the I{Distinguished Name}. Something like C{ou=employees,dc=example,dc=org}, to which will be prepended the user id: C{uid=jsmith,ou=employees,dc=example,dc=org}. @type base_dn: C{unicode} @raise ValueError: If at least one of the parameters is not defined. """ if base_dn is None: raise ValueError('A base Distinguished Name must be specified') self.ldap_connection = make_ldap_connection(ldap_connection) self.base_dn = base_dn # IAuthenticatorPlugin def authenticate(self, environ, identity): """Return the Distinguished Name of the user to be authenticated. @attention: The uid is not returned because it may not be unique; the DN, on the contrary, is always unique. @return: The Distinguished Name (DN), if the credentials were valid. @rtype: C{unicode} or C{None} """ try: dn = self._get_dn(environ, identity) password = identity['password'] except (KeyError, TypeError, ValueError): return None if not hasattr(self.ldap_connection, 'simple_bind_s'): environ['repoze.who.logger'].warn('Cannot bind with the provided ' 'LDAP connection object') return None try: self.ldap_connection.simple_bind_s(dn, password) # The credentials are valid! return dn except ldap.LDAPError: return None def _get_dn(self, environ, identity): """ Return the DN based on the environment and the identity. It prepends the user id to the base DN given in the constructor: If the C{login} item of the identity is C{rms} and the base DN is C{ou=developers,dc=gnu,dc=org}, the resulting DN will be: C{uid=rms,ou=developers,dc=gnu,dc=org}. @attention: You may want to override this method if the DN generated by default doesn't meet your requirements. If you do so, make sure to raise a C{ValueError} exception if the operation is not successful. @param environ: The WSGI environment. @param identity: The identity dictionary. @return: The Distinguished Name (DN) @rtype: C{unicode} @raise ValueError: If the C{login} key is not in the I{identity} dict. """ try: return u'uid=%s,%s' % (identity['login'], self.base_dn) except (KeyError, TypeError): raise ValueError def __repr__(self): return '<%s %s>' % (self.__class__.__name__, id(self)) #{ Metadata providers class LDAPAttributesPlugin(object): """Loads LDAP attributes of the authenticated user.""" implements(IMetadataProvider) def __init__(self, ldap_connection, attributes=None, filterstr='(objectClass=*)'): """ Fetch LDAP attributes of the authenticated user. @param ldap_connection: The LDAP connection to use to fetch this data. @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject} or C{str} @param attributes: The authenticated user's LDAP attributes you want to use in your application; an interable or a comma-separate list of attributes in a string, or C{None} to fetch them all. @type attributes: C{iterable} or C{str} @param filterstr: A filter for the search, as documented in U{RFC4515 }; the results won't be filtered unless you define this. @type filterstr: C{str} @raise ValueError: If L{make_ldap_connection} could not create a connection from C{ldap_connection}, or if C{attributes} is not an iterable. """ if hasattr(attributes, 'split'): attributes = attributes.split(',') elif hasattr(attributes, '__iter__'): # Converted to list, just in case... attributes = list(attributes) elif attributes is not None: raise ValueError('The needed LDAP attributes are not valid') self.ldap_connection = make_ldap_connection(ldap_connection) self.attributes = attributes self.filterstr = filterstr # IMetadataProvider def add_metadata(self, environ, identity): """ Add metadata about the authenticated user to the identity. It modifies the C{identity} dictionary to add the metadata. @param environ: The WSGI environment. @param identity: The repoze.who's identity dictionary. """ # Search arguments: args = ( identity.get('repoze.who.userid'), ldap.SCOPE_BASE, self.filterstr, self.attributes ) try: for (dn, attributes) in self.ldap_connection.search_s(*args): identity.update(attributes) except ldap.LDAPError, msg: environ['repoze.who.logger'].warn('Cannot add metadata: %s' % \ msg) return #{ Utilities def make_ldap_connection(ldap_connection): """Return an LDAP connection object to the specified server. If the C{ldap_connection} is already an LDAP connection object, it will be returned as is. If it's an LDAP URL, it will return an LDAP connection to the LDAP server specified in the URL. @param ldap_connection: The LDAP connection object or the LDAP URL of the server to be connected to. @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject}, C{str} or C{unicode} @return: The LDAP connection object. @rtype: C{ldap.ldapobject.SimpleLDAPObject} @raise ValueError: If C{ldap_connection} is C{None}. """ if isinstance(ldap_connection, str) or isinstance(ldap_connection, unicode): return ldap.initialize(ldap_connection) elif ldap_connection is None: raise ValueError('An LDAP connection must be specified') return ldap_connection #} python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/who/plugins/ldap/__init__.py0000644000175000017500000000333411061250424031327 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who.plugins.ldap, LDAP authentication for WSGI applications. # Copyright (C) 2008 by Gustavo Narea # # This file is part of repoze.who.plugins.ldap # # # repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or any later # version. # # repoze.who.plugins.ldap 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 General # Public License for more details. # # You should have received a copy of the GNU General Public License along with # repoze.who.plugins.ldap. If not, see . """repoze.who LDAP plugin U{repoze.who.plugins.ldap } is a Python package that provides U{repoze.who } plugins for U{LDAP } authentication in U{WSGI } applications. It can be used with any LDAP server and any WSGI framework (or no framework at all). For information on how to get started, you may want to visit its web site: U{http://code.gustavonarea.net/repoze.who.plugins.ldap/} G{packagetree} """ import ldap from repoze.who.plugins.ldap.plugins import LDAPAuthenticatorPlugin, \ LDAPAttributesPlugin __all__ = ['LDAPAuthenticatorPlugin', 'LDAPAttributesPlugin'] python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/who/plugins/ldap/tests.py0000644000175000017500000002375411061271442030745 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who.plugins.ldap, LDAP authentication for WSGI applications. # Copyright (C) 2008 by Gustavo Narea # # This file is part of repoze.who.plugins.ldap # # # repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or any later # version. # # repoze.who.plugins.ldap 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 General # Public License for more details. # # You should have received a copy of the GNU General Public License along with # repoze.who.plugins.ldap. If not, see . """Test suite for repoze.who.plugins.ldap""" import unittest from dataflake.ldapconnection.tests import fakeldap from ldap import modlist from ldap.ldapobject import SimpleLDAPObject from zope.interface.verify import verifyClass from repoze.who.interfaces import IAuthenticator, IMetadataProvider from repoze.who.plugins.ldap import LDAPAuthenticatorPlugin, \ LDAPAttributesPlugin from repoze.who.plugins.ldap.plugins import make_ldap_connection class Base(unittest.TestCase): """Base test case for the plugins""" def setUp(self): # Connecting to a fake server with a fake account: conn = fakeldap.initialize('ldap://example.org') conn.simple_bind_s('Manager', 'some password') # Adding a fake user, which is used in the tests person_attr = {'cn': [fakeuser['cn']], 'uid': fakeuser['uid'], 'userPassword': [fakeuser['hashedPassword']]} conn.add_s(fakeuser['dn'], modlist.addModlist(person_attr)) self.connection = conn # Creating a fake environment: self.env = self._makeEnviron() def tearDown(self): self.connection.delete_s(fakeuser['dn']) def _makeEnviron(self, kw=None): """Create a fake WSGI environment This is based on the same method of the test suite of repoze.who. """ environ = {} environ['wsgi.version'] = (1,0) if kw is not None: environ.update(kw) return environ #{ Test cases for the plugins class TestMakeLDAPAuthenticatorPlugin(unittest.TestCase): """Tests for the constructor of the L{LDAPAuthenticatorPlugin} plugin""" def test_without_connection(self): self.assertRaises(ValueError, LDAPAuthenticatorPlugin, None, 'dc=example,dc=org') def test_without_base_dn(self): conn = fakeldap.initialize('ldap://example.org') self.assertRaises(TypeError, LDAPAuthenticatorPlugin, conn) self.assertRaises(ValueError, LDAPAuthenticatorPlugin, conn, None) def test_with_connection(self): conn = fakeldap.initialize('ldap://example.org') LDAPAuthenticatorPlugin(conn, 'dc=example,dc=org') def test_connection_is_url(self): LDAPAuthenticatorPlugin('ldap://example.org', 'dc=example,dc=org') class TestLDAPAuthenticatorPlugin(Base): """Tests for the L{LDAPAuthenticatorPlugin} IAuthenticator plugin""" def setUp(self): super(TestLDAPAuthenticatorPlugin, self).setUp() # Loading the plugin: self.plugin = LDAPAuthenticatorPlugin(self.connection, base_dn) def test_implements(self): verifyClass(IAuthenticator, LDAPAuthenticatorPlugin, tentative=True) def test_authenticate_nologin(self): result = self.plugin.authenticate(self.env, None) self.assertEqual(result, None) def test_authenticate_incomplete_credentials(self): identity1 = {'login': fakeuser['uid']} identity2 = {'password': fakeuser['password']} result1 = self.plugin.authenticate(self.env, identity1) result2 = self.plugin.authenticate(self.env, identity2) self.assertEqual(result1, None) self.assertEqual(result2, None) def test_authenticate_noresults(self): identity = {'login': 'i_dont_exist', 'password': 'super secure password'} result = self.plugin.authenticate(self.env, identity) self.assertEqual(result, None) def test_authenticate_comparefail(self): identity = {'login': fakeuser['uid'], 'password': 'wrong password'} result = self.plugin.authenticate(self.env, identity) self.assertEqual(result, None) def test_authenticate_comparesuccess(self): identity = {'login': fakeuser['uid'], 'password': fakeuser['password']} result = self.plugin.authenticate(self.env, identity) self.assertEqual(result, fakeuser['dn']) def test_custom_authenticator(self): """L{LDAPAuthenticatorPlugin._get_dn} should be overriden with no problems""" plugin = CustomLDAPAuthenticatorPlugin(self.connection, base_dn) identity = {'login': fakeuser['uid'], 'password': fakeuser['password']} result = plugin.authenticate(self.env, identity) expected = 'uid=%s,ou=admins,%s' % (fakeuser['uid'], base_dn) self.assertEqual(result, expected) class TestMakeLDAPAttributesPlugin(unittest.TestCase): """Tests for the constructor of L{LDAPAttributesPlugin}""" def test_connection_is_invalid(self): self.assertRaises(ValueError, LDAPAttributesPlugin, None, 'cn') def test_attributes_is_none(self): """If attributes is None then fetch all the attributes""" plugin = LDAPAttributesPlugin('ldap://localhost', None) self.assertEqual(plugin.attributes, None) def test_attributes_is_comma_separated_str(self): attributes = "cn,uid,mail" plugin = LDAPAttributesPlugin('ldap://localhost', attributes) self.assertEqual(plugin.attributes, attributes.split(',')) def test_attributes_is_only_one_as_str(self): attributes = "mail" plugin = LDAPAttributesPlugin('ldap://localhost', attributes) self.assertEqual(plugin.attributes, ['mail']) def test_attributes_is_iterable(self): # The plugin, with a tuple as attributes attributes_t = ('cn', 'mail') plugin_t = LDAPAttributesPlugin('ldap://localhost', attributes_t) self.assertEqual(plugin_t.attributes, list(attributes_t)) # The plugin, with a list as attributes attributes_l = ['cn', 'mail'] plugin_l = LDAPAttributesPlugin('ldap://localhost', attributes_l) self.assertEqual(plugin_l.attributes, attributes_l) # The plugin, with a dict as attributes attributes_d = {'first': 'cn', 'second': 'mail'} plugin_d = LDAPAttributesPlugin('ldap://localhost', attributes_d) self.assertEqual(plugin_d.attributes, list(attributes_d)) def test_attributes_is_not_iterable_nor_string(self): self.assertRaises(ValueError, LDAPAttributesPlugin, 'ldap://localhost', 12345) def test_parameters_are_valid(self): LDAPAttributesPlugin('ldap://localhost', 'cn', '(objectClass=*)') class TestLDAPAttributesPlugin(Base): """Tests for the L{LDAPAttributesPlugin} IMetadata plugin""" def test_implements(self): verifyClass(IMetadataProvider, LDAPAttributesPlugin, tentative=True) def test_add_metadata(self): plugin = LDAPAttributesPlugin(self.connection) environ = {} identity = {'repoze.who.userid': fakeuser['dn']} expected_identity = { 'repoze.who.userid': fakeuser['dn'], 'cn': [fakeuser['cn']], 'userPassword': [fakeuser['hashedPassword']], 'uid': fakeuser['uid'] } plugin.add_metadata(environ, identity) self.assertEqual(identity, expected_identity) # Test cases for plugin utilities class TestLDAPConnectionFactory(unittest.TestCase): """Tests for L{make_ldap_connection}""" def test_connection_is_object(self): conn = fakeldap.initialize('ldap://example.org') self.assertEqual(make_ldap_connection(conn), conn) def test_connection_is_str(self): conn = make_ldap_connection('ldap://example.org') self.assertTrue(isinstance(conn, SimpleLDAPObject)) def test_connection_is_unicode(self): conn = make_ldap_connection(u'ldap://example.org') self.assertTrue(isinstance(conn, SimpleLDAPObject)) def test_connection_is_none(self): self.assertRaises(ValueError, make_ldap_connection, None) #{ Fixtures base_dn = 'ou=people,dc=example,dc=org' fakeuser = { 'dn': 'uid=carla,%s' % base_dn, 'uid': 'carla', 'cn': 'Carla Paola', 'mail': 'carla@example.org', 'password': 'hello', 'hashedPassword': '{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00=' } class CustomLDAPAuthenticatorPlugin(LDAPAuthenticatorPlugin): """Fake class to test that L{LDAPAuthenticatorPlugin._get_dn} can be overriden with no problems""" def _get_dn(self, environ, identity): try: return u'uid=%s,ou=admins,%s' % (identity['login'], self.base_dn) except (KeyError, TypeError): raise ValueError, ('Could not find the DN from the identity and ' 'environment') #} def suite(): """ Return the test suite. @return: The test suite for the plugin. @rtype: C{unittest.TestSuite} """ suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestMakeLDAPAuthenticatorPlugin, "test")) suite.addTest(unittest.makeSuite(TestLDAPAuthenticatorPlugin, "test")) suite.addTest(unittest.makeSuite(TestMakeLDAPAttributesPlugin, "test")) suite.addTest(unittest.makeSuite(TestLDAPAttributesPlugin, "test")) suite.addTest(unittest.makeSuite(TestLDAPConnectionFactory, "test")) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze/who/plugins/__init__.py0000644000175000017500000000221211056237272030413 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who.plugins.ldap, LDAP authentication for WSGI applications. # Copyright (C) 2008 by Gustavo Narea # # This file is part of repoze.who.plugins.ldap # # # repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or any later # version. # # repoze.who.plugins.ldap 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 General # Public License for more details. # # You should have received a copy of the GNU General Public License along with # repoze.who.plugins.ldap. If not, see . # See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages try: __import__('pkg_resources').declare_namespace(__name__) except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/LICENSE0000644000175000017500000007733011056226737023546 0ustar zackzack GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/PKG-INFO0000644000175000017500000000463411061527367023632 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who.plugins.ldap Version: 1.0 Summary: LDAP plugin for repoze.who Home-page: http://code.gustavonarea.net/repoze.who.plugins.ldap/ Author: Gustavo Narea Author-email: me@gustavonarea.net License: GNU General Public License v3 Download-URL: https://launchpad.net/repoze.who.plugins.ldap/+download Description: repoze.who.plugins.ldap -- LDAP Authentication for WSGI Applications repoze.who.plugins.ldap is an LDAP plugin for the identification and authentication framework for WSGI applications, repoze.who, which acts as WSGI middleware. It provides with an straightforward solution to enable LDAP support in your applications. Yes, you read well: "straightforward", "LDAP" and "applications" are in the same sentence. In fact, you may make your application LDAP-aware in few minutes and with few lines of code. Another great news is that this package is *fully* documented and provides you with a working and documented demo project. See the `docs` subdirectory of this package for more information, or browse the online documentation . repoze.who.plugins.ldap Changelog ================================= 1.0 (2008-09-11) ------------------------------- The initial release. - Provided the LDAP authenticator, which is compatible with identifiers that define the 'login' item in the identity dict. - Included the plugin to load metadata about the authenticated user from the LDAP server. - Documented how to install and use the plugins. - Included Turbogears 2 demo project, using the plugin. There is also a section in the documentation to explain how the demo works. Keywords: ldap web application server wsgi repoze repoze.who Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/0000755000175000017500000000000011061527367023452 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/test.ini0000644000175000017500000000100011057541272025115 0ustar zackzack# # LDAPAuth - TurboGears 2 testing environment configuration # # The %(here)s variable will be replaced with the parent directory of this file # [DEFAULT] debug = true # Uncomment and replace with the address which should receive any error reports # email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 0.0.0.0 port = 5000 [app:main] use = config:development.ini # Add additional test specific configuration options as necessary. python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/0000755000175000017500000000000011061527367025254 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/tests/0000755000175000017500000000000011061527367026416 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/tests/functional/0000755000175000017500000000000011061527367030560 5ustar zackzack././@LongLink0000000000000000000000000000015200000000000011563 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/tests/functional/__init__.pypython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/tests/functional/__init0000644000175000017500000000000011057541272031726 0ustar zackzack././@LongLink0000000000000000000000000000015300000000000011564 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/tests/functional/test_root.pypython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/tests/functional/test_r0000644000175000017500000000151511057541272032001 0ustar zackzack# -*- coding: utf-8 -*- from ldapauth.tests import TestController # This is an example of how you can write functional tests for your controller. # As opposed to a pure unit-test which test a small unit of functionallity, # these functional tests exercise the whole app and it's WSGI stack. # Please read http://pythonpaste.org/webtest/ for more information class TestRootController(TestController): def test_index(self): response = self.app.get('/') msg = 'TurboGears 2 is rapid web application development toolkit '\ 'designed to make your life easier.' # You can look for specific strings: self.failUnless(msg in response) # You cam also access a BeautifulSoup'ed version links = response.html.findAll('a') self.failUnless(links, "Mummy, there are no links here!") python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/tests/__init__.py0000644000175000017500000000234411057541272030526 0ustar zackzack"""Pylons application test package When the test runner finds and executes tests within this directory, this file will be loaded to setup the test environment. It registers the root directory of the project in sys.path and pkg_resources, in case the project hasn't been installed with setuptools. It also initializes the application via websetup (paster setup-app) with the project's test.ini configuration file. """ import os import sys from unittest import TestCase import pkg_resources import webtest import paste.script.appinstall from paste.deploy import loadapp from routes import url_for __all__ = ['url_for', 'TestController'] here_dir = os.path.dirname(os.path.abspath(__file__)) conf_dir = os.path.dirname(os.path.dirname(here_dir)) sys.path.insert(0, conf_dir) pkg_resources.working_set.add_entry(conf_dir) pkg_resources.require('Paste') pkg_resources.require('PasteScript') test_file = os.path.join(conf_dir, 'test.ini') cmd = paste.script.appinstall.SetupCommand('setup-app') cmd.run([test_file]) class TestController(TestCase): def __init__(self, *args, **kwargs): wsgiapp = loadapp('config:test.ini', relative_to=conf_dir) self.app = webtest.TestApp(wsgiapp) TestCase.__init__(self, *args, **kwargs) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/tests/test_models.py0000644000175000017500000000263311057541272031312 0ustar zackzack# -*- coding: utf-8 -*- """Test suite for the TG app's models""" from tg.testutil import DBTest from sqlalchemy import create_engine from ldapauth import model test_database = create_engine("sqlite:///:memory:") class TestModel(DBTest): """The base class for testing models in you TG project.""" model = model database = test_database class TestUser(TestModel): """Test case for the User model.""" def setUp(self): super(TestUser, self).setUp() self.member = model.User() self.member.user_name = u"ignucius" self.member.email_address = u"ignucius@example.org" def test_member_creation_username(self): """The member constructor must set the user name right""" self.assertEqual(self.member.user_name, u"ignucius") def test_member_creation_email(self): """The member constructor must set the email right""" self.assertEqual(self.member.email_address, u"ignucius@example.org") def test_no_permissions_by_default(self): """User objects should have no permission by default.""" self.assertEqual(len(self.member.permissions), 0) def test_getting_by_email(self): """Users should be fetcheable by their email addresses""" model.DBSession.add(self.member) him = model.User.by_email_address(u"ignucius@example.org") self.assertEqual(him, self.member) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/0000755000175000017500000000000011061527367026532 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/0000755000175000017500000000000011061527367027777 5ustar zackzack././@LongLink0000000000000000000000000000014600000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/tg2_04.gifpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/tg2_04.gi0000644000175000017500000005436711057541271031331 0ustar zackzackGIF89a    !!"$"%*))%,+,,.22-3$3233 4883$9-:4<;=<>;>BB=DCDE EIFDHE I>IEJI&KJKKLK LPPM QDRLSQSR SRSRTR&XX6ZW[T[V ^](c[ cb7cbHdZd`ib jcjkFldqkrkrm#rnrqUurH|r}q}s}|Q^z l{{&>c -/w!i; -v .>!>.SB+>*ůESǰ/Ȯ#ȱ:˟ƘͭήТ,ӑֺH*԰ض,٫+ٲ:I۱;ܶߢ4J(-(GT#D<ބ;,2$"#HT ,2? #$$" #&$)I[3+;4$F:#E+3+39+3:C$S,3N(gcqTҐ)CPq-԰B %by##耗1p],\).𥀭Y"c )ȐF0!E oBV\) Z )psf `@9΀^1 @ k`@K`FqtbL)Ƞrj2APCRiu  pbe4Pm2e f6(/RJ/" %2)I&`XkÉYXmRB =W,(1d~ IfNYJ-EuGCH2@& ȅdbx1|Adu-vF8\KH0(z;(2 =Az%O<X Xߍ I1%0`6[d~Z+.(7@ /f]. eP ?0`a4uG(7 D BA\#2q ِ85Saz#ߊ:`@r}TиVa\1 aX .W($dub/[x-;x0D딑 Ib#U zwk;^"z8yZbЁ%V@8Qkn+xЈ2=#8 ߆ ZP  L/wCl` _ I !V6"SW=-pYlP3Ɓ`=.s]7x20a g x@\X@^]7`y=@ 1X ;Za Z$$]]h!? BC6>Y'W %XP fPq@ àc 0 dC$ p<p ( (( d?V  A54~"y#y ^۴E0 րkP]Mf2 P`z] ?O0@ `@p P  ?" $ eW|Un"Ђ 8^xqY_3:ȡ!WqVc~2#" W@ R#2P0 P4  u )1 à 0 ` Ā F R  ` nh L@A:hG `xQ+y1  QPVw8AB\0 P9( n`Ӑ p3P 0 % 6 1B q0 [ d` X W5pe MWv$QWL.+Pai@FP Q;d1)GA)W @ 0 `  ) Wߠ @ ̰ ɰ  $ d 0 @s B`a78 P\@gI \e>)P dʴV-va` xINx3)CO>1|mi  ~9 ΐ q2pg ɐ  F@X p }=1` ` `I |O)#@ 0\? & SU@TC!#)w)w ` ŀ݉ ` 1'\iU ð ` 11$;F(yiIА: @ u9P(ǜii@ z <#ٰ p i<  Pah K0Mv:{p ?ݶB67 P^;"X4 A)& @e |Y @ nMw|O(@ @3j~2" PPǐ p  .9KpQdQR hwn bCBS7`0D##3ϲ#PP0ҹ0!0?0Ed5B 0 9 QG#%0w@ D . @OK:Xn F } 8 P^=@CHGG>4g;h1)P0 4A9P  d x(? L@AeP H" Ġ ;v iwkzW3M)uv@Mp7l YJЮՅT;b|l;2M7$@2BbP 9.w hHO9 p @| e0M@ YŸlHQt5 @ YO;ծurASu 氊` ŀ % P  ʃ  pR_`5d7VP @ S *Mx%  P'!PjH5^5vl"-/#9p ;, З@\Pq@ =X Ő 50Pμ % /0\y%v^L$+;BAP ӟA` q@q0k۰?9 ?1 W D٠U iTW@ pR' !(^C :e]zv\ذۮPY`{ /* p Ѐ)k  fͧWXA ̐ bmcRX 0  1* Zŗi*0Hymֺ 59aiIe jءQ R`;?pk<0`)'P1Y P `  q#ư T@ f  - ,vp̀ ̼S9AA~kZ71@ @ . P0Ȱ͐ .0 `2YP ` Ͱ X#6pWx)0P Chœj 0~;Pu}@ +)PppLB fu  0M!L z` 0 Ұ x]p3f p̪pwzX { n3 zqFٍ;ŧ0}ӇjEY:0)Fx"0-p8l4d~Y˖]֚}Kƈd}el"E2H$\`Akرev) '#;t)tiY5Rm}3]"vh Oݗƚezqa#~ER((E#_.H4hVmDK4h Zl  )8qy1 XsIvyO|jfSjvlqLrPAAfsdQaZ.EPX2^ŗMDL3`@`1'&8zT%zJAx9ǝ$!NZ!pW7FhARiqq&f'lԉzizRIA]:f~IcafwcG10>6xҔX`Ttx)watpy`)2Rb* KIm!"@qgde0Aif>gz1H4q7EӅU8E^^fM8`ER!TQ$hq1 yr'B9y'@qnv:x1j;hv)'IqR'h@ croZhi&dij]Řw|{Tpmуys[*ǝq)2vFmLIgbPE/Q ݮCX$,X6hD ۚ* <SK#W(F"3j|a|xLhĒ+ym`bIO8Tdch~Q_gE, Zc`6Y@i06#čy!Q@u9xA @qCEb_?|'@adAS3 _B ])H@nb )*نQM@KxG͍y!}09x0By8eoI!hFxO;A 8 m`ycƇ@&Zyx!LѬJbx\E(zӇ|J -\x. %"*p0F5G`;wG Q ~\}В6I7l.cy5?`T#`%Kի,{4~8%1ЉZPYt)&x̺& q [@q]o{k2Aug;&eg[%cPUPm.\Qʊ@\q .:&, h!ǜ+Q q:le?ڡs(_8\*NZ 2l"Сm``$7 qPv~wf`*ঢC<9?< @+`@]]zCF ޸-88xeut^6hV@=ty XA.: 4Gϕ`2 ?kUx 8{4<<}G:0G2M9X90=Tk0z/`c=)@$@Ĕ.: [-XAa$z p h2}qqq@ ${ '(S,l)[A+3 )hBp B1 9XA( (TA3 lf#:c=.1 ? 3[:śs۸d=@AEcx S´>ecwx&?۾0$Ebt k)HF[h 2XFF,_AKb;+t<ȔH@2C3=x )S$84K%MrĨCHt3.34.TE(D6V3,RзCI4h$0BchESC@eQAf--LJR s>܁&  Ё D!s8[a GzyA j;6HzL>DK9P,8!07hg@hA~ ېpNDKx(dO*88M9ÂZb 3LJШS|:<538lK6N\LlZ3 QWa=;:b93QQOkOSs$'Ф8 >$QembÊ<u6#.ܸeRA@x*R]pT2(ދ"6N,ûH0֐R'@.4 uV= LUQۼQ4H8M5\DpV9XL@AUxV}}ôH?X=S*xFr ,B'tLg,MpIEu)U!yhSڥa. xل]rMepV1hTAqeryI38[-=>< p#5t )MvE14jPę]@MU#UL/zٹqŁr}HXeeu*A0 S5Uh IM՛M8_}j\ehDdW<v- ܩH ReM,4\y<ȥ~40W_"8#ȁY L@h7,>PR|5?J^뽻U3+7:6hC6w(-(vE!~xأƊĀ^QVSL443Rw@0XMPSj剩 Hg$Vg|yL`!L!>q4+ߺ% 8l3RNKF$Ϳ^ݛ_c-UDȁ#-&5%054hQ#1xRbNk9#`8'ؼ 0R1 8()9hICc3 1j!7Md҆;.;E6='މbp՞eeBN56r^#Wbo6%j* p"+/`))HZ d!df]Nޛ^{g_6nel#2Val-FS  ("h^݊VdJˀ j}Dg;k]E֛z_q$"MW9!I>h6: nL3٠y$<ƫ3i>n(@]f*cs&V.if-&k:J 0 Kk"ZbwPZvD[vękVkn.h!軈3Q\UlV839ۉska9^i:fӻS ߫d1Vm֑xt&faY$nvkk߮P X]<45jUk]]iHkOE]raQf0 n`d{8;:Do6*Z}X1HI6phw>`Q9]AP-C+`T}`.}K@ 5`.6qomɭX6dA G$OMo4ՇR٘S='`B($ /Xr撒/䩘?a.wnl3ǩ*'ۺjc3Їr(.i:/p$UчF5Q: >D#qq4,a{h [,#r_n} nZ3r  `*Sc[5?PhŇMZʻ_, B9wq2$xqSHH_ 4C[8`,t{,z@L;t8UZiY4ȁB70$9 $QrVy(wZsT/PY9: g;nn2΍zFRXȖJv(jz%ަGz2 <kvyLhCLp=ow]gLD}t"sX}`%Icn{4΍^rwPyywQW  pp\b]UN, 8pqw?yns}[nd#Wbͺw0Խ6%U4:(7gp˃乹 gn }ۂ ׍3VhbF чN8pQΜ0rdw\;$ʔ*WlK fN@,x g߼J2m4={*֬Z]v @,b)(HÃ,ԕ"Ŏ=b˷9mj0V7kΔL `@C)Ahn5@ mh"%̅qbE?͘`u՞2M:bH::4zsbt1 Uwa 40B a>@T]3Ga^>&շ!K$4S 8J%98ov σCZb؁3ՊAP$c4PRa̔A QXҁ  :P .CEA @|S 3Ì5 -N9Y997Gȝ0݁l>ʹMPw0phm@`fxϋEg@3d  ,Pg2uyQD0G7n32\4R`94pԠf h#ngR+QI&sL`3 |BOX%2@ƕqklF3 Y)q 8BvC0`Jh`Mn%y`VB8s j/yF;do  ` $ ~$dA +0`4I,Ȃ:ar11BDգ#B+UI5mjU7 j JQrAy?IO.v R!=VLv%hc@H}]*!H`Յx.~qFP]JcSb)~ȁvGtX @Μ'Iq4~h@0+ X\ *`-mp zE`1< k0cz`KKʈ8j/2AVM! c~9 XPdę zeuM͂%>Ctd0@9m3 |Ch8,c2C̘q.l$EeG >C`1|%GcW4y uF @X@@C:Scq!@M]_--v@ A$-ȑdLMՌq e JP]!2#@<<`A x|oUXCᔙhel<44@hA ptנS_}OEGF̙_ `8209ʤ~C'ͤ 84:`4e!&!:eN 8"Y\7Xƃ;e(,X@#b ?)Mn4$ A@ 9B4+S Z@ ;hT(@ hAB;CABb[ v%k@4TK=B ؙ>IC+b@%N@MYe*C=%Ut C)P#ɡ|B5W3`8N-pTĆy5 T-@ j ß^D=]$FBMy+AC Xb@X4+k*C?LkD 6|B.xP380L9|F58i 92 C\l@5EUBȟ8El@ùC̓HAæ؆+brlz-]mJ܃@ 28)644iLmvX4C) @vQ vY |8T4Cx._`C(B LBs<<\xU7C%LXNUT=aEC(C @ BnXEA4ܢhc@X:.:/M,&4TdF,DϞY$T@@yY8>eYO69LB<;sN 8t,D@r+6XpDD@kV`=jeo<" ru@w f @t\S`/pJA h@1øC$4A\7d/XQ}&4hA9X38Tυ1,7A,5d;bA`A89;79|< p܁`6><\C `AWH9 0.4ZnM X*<'.ٙ-rI@D7 LR$$0ܒ{ޒ5N5;\Մk,H4T C6Ct!6|-X;HX p5U Y~g%pؒIiGI|%8G Sc[6/3")TCׯ4.8p20Cs#B QKG59tD 9xS! &&vǁވC56ܺ 8C 0XdLG{֣X"Ԡz @XYnCF+Cy>JXE5?Uu 8愀R.W,A-N13533$B8B'Ђ8Ar6^$Kjc9ԁCXso>doElC@h5>;;I4D (D egWbH-Ѐ< AhO6AcL9 d;p:,36U53LCY/B-A<@dc  9HC98C8xA2 -&;4>.X>A @ag}h| .=<|?}<X,f~,P'TL00$="Rl׮bvl5uS `1Rf=[d 1e-p5Cǩ܊"@Q6iȩ'8H-1<6va΋æA2F8aA ٳi׶}wc,H)'5f-_emYbtᛮuӚ## R\&.Y cf "PYٷpIo3`B`vFZfrDrځ'.Vh!x؉gdafF@::`*1s1y4xlx9C*@ ԤLmQMCpݼƠJ F ~ bsgf `uĚ\ƌ#srx ~xp2gNK f$p+`qxAlƮf}i$D m9GvA fxFFP`p@sH %k jU2,' ()QS^J㇞069j3N5=!s:i#Ziه "x(U飃 lۅp )Jvʰl(Gnqډ!̈+} :m<1h3䁇CpylǼx0 ^ɂA[9 4yyq`<@S("&*fb ,Ypr&@p𠄂p&DЀ dy'tLy9f B qșV(qvd 09gv8x h@\=p>-\AmkZ@`&NpX2 !~$ÿn0)B81 S2e"R0k" $ `aEf@+$ЁM!4f"?Y  L"`G L{818sjtd69G8%xC5$)p`2L>P0F$69+`Xx5L x}l@-H8 RE9؟p'@ z)`6֡DM8Z6'sdHl  a0C#@ <$ӎV|;,1QbRX*5}tPLuӍ,t Y [NDXDdCADo,O5j4 Lk 1}#`aAwճ; )spe0C *A`9ށƒX<~5ˊX$hc~ylCl6b0b`2 ͏XAnq4 4f^$@FFad #̸$ BL !`Ȇ51 Z#` #@~ p삅ӐF8L) )hO-8A`, *(bT##|0#|!F3Q i4# j-$$!fp & D#" 7hRqc+ఆ>P?D 0WbWOְ>m"sL(37z-40.KP!XXH cXCP:`|A-xF,kL-CG)8TJHzq!4(tV@ #9Rh Iã ?98x! ;w ZWW&$"X@%@VĠ>q|P&H3Q  B _a,"i00Jv#.hלYT#xa8~Ѐ,XH"O {Y=p0˳uԇȰ:*1C\u;ӷ+>8#H_Vc#pKnt#A40LA0, Lm@ FuoHkr`V8ܡ3VQ_cο1ޡ1fP(s8]0RpADh@$8SPСl,AXH@@ 4:aơPb``krD:n/\cg÷ $ $^k`!Vbށ&ԁn'.A LV9A:+ bA K! f'?bX,\@at+ 2 LKWlF H e:0ZgQ6@F~ѵn,R za94&!Ϝa$xcu ~@!F d(A! fIPZh4L!=RO,` R l .2<3]ˋV!0x9쾁 bX N:GP!g Azb !}-D 6z/hR, rࡳ!!Y<A .ZF`€ x ~x343@M|65: !v!r!V!L%r!4fxA f@LN`<":t`h!ha?f)p:?@pF!(Tm+Vr`hGb R`p*~`\,@A4M̊R`!qVlaB! @ r 8@ Lr?@"B!zc:OՋ!4Ue"}%IA Rpjдd "N @dAZoP bz&t8a`&& " ZQTdS` Sge 'MGnBjrc~0`HI?V= N~J&FbdF~2_ g7 LޘAZ3hh#E  `~5{&>e0hd Kށ&! .A֋|B`8Tbrʥ*0!JA~npX E^d @ T0f7wsPA.t` @pTfd\sT!5! 81A`& :ax!pLN @PNUng#8C $oCC(N)hIpfZZdXW@~ BAF3WAArza< f!x&n%Lm-?AL\W&`_'Bq*Yv.({L/!` @ ZahCtAoza"HA$(yT aar!С ` `9aA6pG<0 A9QAR_T !.y F "!n |aw{Lo@vj(ɀ7fA:Ϙ%AzA% @ͯ9m @ A]Aq %ٖNh{.~fbW#}?"AԆ7 r > FPE !v*0a"\Haj@:a@ nXr\zH"OO(# M:ϱq$FS8j:L!:@*<ra"Jq"AaI onԡځFB:JsL,Y;@X[t'!_n@A#4D@&#!Zq 2zz ~<ġz<*-WK\6`EҀi9ff@'J2M/w)9@ĆǻG6;U d%1!M"L!|ajawz,pۯ7:RRL(g@bb|)((9ܳ< aMтKap #'@BC*y +Сn=̻W%dEܡ+Ľa1¡V6n )& qv br~ɥci?62d)j4M aA KD z a̕8+!ġ+f<7& A!f!  !AgH{[{6|@̀DAq# @aԂo)A *[ _A a|0@6NUscRѠ/9!vQ_6@ a>V 48ɡ}};4C8fa&m\h:|1ĉ+Z1ƍ;zr"( @}kV+ܷoP 3Ν<{)1E!3tY+G]7TZ5+$ a]f}ֵlۺ}QͦQ&ڬI 80{ vطlĵ&8ɀ*"yճG9W>:լ[~ ;ٴk۾;ݼ{ <ċ?<̛;=ԫ[=ܻ{>;././@LongLink0000000000000000000000000000014500000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/error.pngpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/error.png0000644000175000017500000000267711057541271031645 0ustar zackzackPNG  IHDR szzgAMAܲvIDATxK\Y羫+~L'q21!"n|dF >Fw JBAtQ q@@G5 8 $Hw'NwݕǽU]vLp_[mll >("Ƌ~毟+FTs~Cٱw=eYS{֝ލ~w)7ߞڿ_{( v+._/Ϥς8;Oa Nw{_ezů]~_/-}y,<ӟ0D:S!_t掵~~kWN)3߆ hZAgca{s pqQ^z7zI<'Q7GZbwDep3ױ5pS)0[,8!* 8B <*|KA{G8M=s8FDPHQ yElU<| $cnV?;SjR.A.@H ǕmjppVWW0PNn#EEޕ"h 0eɓ'8փQ4_JWBC#EF2{P&mgA; DynylyOXV׺̆0ccb`&81 `%n6tw4 ׅ4c@[ZruUeY1ĘuSy!e (ڀ`,Xx>&2*PMCR+iNǿbe&X *S1$3=Wκd0uq)WPmskIY(*1t`om5hPph Ip73 *meg{Rr?S1pܹNEK LS 5&i\`1g(n"&_WtcvAʙT:P#}444D{+Apeu?EMCt+Nz z:)ADžL!"VF?vFW$I&5xX\\&Iz$e 674-,,ē=H@EjALj8fs;FcK0~|u@m-AZ5V QkX.]vg̽y0K,,*>ɝLnw-d p܅!*qy|G*54nϜ*p l c iwzDXx{9$oNV]k@H:\cSiCiq%ДY?(ĺ x܅&Lb0 DI(ueI& F2~\i6d 1XHzA0`s݀ޑ]@ /m \_^Y.x6>XX9a\޵w`/ q0~r"ޮYoV+~f[aeO,`-9cPPP̓aXGD%~funyk[KNL lcr@C5@3xG66gPwVOg?Pނwcs׋uK0@Hj ꃯ1t׵jwt!9}o'=a㜀[e=Tl'lX;o']Ń_^Z J)MA1.G1.]u%q)NjêQՏ'Ozo.9;V`g7=7u=Wqd||jZ0v\>T'?=IhgOܖsfoeq! nRIqw9R \Ռ1=rdb+07Q5MF8$Irwgv~fIբh099@#,K9rV.-_3g000@RahhRH-`ǎ9qWUZeJLLLo7:3fYF`xx(ji%Drq4{Oww7bgWӧ7۷/U9G׃ x *dў}P\|*H>H>wLX{D Ow6YukNcohohjLbdj>ޱqˌ3=<|||}|CBBBB#""QQ111f%$$ILLJ4gܹ4ZrrC:`2Xgl6;-m޼ys8\.32P+efYYY99 ???-YxɒK-+*Z|w߭XQ\j%%kƮY?vu5֯߰aM6Oܼy˖[Ll۶w(7(/;+**vm{wuuM/{ڻw߾8P[{!C5446>|ȑGcǏ8qSgΜ={.ܹ/\h nixRإKq+W^v=7n޼{wܽ{/޽mm>z'O>{yG_uvxeOϫWo޼}>|$#UGPUS7^GSM5M7l`hhl0x[[{G''Gӧ̘9s@`PPPpPHHhhhKXXxxJFEEGG_;!!1aΟt:dX))is8 .3=P(^g,qV_vԉܼ N,BQԉQ+WJP)))-:vuR'6mvUNڽƬ+Fsohhl<TaxXASZ#6 p8`±nR—D\i@w˗_'}lbU*XL@ 2vv ޷11ҏ d YBNA cqЀ6(0%  Le؋xw@48A1aʔܕՑ#!"g/W "F~<_IA[A]UBqb+FuMOhcPWޮbRjZ?klZ׸ի{yhB섏Z'ko y7ROE@`daq_UӪ9f:-YXG۪]op,rʘ0}ڌ3]e]?lj~/ F gEFVG5GHi KgkD0Rصi/M;xm*Q1|قUcY6rŅVהxm#7ouYڶk;#*Z.VG\`{CJuLm;urWGӹ/$HZ/\|Fϭ;w%=`Z巺fM -[_ok_~mgDePuL:<Fw_OCoϷF?>/a~,'KN(Cr><RMCwX͔!5n@5S{Ȍ٪78ls,"b2<=dI'ҹFD%ݿ~P?w;@\* 7(t gA4pAP@l! `#`&H4&,;|O C7Fo{%˙j]m#(A;2X=bg] )Caah@ &3hpȳ7([(PnpODlx9vJ3)^<?SStKsR cUIDAT8uAn0Dz^w*U &NMl0 B?4 P,G=S hX볥K aJG%?s֨pL̺PD*wlȖU:sZXU%KowӢB2Ҭ fGm֥X:(dk(PmzWg+6.*PB*֥,)rXlC+Q.M $i Tu{{LD6[5uR1=}fJ"u;Pg⚬$`"׽8ݷ(,9ܧ+r[}qjQokv,?;^ CRpw|Xpx$ݞ"36\u64/ mIENDB`././@LongLink0000000000000000000000000000015500000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/header_inner2.pngpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/header_in0000644000175000017500000011157011057541271031640 0ustar zackzackPNG  IHDR PR9.sRGBbKGD pHYs  tIME3xzI IDATxw\W}7;3[f{Ѯd[ncB5 `0!@ t'$f \"YͲj-ھ3c;6_ڙ{zBx)HO8~_j:ZW] ,KjAiv6V2vlkV_B^6l ?xʚ76Ε<`޾ro9zp{<xx@{!mcywGko1ɻ PuUq#͌[ɄPHN䱗SR{< VCUAڷئ<qR$?{p0%;Tz,Q M@Tԍ R״!ӺR#䑿4F6-e0 oʀ{1tB"#h* B B/zR1@b0F7cMzp{rϰƦy]+5_?ㇻg a#HŷA 7"93~H,c{\!v8QX %<^OFvx !b P %Hj2{ ?/kɇU` \怳qdLO=̘s Ȼp'.< ;npAR`N87\jQ¼ipV@(4q+>/#\h]G Qa*AE+)71+|rԔu|8F&=* r|2PQd)gR vjnB>9(p #hOT`"XyuKW 7t7sW$wdK&AUjmR8.$D~u&9}tI]m9 B;n"fLqqQɻ42FexmݰlY!9C>)j(|᱊D~G ."px\:r(Ψe>k8N~)a$(e1C#T,Bi(^jvR8 S~)ԩn L?]RE P8’&$qq2dW3Dn4 .g cU.RWL'n)<)y eaC*uT Eu Vn3_n ex.2%G72Vp\Av@ _V0},F}__SR*Օh m- }WLP?ZQ5)vV\LzEWsiw^)8[MzT-R)ȌCT 'bp"DkIĪ" U 5 !PhF y;b'+Q}ۤ0-KRaSʁPMcHPkݟ3m2ah9>%9_>0O,8[<$3Me&(p3\$)2HdŰ'^ uRCש퓯L MZ89>ٷ< /"F!Aw0 Ѹ̙s2RU xD;Pkd4O5iPCgt"t<nYbXdw*9!_ ci2">ܑŠ<^?'uQ|.Q `cw PP>f%D&C3"~Q5ֈL?%# DJP!#3]-Dob2sr:qo^RJ0fvR ?=I"TWDJ *NIXT/JP1;3'Ax Gr'*w@څ'E΍l8eFLP1Ȓ(Q(y 2rّS$pDÐѾ\Dhs$DI#mHILL $s.1m- 7Z_sVJC-w+Di]FA BfW%]G`L1,.֕whnK(I/xL?u;a^,2xBFQzs8)"{G6yp֪v]Ԍ8Yܱc\X=r9(ǽt8_3xOܴ; -{Tdxq5ZJNXM ZDN\K>ZQ̗r$&EhaIiGtV&8L 0q&>S$q/2 VmnDu_kn9+Ek |c0Nd9 Иuȹ~*⎶3' !THxTb3 YuL$Ko6QU6ǪI+f<972ԓ)䍕mjR$q^T dR@yRQ/$\s1.J++*C94)%gD9.3uB{~zPq$ޕ$0$ūjT*VM\ceOoZ/8A[\&iD&w(TD#4<%ſ3u`lz<@bt{Mn6Tm!t^Bh>u%&J3p2Ձ'ׁ#͔w2UmM.)Pa1?&`,7o߇w^'.n4!ė+ MpGzp KݡR- -炖!$qZ{S$6Z%ҵ24*,KlZC~wp!vzi ecx/${|C$.Kd쐏 Wg@f@̒&rh^}({"Zbn ewF.E5i%)Ġ <'bsWf'|-soK`Vڛ*J&%QBW;|< \+ǮU5&[s0N; #" ;2S1cdABq PmA@xL -c)N#iq{ ,H cD#y!tsJq}850 PpѴHhY|RuebT+IX n\cL,Mח% Ji }$K̔l9JNK^>Ԑ8ڬī/$IA (*Z)@[vDG=\shW9$Ӵ2g$aPDƿ1~~8yI 84o޵I̍[2Ixρ\Z ,+!ȈV ?ڑ.!9TME% 鵔$эfAb`Hx$PK'$6N}%c@!7?B H礃mBcJc rq>> cpD܊B֯.W$7p٤lg/FS\v& pw3֯aq{]i;L_p㊳ar6+yn6g VQH0:ID 6k ſI.A!@;w,8G]AQ'hFf;.&6U[^D(ncu0P4s@u@$HTXhR|C"J@ N>NDM,UdyP|VOKD-` ><'1 #+P|^*nG02ǣfmdwNZ6>j8oI 7yi8~c [xi2Jq1\w,o?>{o:o) ކM:KvA=Y pZ#IAƨ(d*Yc5VHZA>pv>lrV\f=zKv,#%ѥ)gXSS6 &PPd#ofF*bY*kp|GLBOc2,c8]!B!3YL3IZG6J/war>_iKTRrd8Bt IDAT]򻡩y`(0I Z$B߫ڲxiݸtUk: 5csdVQծB̖‹(eKV~V7oN9߿|oxYW,nӈuǞYD] Z:ӥ1Fʒ)Vok/9 ?}tFZnϔ:[38Q02=,m9lA]}h*d o?W]4 b$r^GxD`w(>|0;_?ڎ۞8DLI6 Ҙ}SlE QgMYL -KXd6$7f\>%= wTө lqnOwz366#`zG~V 1},]rF/"WD+i> ^$B%`8Dc wOB;ZcP-M29z#r~AXgk0?f&К9 > xAܺoJs= Q|Ȟyb.][oދeBDh*/i6"|^jM;UxV|C]1=Dqc@*fI'sd+՘UzÓcjF`'C0B{2كsĠH'{P^ɫ.Znl1NRNͥsgs7ŗnً|!L՜5o:dzsi\pO u"gC]Ǖ?y7mYa/33s]<>117: }uiHFdD¦́E5?t嘱toY =WVx)")</sR>q\I  : y&A⥠vc˵dX3YCZkMF|ccFs~Tx$s>?S]2!ӹW/boÅ+*OD:!8'Jd \}*}ç9XA+³n/,}1`w 7[G=PfߌOLLiG+wELC0&ruR3n U*%p1!0%$C @$-zĐתa%m2߸!/EJ:A6MFXεQdu;a)YSP-tiER P:f }j|MR9}xH-q÷򳾯_` Na#Ck6XքfIe+q {)T&L0<U>}>6ߝdؗoT\Td"R5Q-eSC*S.խXH!Eyj73`!Lngn, plfbf>M{F;qz_~*1":sQc [yx8~NcX(+2ig*@B)|RFҽheR~ݤȗs!z̐'}In9.J!7!ԩ9LB'qc>S~Ct6|垭Ḟ6ڞ^֍v>rr^)Eٳ~tWV:iV&\<ЈLy8;o?w q>ЌlGzN\|"h݋Cq5p5Don^ܱ6 =,w4a\r,ͮ ]Fly2_< _%yBi]> ڂڌwo\cmnlbFL{&LVpP`}Fpa|mxk)6¹ŸgphzB,ڋyj^y\}h_i s69ڍhM? ):6!_Ht tLlpTl9'o 5SB0/;,%3QU'{B][û~1$RؕlO5mH Ӈ+~q/= iù-[]\(@k<.{>NZ^tmc'^ DNX4$xﳋv | +zБy9`ځI|2!ڔdzrdV={bƅYɏXf<ąz?$}ʹPc({w-f3D`i {2N))=k2bюO\r)a&h 4M).! L|kCػB t`fI)NjV@r^@UŲhX8t=*Pg,ۤ')J2$4W}~Ҩ QGOў|v(e(^;>iti3~gmceRWSI/lhoâh7 Ya#Rɦ÷/^EM:؝Ǖ?߶X<:Zq)O~0n޶_z47h+__I1@nڍ-/to݂OaL勄R9L(-7́Lۇq~W_k{Ԑkn$LM9Dn;acU41 5d~Z)Kchqh*9[ {sUztfn'Z<>v#vD"* m>?w|~|Mg>_ܿr `Vy/m(%ɐHkTۡ*oJ' l R7i!1.xX @/~: ',psThAKaMM:k(PƒPw(|ȵΔ?- CFam>e@K"2DlR.tirgi@L$ɶ4Xd;m|xϙGIuNGvrRG5n2ML#l“X&oҒǯ:OZNiYc b`Z{A 8@FDFnܲ^[ןF3ZmΣ#Z5]Y<:<8iqu[\{,Cn6Heb<5dE(L8>G [4dž4JXIqK6QnH1v|dV;@S?^0~ eNÝCGON F:B&'1ikkLBjS=Ls*-ǡ֌yM,l9T ` t@v.CK!٘ BaU ~P g!}N!H<pghHdNH޹)[&QeS1! i!J_2MxttKpIs<)%IkM Ƕ+1t2PEQTW?M?lݧv'[09/>wI¥-`|6td Wݸ#nڍo2`x[[P8rXh(qlsEj-nSg" t;{9|C3\x߾P:Gp`l2XߗbG^Am~GHu-A~xXk/Xck|`'N,Q32'1 k51 ǙW>El+|PqpoT4XG S2}:A.U&YC_B aeshQ}LaROs_?%PC>$` LBwÜ2PK?tIg-CŸRzƆO!P7 fqgDeq]Jz߼CJD0Ej֢C糓B D o9hX ֜+&TԩB}6Lӿ߬ޘ5k\@nڄ_?e:MO,r c-$3`py Spm>zFX jm!zs x!nޢ$hn+o=-)㷜Ku҇E36kkfЗ{ ԏ]܎SV0 ET(Aj1E>JΈ߈QJaR&ME6VI*RNlJuJH#V*F%3*Ii*ֶ4:M+ciysUZʺְD10KcK'䲂RC-bNrH{rCHoZK,ߐ2aC":sHX$sA1t~-ֶq쌆6&!ERr:0_xJ=* 8n=`:ߚt0!~MT)!lLO|\Ą ^ݍ90?'1Y-k>q R}@F)aScKיHHd;gTϴ{NӬ-޴ 1ҽK5Vc01+2QVH1Y>Ŷz0 H"b+K@sfOF:>6>1L/Xlf;"9R}05$B wQ쮂x%qN:G'JxmLמkm7ޭisVP\d >0O|^cD23Zz[pkOB:E:$ 3&Jؐ,W-W-g*'Xgo".bS r<OdQCNu*c ♶.["fzN[o1KcWD3YK]j)C> bѦE)`bjqLpH }cB^ Mc:v^N+2E[wįw6Jg]O[ȥ+m ȥ$ į=fEz/>i ^|R џu?|Pu$w%W<[kI. b4&]8e

,>ϭp||Ÿ`' ) QaOOɚU@bCΑKoR< IRu)X)ivlғdaCz "i b&_P-}Y,>!q1sPlh -sxr._zQT^J-м)kNXVK/ :3t6QrMQIP*r IUň I,U8EJ-(L5܁&'>5d[΅KS-EV`6/Xy?TLJwBO-i ptg%ig#1ZH#vPS(]݀BaPWЉflkb\7~}Ao.#.ׇ?\&'Bleǵ=n+Haf;~?xEkJGk׵swa)F }W`:ysLhax _(p֑q'$mv:\D˪(ݯh7_*naF#`-pX D5<5ٷN`*njLgJvi4ᑨ =]TLjaARJ Dţ(NIQɵ@iF eq}#8PM18P3 ~qhᢔk`ё1;G&ZJv\[neu,<<ahj$qaDMA*™0y #]w9#'n> t-?q|j ,'Dջ'jD׵7m{GZ@ y8api0>'&X4L =k%.,nM)Š NjfP[weA2vͻq!|S/cZcZqQ|AL#0'P),''f-8"HalؐRc¹Vs$40*5b@Iؼ%/W7TuGj΄Tdg N,]S> "YLM@fm+4ZP%#3?/Y`2 aPK@#PPwb`PlJU͸35|ZfBpr._LF6=!94S0 'KNz,,}U͍¨,oM㸮i+7cdj?}y8yI)މۇqgo|oh/, A~6ǴV̟klr]R;?4wG6]%#Fx)߭|OoI)h1KuO}*5oktҵm8 9j~~npUEjXN-rD?%S2()D8e?5!pEھ BՆ\wrSZja4F uz ' IDAT]&r)?4t$Ǧivil?8/t۪bQ/ϘҾܺe?013l; a"<%snfٖibI1x.]d $7^`Vy5DWR6<'Ma\k\!Ȅۈ(N">hֱɱ)[qc0QKlj$J^xIፇ2i`UEO<$I7I/&)άd:_x[x9r4fJ l9 +7o)I軫Qu3)F|=2}w؎OQLADKK&nճf\RQSQc3##CqL_+` e~{R拖?|s\Ɉn`jM1)f(Ozv%q8*L2Q  ((JMp`w+pFÝěheȇEaL`RB.nM`(<52TOˇ|bԡ݄µ4)A"3>-N4F~aW &uvI ]ݍO1/6-3$nt= 4No_^U+Zpn>0ZmMXkB:f3( lENՋs8ϒ&<.txɺ[vjE)P:JeJ5%c?p^\xrl~{jn#*UBKEfyL> i,yܹ0Z!kU||{vqAnĮCGbtȄLeT*mGںv05øG[/=geik8 _x`o3n M`h\ą-Fl#M,LIm;T%eMqQ5S[\e}H&g%!ȆOoT@\l$zZ} }j)gT2QUNp9ԑ1ÀPC &~O5FJ )!Aצ;X $baǔU7&<)mFqToߏ@41/ Hw!j*7ū8CC(=:/ݳ>hZiC3y,Qۀ]ZU+ ҭ;O (bx|lv48ex|(B_yd ]4מR/9۸F%@CnTE8mQ#"1O4[ׇ:cal*|dq2!۶!ρTk3W781 ,.]ـvcxw·^{&zsvatz"DDml)LĢF_R0B҆(>;-co݆$nL~|KoȟcN4z-dL``[.;o0azښb6@4۵u c6N9 B둟w,ҔI =EϢ{)q1.K ZKI:PLl-^1+IAz'`(ၡQ'j6b#f 虅̡Y\^ӊן(GYތ]|! 6(݀o)< S]XބKmt4<<ƑYI8{i72w b^2'ϝ5c Ә"ѣ<48 "R:E-Cdr$>t>|8sE7"\q7NJa1YS,. SEt>q#u sGnٕ9m~'zi]Ʊtymʚ7?^ w!O^v73oT"K|(Eg 4>pjGLPKkft#J-x-,!'Or;Vא8T`Yړh2>n;h ,"R z0 q#&K`&8¢z}긦T˝J5б+j%PzM PM\ɤNTCu1m ?|SE'qjNGl A͖]sEEE6|I"tunpp݉,[$z=UZf3r<<l84Kr !,wB9);@ n`VQSzQk?(kb>lwgu]5!oT2/inwcT$!|aO8y0Xў%G}6k où!ɢ צRu ߹g'^w |񞛇IZaѸ۬F4&N$lD Sr'BԱ _Vϡk_ dG:Ê2YmC7#e!8ԍ9 CBz16> fA\I76*0p:xT}èRLf%޻z$Z~̨IRRѠ APظD 9 ϙiʀfݜM ԗ1B529Qsx=ǶC=5(#!vpFj܁6 I@ \3yIKRkC!F%^)ErNNqʝKwce3a|vDDxˉ]أse>"e&{#ލPȃA,U=WjVk|UlHԇ;-`T7ҍ;~{XM9Vwek;}ӸvSޟKahrsT8Aҙ|psBQ(")}|L16<斢 LDx)9'`]{T]Be2Z;E%$:ٞ"bw_?$3EbӵwQySd|FTSC0PĀ'_iB>ݍչbHo܁f-x"_G,9cT^l~o9_}n?x 8' XXS*CF'„!T&ŀJ]VGs0V,"f $+9+g3VtaD`=g=&܁bRHS< :9)pP'~mD9`)?DvRrpԄNU׆,TXGDZw>RX3jdEq>qR1,גQ49Ŕ]KHs6a kE Y'l+YN9IJ Mo` PFɺe`l3G8c@g%!I8ֲGS[c+plu^e*彪0Lg hD 8`S HG: >#C)RDwfn˚(n/c#m  Sxyyyq<gqJgFJxmnIamNapFAohϟCSZ&߰wǝgVFנ2dw|Po5k2i;qņCxQb1>Ď\}Y2{v`ǡ DNE`I\dI<{,.8Z\d`"y}'/(׌҆O=0*m+q U6O2MN]pZoh06sL0$x(]Bu&$W;F4O/+0őIݰ@̕%AUubJÕP I`kO<)R+X\l'IeG.3wfkײA]0 gk|B[mHcڼ#$ B@IHLzF@NG뎛zf8uIk6"|vcW78P2%aq,*`C>r|ܣw8.m8óRs5m;vW#ŝSxъ9ge&)##v܍N G煋;ʏǗbtg,Nj݌ ',j|]wֱ9 3+\9띙=4Z{xrq3&fiv+Rʶ0L ׬mGA_Vl?m[5&!1\&=:|YT4ǩL+ۈNrhn7ԅ;fq)qkI';׿~?(KU/"}%9w, ,\I:Zgfc sp/بM *^)@(D)ͳI -qAjN9)cs9IZi+]bc1?f?3w3E")CDLLQJbʆCk}Ra92*jy$'LdQQȺ1t'GqRYztG3| U<̿CS۱U"ܹA8&UIj"wVXw6OmZ1[Rӊ ?TgOdv?sĜrb [Ʊo*o'5x&3ud^L hEiԜY[ #nkEWKsjkO׏<:oĈցL죫׏kklFazQKDKf޾ar &[[#6!5IþE'0( DKE[nW04f|NI-w-L›m3{0'gКޡ8~B&x$ AF#l=(܀;x[6+FR+p$Ad.08|bC5?Y5ԃ H'?1JdR:D}B7h0y5^GIۭ%"S!IfMI5^S}IhM@-:Y=y qܸ3yH3…=ݚi D-#soh7v4?4o?wOoD1ГCCkCծͥ˥q~w-{/ݻyH5$|3k^<Јeݓn6HYfMXL}'gHw6ƃX`M{ڻو5mGp6 Y̥qA9:p=zIA34Mav5gp¢RmXv6l.[݂&}߼k }`İ;F5en?P*3~29"%$igc>ouX[wONҵ\.wctWriֵG[AQRt.~$hA A(9a~ D"2*@wЇ1DSIf5t)i!D+], $ MFrZ\.nKFM֐X-azf#) /fڢ^-D]uǙǘ<@z?߱=OWFIsl?2cEb\ [VB>{VܷoQc ˋ_&mݏ\:xxl.458-x`?޴,J!jl]*1}EЭ/o˛qx}y윘d#aYSʹ@QăGA42@r3S~dӘ/=pngm o;o;f 79C3+`YK M)tfeݺin_aL~r%d2}!n[jx )x @Q~grf^DNDƖ;S~T5J}LPb!(A0Eu>T'זYPL#qC\?+I|dXqNf" miЖZoڃo> Bnk~W>lڋW; fX˒oy3bEG@("B(R"A#IB㱱=cƯO?ܯ{o߾}V~~Tծڏs 8ҙ{޵k׮ַx\зObK5u&~pWc\ݎ ;ƻ^̝gn\yT֪ ~}7N?鞻Cp/X4KxCL\JOwY# ÿkXjA{ Yw/k7b?5.|4%km$<O~~w]`O=X8mv>p>3[)j;OADH.ÿܤ@atL)Gq7%SpDEAL*)ٻXrlDr-:5H"Z#̾:(4y /|ZS`:kAE={3y}Y:yI*v;;ҭqU IDATI|:] Jyϧ.9C;߉٠CXT+ωFBE󖴦5~^y~?G/ .A7"cJq]6 -:~>w x̮(r( w |o)>ۧb,3_͠ lT% a)y<+ͩ~K[#h|#8"ML6\D>ewk?|=b_kPit{"~;`jNq;S|7jץřah>tPr?hBdyzoY*XۨINv׃P[>׹;r T{ +[ZP֗lPdU~=iQS1}Mkʾ7:$ .v!;a0a0{*ܶolW:AM@k;e,^@R}3kO>@9_N_>G;+K/Z?Nm'_xK]Wr&n1~C}E,e֣ySg?~L=|SÜRZ.oMqpO=nEP- |?O/o&dLHem^{/rԘд.[zcgKx7O|/`٨/3|]Ǐ|oxg@I.[5ө6JOY>Y27 A$H ۞=)E ; pN.s׃ğ#m'T]gY|nSnZI~_Sy5! OA_tRN'j&Yc {PW%]i ZtqL"EFF0Zb_\~j]G/2;MT{[xܦqia_xw M/CMc3s^|uc^BKאlJ,ZKX`d<3 ==hz>mdCsn21*7R&,`G3pÙk$;H./7@Ľ}ɂZola&}KTi?~ N.lsw0]e\fj"h7yq>I1U摧0jəsH.sN\|udLJ߇WZmdwk}=|i^J`#`\q$+ r6v7 Եw {x|!>y6&QL;yaYG%i&=<͎>Ͳ3')hi Rt 1T:1 |p!>}0"#m`Yj}ihEvlAmlk\i |~>w7zg/{u?|S;_~!xR?5JIҜ_͎sVQowglQ'H`YqH nn._9Xqc /<.Vw[ȍaҽLFAZӜ Z 57+iLƷRh@K$GĖUsn=Zwm8"u~"R_:H`zA67)%9^̠ܖ<GcѠEiIu'N'HV 1TVl$-?vQq\=GuOS7헆4G[5Hp/|{]4{܂"o~)~bɖccP"o^3]Jݒ1YL3XsM%- 6\69ж ODI&ΩyO0>~ rDO[AΈO܂WC"`{7 Mʲޫ Z$&gWҗEH[7 B.ֽw@a9Chx8u%@Iݮ/PM+ީ- 3PDƴI73$o;Vm)f=TzUؤCOG%XRB^;oz2WTvoGcxҮHP}zf+I n5~慛ٯNj x姠!+0>ڽH2 "9{1u - >cLt6r_k#B-B]$ 6mXёLm-e$Ѐ_œhO\Yr>eDwo\_ ;f0KsBܜ1WuecBRy V8#cSN\DEY˧3R5߱mZ3S7e Gq7$W2YjmZ82*gm;! (x^8y/&)Psb(󆴾7Od׹{3ȓÓqS džeO3OQ#O:b Z`p (},aJ%ܹ{1x <CdG@*ibnpE~hJwۺRߋN*v(P{[C,Q4CJs˞M&|vZ vj+b=7㳥{8b:~f)ÒBn?#Uѫe;}q2ĊG"V[ut|ǝz]KxYI3"%U*?%lIŻFJ@+`&ggm@ H8sy 7[S5(MBhD >#F;uVlbJjc%-y*]Bz&ic}#Tshݱ^<һT5X|oox'OxzXaïށL5NVY?<@2] Q SA=FƊ طG,kaxq Ԙ_@N9hHc QvVW 5ؚu5\G%xqA&K-P: |ᦊTy]vɳ |GI{9EΑu%qNNGrr =Mɯ-y!;jΓp#F5׆^9U-aåY };$n[>i;f(PMmPݪ]:[p!x5^,Of0֋j.!9Bv2eHg\U 奣=7>عu\]Ў" Km^w~J,PyJ0jx~T#xu晔Xxq pqFŵkx2 )[)F`oS W pVA us~*߀pok>Y/`$-ׄG!'&K)jV(*61qcS 5co:;={e[ Ȝ. --kե*8TVTǙ"9_ dUi2N NU=⒨svJ+FLCPNxy8o6Ɯյ M!j3 L@ҪKx> 'tC-%99Gc  ƬL]^p(݅%!-6fRGjI{提woqu/&;V@WJ)!0 ;<mhblR$ٚ_-<%d!ED`%8L>"[S~_A)$"@9 tG wy'E/$Rzġ~pB x^RX3GBɊMxdYe*mFRɏ=3E:,2xon-xHY{C-iI`EW}L3 + ȋ(m$D,s܀xl S`o!UAa~uѺa]|˭XZYإCNl~S@NkW~砜Z›[7AH1L0 @$nQ3Ue \W؉&%Kdp G '536lP"mQ)Z莓Dٵ GU?R561&a:wyzM :QmHA.yF]i.0m:ps̝:"DcM6ѫ: ' bV .4̳ܫDqq1 #E':` g KY&bI\a[t;. paHMc,4V!e•7L+B&if\ 2ZOP ƙaLi1GKK6hR4=5<8!gk :aP>MvhnWrV*<]ܫF]rX@zS"aPy9*>h2X45忨@` .^^V."(w<~VXْȢEw¼Xn&DkDPM7(u(ۼvc+'SW8bQ; t3Bc5Tinc7&! r,KJEh ɪѸDmBjU/v\0(,Ws.S)v IL(BG L $e$@@ߛ zt>1+mmd1r#C3 q4 /ň!c4j~ ;ϜMfx/Hro$K^Eb3IrBItrE/NҴ[jYv@ jP6C6vZ(D 2ɫ _:yxFu1fB"jv|Q 5eQV+q.!;!ST?f87dQk?pwbha65Fʥ+:#JqaDX>sF%S@NZ )v1^7VsKc3ڛjwV ęTHe^1;`@a#!gd쥌6aG<2l<޴p ƒY&8\jp~%(LQ3 >sep#EVbzxJp - g @m Quk296+,5Pp/nS #`#Hj3Õl+Qf YE:00(~*q"H!.Ȫ{x4h[2x2K sVvS Φ,Hx _QlP'(#=uOba(;C%yPn;w̭8m f._vLzXP6uBDփOX m" .M%Kka ro[j %dLם$>^U,1%-X%媈5駜GSh 9& Y7մ04#hS*BfR=Dy( &ae)n8I'x_joM&^yKTRyHm,JAü!ޘ?n<5>=zͺ'B 2Hk+Hwo;e(6H>,Våif*h׹*Ӹyqe`T+!Ls-~$&dNʥFb%9FőASpi߳bA[ 5v n4l@-R 2bK ˕yɅ]!7gΦLs2p~3*- 3,5.j\3Ewh.Lpn 5,L VY3PQ /=`,{Vq![(垫U(%H.Ϧj3m}T-\1:\ j]M։"Lq-gAV;jx t {ZG Bƞ.a16 W3 eڅ#￀TaK~xC8)姸P2'6nk KyrG"مK-\,FT{6tK\} ٰ&KV@ZM3w-6]Px@8"X~:ԉɈֿ7Qs-Ϛk H¦9u'I-)P܈j-!0LU!! 9:Kܙg06E059Xh a= 0ksU=Թ`Lt6%;>Pa&T"f<fR EPvR?eno(.Ú.D0VB946rt>&`G-DPoe,{}Y dHHs/"D—־Sʸxo#@Hg[0 uZ 8z5UFF%7N/~ 7^'&Gao/dՙ(;yVeU+VU{XyP=$ :WMtDg(ٝnO cFa]GsI'cwxAs>$m[u ǮԽ9MV9_ڦ *b\,r)GFv@ZJumy*%xr)5VD&DWnTUUrk)(B9D& "sBBKE1 HIԌ`PE]}MQ7-Rm۹T$fpN>l'&̴k$#9* q7dbX  xMx& ctg-Nq3E`1وKA',O9b'/fsyQ 8kU91"߻r fJ`ݽ<"GhenF ]Ez00dwW3cʺ׼fOeeRF 7\? 4M6xtb$qK8w(ԥCˑ8 *tS&S,%k%/TC'TT+C Y9;ތ2wBm0}JB"gėdbh+Wgm٩ $,__ W(b w*`ĭ0T.{ akZh8.W2Vr1UIIEEeRgKK(Ʊ]4]r Zjh=EZ8wȰ+Hg#L@I`/ブ|k|7-<5n/b}oݧu G pָ/e/'^[\Fӏ?xe#%0Y/y˘>S#CVeЇ1M,+Ʒ͔O=BP+Xqq?u Ɗ W#\Np}8#]ߝm`PRLf0=7JZHZQ<!qM8l`å|!]4dXz@, T@ 1;(0`?`Eta*:!^exj"x^TsV\ꌥaql)`R hٕoj83LIB :kߕX` WRv Ĕ&H\¸ˌ5JD}$[ 7cˊ ps,$ "B믲==.~ѸJ^D rDz7"80CshHS? z}3Ux]P` Iz"e8WJfȏVHʒcvnS47]VFCm L([Bim1llr7?ݾ%lx-ם9~"nd>:}'{$kߖ_ySY[~>+@0ܦ݋UD m_tBa Eda/tzŗ9OpНmBPt*檕W(Ɏ}+ 2G=kN@>Rd!枧h!>d¢'v. %ӯb71X0해:J>yBZllIb`^& -#ھbJ2+Cͮ>IB/[*ZGƸ kChFJix#ѯZK,َ_^1WM1v%/*@b6 W <vi zE[=?v*$eLA![@` P`'-_d]6cC`9lkQ[W1cty:[Wgc DaH[%XVVҐX}ů)+}Taɼ{Ts.7Ӎ_lթm&6w1 wWh{_l*U,[ֽZDX;[V4B?>gWgq]*FBp^0Bcǫqc-?M̻=E!7λ+8 -]G uA0tT:d[WܷZHfW+9Z;+D j`Lm\ާ6-HZ#A2է?_pK`:̹rL~%pu 46MFX.sǮ(!UPW^pcʒ vP?/˖\㚭~9א/(<@EY%\-obli(-Iiejpή9i人)ĘR: 2{$ģwTMyԽ{@GG[pKzo3}VɜyZt{en+Yg uXV(ȁ7!A:UL^`q-P: p^>c2TXZcOMTV& OaxT% ,'ޣBX -(BZ4caLV JNԉ&$)F,H3 " ӱR~(tC@f'XFlZu,;l}Ɣt,vZ @C>zlL\:IDPP3 #n<}V_]LH@G~{(~ʔ8QҀ@vw/ڃ(j|$j0plc˓]>/'K]L(ذW*s ūfDt-cu4MqbC+>Đ$l_o~}610`%[ڳA A!=@_zҭl ލˠА'1}H[0}Hil=%tW&l Z~~_&REy+4،чśv-?7 0<`0M̶,(*AtGw6^r3b)2~͡g]XI㙈%iTs룭Z<_7JN<ƀS9%B a#LQLGXzR8\%s8ΟIӯM@P@`+4ԇ)R.SpgYm|eI i6O!(m,uJ 8i>RM)kgOCkxe|"qmNpm c7kʌ~ ڔCQ9 Cڷ&# 5T(1SZH(f0qb?Р4xQ<;" Jƭ )õԦ{4Rjs~t~GzW#xyRPiذ-=6XՐV. Q,,BsŌ@`0|i7h>/[ ecAK>ܓ {#2mIC?^iSٲ$K#4 s*^98nvB v |Nw:yʥk@}oSrq'ZwA1fiQ<I 6 `mgVSo 4t m%YV<H"d_u!mvU B%4C,Qˍrv׌׽>I+Ȍʠc.k옙? -TEJ CZ$ NnYWߺ?eWiy{#i8DZ-Mz,T umCRG7Y摤*IENDB`python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/ok.png0000644000175000017500000000275711057541271031124 0ustar zackzackPNG  IHDR- D pHYs  IDATX}lu=zncu&n(`$  C&ĨAcb4AL@ Q'?T "4X;1=wσm}\ۻy{~_r&^8wJ5dwʶp:`>mߞ3/ f6w+/~n0[=ʴβ}+kq]DH7"Ln;`e;}g-u'[On*&犴/Gx{ +sunqk A{~G2 YC&iՑrVerV]Y\v!HaG$:.0@pҐkɶ&6־1ʪTPEbmIu*7eo:5NbZ; zď 3munNCogJq*اSu3Q߀15ޤ8;5^'0$-KCDzi= b-8q>pl *ϾW*A T?ogsE -5 痛z CV%8}~3 _.IhJ:kx|J$MQ"Z|גFh`S!\]iOF}"j&6€eJ1bpb=Йʷ2 OP h:^uk4:WsZ#usF"(dȸAy˂%ya4 j=I9]U]1:TqRAX a8>UbxR_ioLIt)nI"DNa<FW@VS=2'g!P,*B!Ȅ'[Oپ3j2!!NѬ4O@ 0pJ4^GɃ?ęۦ.1N_h۸hXd vՊvj;)81@>E55C<:!{IX-aIENDB`././@LongLink0000000000000000000000000000016100000000000011563 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/tg_under_the_hood.pngpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/tg_under_0000644000175000017500000000765211057541271031675 0ustar zackzackPNG  IHDRD#gAMAOX2tEXtSoftwareAdobe ImageReadyqe<PLTEŦĻs8ziǜsljIƗji3٥sުd@wIrG߹rwCoęZ& ]KڳtS漡Զ½mDzT੃]{;˳bQ۾U:`LVs)ѻJF!ׂYm]1|XFʬ⽑htcxC&õѾk;!xK4O$iZ8ͥ]zS&AvT|f;qCSY1|̱ǔiQ㷣bLg+ t3Ƒ\cƻմֻԴW)㸕t\nuoTŠ}ωfT‰P˿{R=eڪ}Qi[`.hWʾ~MɻtN#9 ؟go@&A_3oG1±x._{>~ ЧpͶbj>L.~R/ϝnǮ÷KFѷfWǢX!ὈQ, 0IDATxԚ XWib@N8h 5X( "EEb-T|Xj-"j}`kJZ1P5ƾVִD{gɀT~/K{933ݼtX !Z9&hI! 25_1ohHhjLQ> K_^L#%z5Q#0%CLPW8 Zɛ9ZW_S%V<@۳5I_Z^xgTԌwΟ/+?yA%R*0t aeeae11+;;ls̬p րx'{yՅ 7y R 99{kR?, =?yҤkx]3ik`mX)MtLQ?FΤ.gt90gW^w^{\\j>8x^{{mܰJ) rC3gmq \ (l€?z'Y3241ec2t{;I p9UL.Av8!KA>dryyLyۡGcA3)8d`l8s v @.czy~#lB29;OxW#NϞ/nT)@]ky `E jö_~jI\_w>~}PiRk/zok'^ (.•oY~˷+Ǝ[v68!i;>~((=zZ-LV쁭[7o98B$W}4@p.f{W7W^_\L'ÑC\ȕR3@9>pWdDԨnf{2_8rʂYW;@ yGe/葧(]_{{e}MJ<\'FipIΔ4Z7ռز,)QLB5%0AlKYIU{ݭ>}ꖏZ7Urޡ;`ktg Lpy!^i_HϴgA)䜵Bae5sdQ555m?*%_1E#V*GML hn. (.xQ1?:*:jg|TH k! I+RDg~T7TD/Cz*y'OX3Zۨ_s8)ݖҠੑӾz5UVV/u\E>ZSP`)u N<󣍯guE;e)13%*x H+OiS8W씵Z՛"ƍE5-€E+Wsp.NFntrZ%'}\6qnܙe9=Qr1w1Y玔qJIr.񔢢U;lt"8eb*4*;#frZRKuf%9`jQ㫢 01կ•NY i)uW)M+hgΤ>X:6:cP i!7龚Z_"zӀ_|@E,oFhZ[͛RW@%mp2>րa5vn3฾h: ./?wjܸZBQ5$SOޑ\dC >eZARB-7&Uo&G ERAxGRy`nJ"6I:<2690=@> vF _mw!@ rL'Ʌ::q![o!a5m2p@ 3`( u  2 7P_wXlr[PM[hSRщ0F$жK\BX}/YMXʞãm ڟv]$#]Ϛ6v[JAh>47v".Ie{副O Ggwv>/NSa*Dru.xJ']-]s],t͡5. ;\ )p"p}zA'l4s"TddZ}vhN :.^blw2`&^̾iLoZZ#gх0ꭤ&SyI ks+7',Z =91(ItxD .Az lYI,:%5}h!b'2ǰ&Yz [<"E<.o;^k6^Y8fW 'a,6j{ H< z"ZQ PbL@c1L0gEgn Htdt".'a D0} -Û0,4gPтɬo8KP-CfUZ)nBr A$Q5%1\օKD\zM6ӣ @+`p{VLYdmk ٮ7$KgnӶ4ւ͠ j?ZtԴwGG$E LUj|7%"ܞ#LhrO`f"A 8F%X^6Do4P Gz ܍q#s$҂Lϟ@7~VLUh]nzzczplOdg; Z@[l-fX`0VZ<|7~OF`6­h]!!)@yHk3;[̹ jj5t5[Nߺ= ~lIENDB`python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/logo.png0000644000175000017500000005026711057541271031452 0ustar zackzackPNG  IHDRzo#B pHYs   IDATxwxյY{fNSeW `CئwH"$P %BMKH( ӛm tm\0n7:efz#0% ` 9hg{2) _'ee۪*+"gkjj @mm-"bHerD$:XUU̟?d$Y\kI[w]7 Je3qڭ ҫvv3hP]K^R__|Y>YSS3ˢjjj̐!Cq<:. <:.uzBU}$ ] juq,xwNuV5]\S짪}>WU:)S&{rH|o=+!w/\>}`bŋ~kW񵐪jnW /}oO_~bEq;)-d8^?px77ڴgŚOXG1eР^ΏS_TU6ncNM ^oAg#Fz6DS/[~up]>y矚}ܠ_FqiT fH&s+7ͤ1.8&۸ˎ# sPRv$@ǯȴ)+!Z}.>7SV fwA ϽbCPDT뫜c+U5Y?uć~g̘jAG[{abи1{ ֳlj:۳8AC\xdNH(-+WrRCA>c9$b;sx$)ǸyGSҩФuiGhMtOL [YZ(0%q( ??1n_&r[Fkl}c^uܸqhݽMrUמwXK?UQ ׈ƬwD"imk1$lMmX n>ӽ8^C[~8`>=4P(C[ $ɭ qI`*raBX1 9'>h|lUJᒓK%fKeTmOb[Oގ&Қu㸒bv|cQ hX337r!Lu(ЍU-l.qaܻ6pozq;4!`,\ 6Y=7eq65fqy=p)3֠E.*NNF[gw,dSx Ģ^A٬˦M>ΐ\ ސM&&|Zx \ؓs70a;9$` IJc\r]}r -fpK, Ybv3 ^mE1<7X&ˢEMGcW@ZM)rchtH8XtAH(kQҦk¿rVx_zPG[[Jzݴi7g&D!"?>qOѷ_av RO y X-W➩8gv [()%C^ZʔGь]/{C!MllVFRlAʕxoeǑqFu N̠8Iq'fC)K;W^$rI7E8Z»OO\^ĝ{pI4&D FGP Asb]EJ`}R7 +nUq2"#c=9gNV\5ݺ(HZ<߃C,?NsO$Ye0E1U\sxưz`G3w17ܾG_ø1lٜcv0\vvUDҩty2=[U8`-ɤc{q);ӄMqLj!ʨ$.S4mДfRnj9Mֹo:5jN :qbUݻCKbCش>8#+v[V-Nܷ/3n271AB,$;k5O"EcڴFnchVdY =x\Jz ~s8g`si Nȫkd[p 8c$q4)RT,t(>H t:3-~j'] |Gm,X3g~`tݺރ ^ybLjDe=67 Qi Ɗ`E! j%Oow[.kCp FTBe XڎRxI2vNP{^ec͎ %JP+˭JdI20:fˆJmBl0M*jLhDaWUp$ALNezHٌo^ޚ>7GLpf7|ީٝO?kEdz 7M9#HŵXTJhkHLu̘ # 8N3dt)fW;pv٧F`Bz*d@8nuKE%1)&ۋ_SzZ+{q!*})[LԣDm3}&yHԼo24$i !m C$U5i% pT gE=ZT8>VEjkkG[:ɒc߫uFGg~k޼79XAk}ܺ:Kwڒ˺f_{k#5V?(A u\Y4{#*U1q4rǖ1{v 7^5q9:z#̚(fZ#GeH؍;S]p+(iVŨNpUk\UPrQNpWv7񵣎ӿK:}mk$as@ iD:}T'{$ 2u@U ;K~ފ1Tafkv-,HP0&6N7q0I˯"Gm1J^9R;oz`S9ޓGĉ%%w]{Otc8HDQQw"+2# 6DĊL DfW7Oq?or?g|-/ZΉ cM8a(0P [<׉:vV< ԩS]@xO?Z|}kk}s=8bPRICA23liUB??fM!uhk"g߾q06WYElzOqo>aR=aj?#ܪ< HT1qb]P__l7b7/잃ν`O>a;s-7L^1 s맲9i'uɩa7d@Y@*+q`͇Pq8)ooʧ=z ~e=l3 F_s0kaWT|a2e쳧>{{^1obRwk+FUt}/ОAp?y+ڧ7^>DUApZ{JaZ[X{m@^n2hU5yȱ+~h' 9׌WL쩪#~b<@.;woO'׽ru^\vNVTLU=UUjw7ѾD'Q-ǩꉪzv;pzA>I{UuD okUU~?omY_l 5 D$_[QT[2T&K |7k3!Kޅ(Q9E5/s5Zګfp)ۇ" 'xW߃=WJuun QCH7WW/utMM2e[S&EoJ'# a- XkC0&ɓ/;GMcDIww^ɏz3' }KAii@1vsѼe-ҋ٬ G9zObw{Nqa&L]SScjkkoI_/?5[ׯoP5g 79'#пWg.aΜ])VHpqP>qn+dА0 y@Ϝ9ӈHϸ⁇.+=*hXTH{5R^a;5_N C$&R +7ѧDJW-η?;Y;;.$SQBy2Z[:Xf mm(QLiK8P^^E?ۅq6(VE՘D.W1V[[+ >C=pWk Yd-2lDoWJ*CGߝa{#pa=*O.|2[p*6Q85" Fӧ^`lڴ|e#ku#eժHO ⴶY6oT:f$t;>{я[nsXzVyf"N;!{d c`Tбv xt&׹0dUUJѳW& Z#Qf1>aKSqUp%8C9r/q‰#I$}ٮw~}X.~ $+?b$j"=1f_H|}I+jޙ5kdk9jcaߥG]@-Cxu9c5cǟmwu{,qBC>=? h}}su˦ᙉ;nI`S:TGak;Qnq>|48qL4[ċe9#{%-H92ߘ(w,U?UWW'DdCQQ* 6#n};[T֋F]AD5Y 3VC}7bK^ Is&NqUk[Rҫ9-G caL ȱj¶f(wk3R v;UUT#rX _gMUǫj̘A0." SLjjj7zsQcΝ=gAAETCenst ȇ7@ IDATVj: `m:tRv? N 5Қ^ٺyqOyC0M[7~h9ܶ}&tkݻwIo}?݅Z ?'zn)# d W8g^ZD-=FcF&r+iacU-^Ы(OL&11*D7Vx7olǟFsϑUm-[_pMM''$~m1kjjܺࢋNn]nmgyz-[XU|-8 k>!uk wVE\}q9jz:?lk(#Eh,Ct&uF),.4n؎涿?v{Z[[ƍf͚7amK.:W_,Tj֦N6oa˖( E|޷?w/cZhU]? U4{T1a`Ca2b^\.KWf07㸞UJ pzΈU㣾r{=R|.h"iQې/fHg*=˯OE ^us:X ={_K|"B_Y3(>(뉃ﶬ,N9;q ] O2G>v6G7$_1^ZňeU b3Ҁxa`a!p t= :; _Ϳ^y1M!ϸVD6ExWR~ Yf1tg5\Dظ)KkxdOF25o吏DxCvH9賮ݰ~^&p\a4BҝsCP &OQdžGv)HTT̸}{C7mi.[ wcW0㰇d_EǀsVXtڦMMj-%$T R.tF[Lש>K?wmy}v0"֚knpmQt5UGN/YԪ"&L"Ĉ@AAl,Va}_{sڌGn;9.aҸ9`sq֙)(LS]K b bRF5T(J& XwCCv3(|oQL-Aj[|ه\`k=-zDTUnej#O_%J̅ΐͰhQ@COk{$Ij݀UnK=/W3fhK7mfDI>?ܻd?jRc^J ښaӆ667vH `L rHN6?#uۨ\WԦCHxl7f|5vQد~k׬,&GEu#*k8ZnNvXgm̖X K>3nO>tmm9;_\ަ=&=:0 Rҭ{+5yO,َF"܋|Ш:6Q uX램Lhؖ1:nC,']>gǽzc#9%"y cG$O9BIZ#2zN29pgg["&L"kΝ 6dQ|?$Ũ1nT6)g!aѶ01ۨІ䟀K~bI^QU}lY+M[`]/ b?zSUut_L3?e`KRf53u4m Ahhݜc`\t s#1Hwu|P1 G>#4mHݭVܫr%׮IㄡZzDyB"a Byi_,ZE$Vp?5ǿ['N >}ztAgiM Ʀv}ca+㉄-f2o)ߩ_Ur9wh nVT4B:l`]q,Z#"k/ | Q`yѓƟ-]2]1B,.!o)"1RSxӎ{YlU^W֯m-\p\w‹>{ɪL81T~S'nv VWoMw灃d]7ir9 77r V4 ":_|Db[ Y69 UGUyJ5;/|ُ}뮽^j)LƍQGb`Lib9۱ރ8xmN 6FƱj뿵l 0i(?xYmVj4WgV>M̙=SZYW7#kp\}Î_yO= 6c?AJl 5ەՉãK#h"}F-&fT'=?fv; 4DM?:o0'[6B/M楏c B 0ơ9d"xQ\r+0KO=9foiް߲@ߘNXLbʘ7)Y])h#48F;~'~uh1֙8.fE{ty[/]^Ƽ$}"n Րt:Gq*L)^ 10v\u<7ܚȣ2b@n5EV6n+֢*c-ÎaȈjuX{UϞ5}~7UD;UQ鿕];L3[k].[:((aZ۔}N9uukvcRwoN3SL6ƍmnڴIl`*SbY?t gOf3Xd6\1 qbaTX`\ޥ jܟjy\pɋۏT浗6qsY4Ǵn=r#ƔU|ctc_{q?>YB j1g8yqxlS[6BSTT;Lz>wD>Ux3=rݯVZ^aZϋqȲqC+6FbCDuL\sBD:u3(cM&~e".BU_xcֽ{sɶwj"Yk" b!VYDZZCvݱK/OA٬,Y¹?~2}ѫ[) Yq`q"ac!ҙ^HU =kDinv8/S<0;q߼)0JA*lڶ^ӓ ]pm<.~^让\˺=ߜ5G Lll#.w?. ;0^Gݐˋx>|.GGgAK9OO~c#޳d9JNm5&΄~(RquY)wn?>K!$&C YĜ@-h: ʩ}Vy?dC8qr?1޺)aƃπ2&KF㤸~ͭphnro!uD_?~8"9ԝ J3CEe1G12{ZYdo6uT3 }xU^WˢunsN㈖&<--󤲲ne14r̷Q}`9LKe&wq'2ƔT3|L8qrEИ#[QǚxdZ w꓈Y|i_->XFu`ܿ7ߴ_憽aG4I+-1{9{}pU z,['Q=2Q\8 C|GJv"bN$oQqR[+amݽS˴,YZ--(fnj{&n 0Y4n\>2}EYl'Gk{Zx67gii$tPO~tFܿ^lY7?K㦐 ju0HP),va])(:ƸO\y#<|x p6_]Ǘq5$\B?KQI#.#Y$X "\|ے[V\T|/[E:(cgF >seMI#p\xYEΝ?Y,ˋ/gQbբY5%1DTbYE,_q3qd**YȚm\wbʤL7je%Z:lٰQIbT3Ia} ;Gӫ#7h;MX([cAdž{WQMWVgL ^!g3;{yݓr7S?~qϼ *1r ׃k/y|4IGqО+YY%+_csV=d??F}m/4;".$6SvL.g#p~d h>0<6|n˟`oc{RB;ɐc,JsRb0v}xmXң}㐓s%{xʶ/G~iy?B,\7mGx%X,ϲDf?Jw\NR O>``L#Juc/~ >-D ~¦ n[)?Wߵ~k!`vn8]WL+HŁ&|,yk:T7ܰ ŕEawػ7J<.l_3H&tx} ٶ:tꏦh@w9;Fxd'?sTUUn\|4!Vӱ6wm[~l -M.'/!-)t/+ )̇X7h7H/NSAAE|i3L,RMxMqLI0{L81Ee+ٿZYgQ< BAQgIQIDAT6QLr1c$W64:-)LXflEY6&b}w3Hfn3K"(xRYQWF`lZgY1}^DѾ5[x)|uZnCȗE:+taom 9EYG:MOcWW[ n뾧lŃ _}V<]bu߻o\Ǽ,Ҥ/Ö@']|`g[4׹^dfH^e PncSf^V֊74pcy=Z9Pg)kKG;o\M׏崳ˀTz6m_(Qʧ ﴬ)R!s3i7k r+7۷>9θ~[KNFcCBHǡ%dk[JW*DN0wܿL~H8&yl63aL% C0JsL=cFvL=DO5~Y؋qՍvGQQo(u6} 3WvHD{|ˠE&CKOxGJ#(yq^j Ea8y!^zn~]b? ݑp.g,=  Ƨ[}"y~͖KwF< ++*S>'k) #t`$Ӏr68@K>Ⱦ],[vYK|6΄.ŲNS#Al.@C $ J@UZ'Rn1VJE6v,wUβeiʂ'[hѴvK~X95Zlt`˫o6g[l|^ x"Fmm;{'iHPzt [(uJe"E>+WJek.Go9v~kkOPIEOfegSqCCS`SGA bFK)be`klMI:̏B0V-^׬Z߾:Ӈ,e?nt`|V{"%=P-_UuoĜ9kw~B*+˦%N< jbq]_VD7°{{f"i l 6y9aNTD"`pY4aһWRY1:S( 6G Thu~7nڳ?_J)&$ jIcΣ=XACNQ?Dۀ "Z眔 ~AbHX>un}?oά=зP{_/r 56J54{9""juyyco>u6ZmMPE2u%imqhiuYgH5 K(0;ı#3lr#d8$#3D)kX쭧+׬Rdǫt}owv //d)suw[n֋kN߲sG"9ǬLs+)//8#XMcNW:$GL@M$:seMiޫ_k7o̝e _%:+ŋs;wQtkٻo7׶:B=Aܔf6N^:;(IčgP^VVӣӶB)m )OksJy.ߟ{zg'=Ew[_{^~NꈻNazwW<{׾;Z8{V}0u)m?3Y®\];ם]6xAܑ~{BWUU̙ZU}}fz WѧO5Ec̘X !`3eV} m̀0Շ'6( hKmkDYעdYEJ}ɿWXV;*++4H7^tB٦ʚf[\L?YWr4w]mUy `(fTYYORs ^/TWrŋɵ#7߸[m o˱dR~=B@Oh+Nx! Vް)I8+EF)todnscChn񧛙t*>?)|fa"qׯ>knEO>Y'|#,1IL3΁M!NttM%ux**2S$:]1"ݓt/uێ:1F^{uxݓ=xކzZ4 Q˖U}/=QUxӥ 'd1ufxs eIOGII"2S$9UiH۩"Dt/vz{HʕMA5}޷s4}*߿z Qi)MqwPLEؤgz\puc)|n  ?*((50zYMR-NYVD[PHXVfVFy&LA^j;K `hE y `h@>`1 հ>Al7hHb'$$0Krić PFHu@PAIH"7W,fA;`h)#_UY*q'BrH# (;\E٘E\ ąxDEm;\32:KRK]K] ۈ h$ P3D~OVNZ霆`Wi*@Hbb,l "" L[RDRRdbg ZYU|B Aqq-!f&yI!!Iynnfy- fPO ԌP"'Kp]eo..YlL: `fWoL@A (ldfu ( 6 ln: deaQcDyRaDq B^ 'W3ǫoг*jBVYm6 J5! %^~acAVJ("Yr||rAr|Rrm|ArRRmmm\Rj*Жm9KԀgMUMU7U,{ ]h @96.6) V֐ocnや;)' RkkאUꎠ2CX5V5&-}x@8 H1# ,8P@ (k/P!+(("koꨐQ uYR\\jgoGv0١Β%0DA*a]1Ks^MA^^V^^^P J", Ѧ J`|Rĕ4 ))m{n\:X(ک *QHu1Y58@E+QugF' ֹKvAAWAVg/YSSdi0g 88[K@: Xr bh8 h u@ Rg1M^@lIENDB`python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/images/logo.gif0000644000175000017500000005647011057541271031435 0ustar zackzackGIF89aA<C>C? DCDD"ECEIEIFI"IDIEJ<JE JE"KJLJLJ"LK*MQNQ#QLQL*QM"RD!RMSRSR#TS*TT3UY#YU$YU*ZSZZ$[Z*\Z]a+^[2^[<^a3a],c[c[#ca#db,ec2edDei9ej4gd:hf4ke+kj*kj4ld!ljBlk9mlOmp4mq;qm((%_( 1VXAmСL C  Y;Kd@41D*oF0tF ,X+ ̊@FB$"&c0 ^HqUX!C(:9(8Ȩsd15 [wXBR,,^xb2(X,"I"\!!+l'H8@|g)E Dt4(L)”K*q5) ȢC.DD (;]EGVM@EZd p)8 ~P/ أb&g 5#.`/mJ'HN4>zK uǮ\Wbx(,L6PMzہ4 9 SBz@$BB? RdGXV007d,Y~@P3)V>Pl N؂ e*H~3 B* @! h!pBQɔ  E7 h~p@j@Xke!*52PH* v Y G(a#;FJ ==X6F{&d)H#xp+8|(PՁMK|A3rsrb`<"l":q1T  1p/qa d@4H"P˪6ytxB OH,0 ,n6Ðf)L ^bzA# XD*@i2p&41b #8A!_P}/dG>) #(!FPh*C @"+)*$n'wX`7P0Yp )J*Xs'({NڐYԦaD)$w@+ADh}_oi_&ST*@(1|8b^$5 $`>8" 2bI ` 2( T$U/<X۔s01-0^Ie`L4fZنuLF 4u4D갡0T=LA (@jk9q#m8 W).'v jhHaLf5^ |KP2UA4%Tad%P A.ȬjPDYp ReU`s'v`{MHm*=(Y݀ 󒘠n6ӡPe7^-82 (0}Nǫ,V0 ` @ G -U#d mfaNj gI P `sR_ /@^ 0|@ #a Q90 YFf+XCtxk `/s/xH@ =MK 8P w V0HWɐ :~0Up\ "f "p, ^ <]tp/lbe`Fc]1dO  }#Cu?q"P@p > @ dHA@ 6UȅgN9@ Le c@ QmpIFgQI`Tq` d&`6gsKP V ٸ``W6433WCP Ј0 0 @ V : ɐ >"j8 Ke @ʠAʠ I`A=&|@ A4k;Б5qL0%SlN+q$1#CM@ G;398 ;ɓ>H ɠ ngG @fd~Oy P u 1 x%>!p`T Wj`]b'@ ( z`g`$Mb$ `_}80To`=- >#"xuv@ ;f P 0 P U` U4  G#? А&9ȅ 0&> y? Ɯ ^ BV6,I 5*5@w0o >b*#B `( <9 P P @-v[P dfxF k c6MP6@{8 HEPS10g$>*]prp6|a p Y`#@3Z!'`s,`=ߒ5<#2(}30 GY Ȫ< -<e 0 9 O<{t jz@]:j'Nw 3cpy9 __F4d:bC"L@@PT5 "2p Wl@ pð F42`#K}03@ @> HpE L%@ptM&4`P OG>r*MgMvh 5gQ 0b*$4o+2}5#߂=g`t 0 0 "S 7c 0 M 0O P-iz>Ff` 3p/, h"v @޵]ZVS,@TpUzCY{++ڵ]#1Jt`А S pP@ Pg۔kxP G m, @o_6 Pzⴺ i pR @0[{Y{=*>"е,CPబ 99 ;  I nuU 55 0O IF3 `P8!rYSs&gE DpM+>Y;JT+cX8=8g m˶  @ ¥[ =I mp? АS K6p`Y(@ h T 0 ŇJR 0 fcY13(5@/`=4k @[ P @݀bʐ ng4 hp?x ?` `ɒeu` 8)D`W$MP,F#25\-m ͫ00rI P VVF 4p^_EhY19@ g ,J͜ b|4i+3[V{YGP~,Bm # z X?Ѐ d ' <)Ef m'K $U`?m4P|W7`9  i[Q+Y   s G x) ݐT^` wfP3& Ff `RX U]TЕ ڭ m`Xq@$Fj5]0   @ Ȑ P ɐ V W +$q\S꓾ [ ͜ xV8+ fd 0=My7@@6j9SFY, n.ɤXD5"8 Uܬ@P Š ِ 0_`<7H ХX@@"<`~2+s0  6^K.[@[HT~qFah9kÀqNb06G`+CР  P h0."-⃞1OPT0JWP+PjI @ <:i > h zPCe 8gй濫V !h?4?+n 00 ? x\"CoM p~㋠}N"F~ʰ 4^OӆSzdh f+bE` $O{-ˑ0 `> =Ϡ p6lK @ M` P[6R`H f < 9;XbVoʇV>u?x(x3oߐ *` P4 g[# m `dypÀ ^6 kRK֯_vE ljdM$YdR`Ū?1eΤYM9uf$<`F (S -Moߴ}ڣ < ڳwf{5  hسUulAOaȆqҖ-[2e+֬\֪2-ZfJLGX/~ܕJ֬NV\}r$Ih|Φ]mj:7-Xxiw5qƝsj9 2.3ϲ 'M Ȍ,nXJG`Y! 1At)'f{YZLp 5LYLj$Uz){p3Dmgh Ba( D.8) F0檪Աnflh`.&pQXƿln#nΪegbs8I?`e_DrH_j-TL15^'EDUt&1bxፙ@K BgoFn++J, ȀpɆgdl|x;Ơ 0ur+xqA|TY&*,h'cZH@m:ұ9aC0Ag!cXA@uL9@4M<;p^ŏ|@ЃG8[߻y$ Ea *֣<'3/t])%JV;P RӁ!4 T 1^v2 4@uYZavG!0PDφ-3&A DfcÑ!]!Uj|5@@s ! XaC,a B@cܽ'?0to?~+ *>x#B SO ><(b4۽K@EKCS C0f4(;0y{ȴu8v44zhzx'c*S@Lh!'1x@@#+ |S{1K9<؇yy>JAtH3r B &Ȃ&&hp1< \; +37{;ddBKCBC478:$h0kCˣ P xfc5PH 0i Tu-VtOXI[ل%Z3b>T3ccL@mO 5:3M-44ꨆ,AU38Az X`bu77m.JXfPkQC> q VHߜudzR tO Ҝ"ҩgCM`dτŚ9]C (-)}SJ}劄1+DD4@rS*᯿TMUR1UZE?kT5T٢J=J|p@b A8Vk4<853VJ8"GA>8P2-AVxtC% <`Lg]tSpy8ZC'Ȃ5 0؋1P4~Us؂'9STCWxS%71 )3X0H+,؂,P>~A7[EtQtk()2׆tU!;u0 ؂p/&hۣetOWhA7 p ة5 *Ps٭/v(;`F@-p (&&F~Ԃ20,,L3hāE$ Yirs ,X0H&&)0C 'WWt(..X,0EyS9ڵTsXA{Y/`ؽ_'@(2l&F YC<*ؽG}]+>PK_ TFrSk""M-)&\5CA]4 3-pT>OtK t5`G&:+$I2`s'h#&0]Apa*`s˃@g%.ܩr9OEڔ+&c+ 2Z9d0>!@mĶu5Ft!\3I3(&[+` >:qHܝ{&* k2+9K@VXj(Bȃ.`k)!x%?졀0PHp~A;HfZngݭ5WXH!*dE N_;ÄEȃ1P!\%Ђ;H@0i4E6۽H$phs]v6wF45&/3iHP>xe;B7 &8!3=DMhE3p㎽:(Yq*(#)nr֬M_jSYX[VC1%p==vKnivc`c,@4?`Nox)(7zvVr+)}yޔ<lg%FĎ'hEЃ1&;hfeéz>–e^b rp_lBV8㘕QY 8 V(@+e ,`3Pd`J3Bmŝ1g蟳(?7 6mib`Z1Qf"hXQ=Fk+y`!xyj mSQw2&8"J;rn~N0:&  c,,;D@nBoj p 77z#.%o&7)`mn1 !)"StjRYNp\ o+#ZyN Gl S?ǵ7H(<8 9-GZH%I_Z{81b0) GNv;Wsv8K7"#ph`l8 4tKh cq wOgves낼K ,j;z["qJ5a7UVqHȆpXwP9w=p)RR 3͚8:Lѭf(3*+! VpvgИwuzGZsT#:(S1BՕo@9lۀ = =Ŀp/Z+@85y^,%!d c¶Q7r j* Z"&B+6$ "l 3?0]R:q`pi(~W.)``7,b?C,Q %=`vugԊj cZPnXV,2 YTd&'M"eiK- HpФ'h98)ñb a0<d5, j.8r,$@8X,nDL2<"@Tn@C0h `1qeˈF,º?skB.,(X!Ѐ6@B yleh 8&2_AX]iV`2hQa(e+ʡ)*Rl!9\ ,\3]P JHXBr ?˖ъX` -3J@"T>ӭsWQK4ny=NX2`<f@^=,т@rj[A 2>4aU1C><xiKB!@@@ ~|a%- 2En! AAT*ZS2ph PPpL ǗC Rz숄 63y 3&0A {p`Y ((Y)OJ}t[{XY:5.dXF9+'򈻍`xDqҔȴarр\kXy;!03*O`<o*; d3bl+اjϨRQ&"} 9NKx hZaȃ ,؍D9<:PAa&F 0n,0+=r a1[ofU@:[_`Qm&#챌s0c ұq(tz_؁!ʚ"hP%|,`os,c X`5%0@1l3khg 4 F1Q%k^rۢ,2<|B 0 n*}Yyr?=1hy <kEp0DbӑX 7Ma3s&y7"0* 4f,$%+YЋ;-"{ Tp#FcT7UBje0A ~ TX'f=qcp7Р^{b[2V<<_ P7p.*! z@@"B=VM> |C#|!^^_ @D=d8d‘4A Ԕ4@Aȍݭ±:!: Z4N+_dLB tS(ϊ XF,5xx1I9A sYb"A h Se A} 4( J]A!nC:X􁖝Xq»e٩OX(GQ 6y`cPL0,WQ 6F \X؀0aQRF940A#d`bt. *864<#X"A A푁E0TBoA `A+hY  <Dµ<"LA4010 ,5 x@`0b#XwM-D1ȍa+< \Y셡E))@g@ ^2,A1A4Ƀ<0+'-X$$L@:Hn ;H%(X֥VLcX^! ,A$:=]F:d8G7>TfkeVB!~,́%G!:C=:Bc0p!=`@!h)Xu 0 \1nYJ:W8L2: N0B`Qj)ӏ~6r%c1 ꯪXʞX&l:)xv!A$|3(tb"4HlFL:Ad&:C$, q',b+A+d m}DR*Ol :Cj*ݰ$@hr$ 2^00h!xgu&Ef` N:"yhR8C$@4@T<A:|678T l DB5²@A`JĿv ]P C81p* ǨP!\A<ԁ*O=쎂*Bxy[7Ph&  g5߯Z\ A 6AT88,8@4@088$47<0d~:'T eaťLĥ֖/L uL0e4*$ =fcՊO &C-?%N#U«A!SC$ , /ԊC56,CEC$X M̀pB- '<܄̚E{ &9`S @P UyoꛫaEp1+qYg & 3YjCqIxpB:p @ 胞lLgsg}125A#,k!C@+j`5fiMdVbD([M؂fdQLqd0 - P!PH!dhUA&< @C7gY"+zȠ cg:Uh9%XQۧ <0C?ƭd! p(sұt|\ H(b*@:Aơ+eà I<xF4qe0EX pbkQ Vp $`jxsйZu OIj&D/`j cfAdztpMd &P,dC"\TC2Od,DxR0!(h@@\얷,MPqpʁ@t) rC&?T zb5ۈ9*L:aotνB! a3X!ra-Qt[RgQ1Ġ*;m 5 k RT `%,38/N +p,G Kϓ{V;@J)pv2as(%m\)Y&Vt|`% ?5/ހ D`@XNnKhJzAh!d xЀ&Ҁ5 LA X fJ>Rz`  Ї eb,j!xvz=jAF&$jAġ @EEXbbU*jil@mjhAB/oAJh@`` pJ`~AX `΀3 ܅ udC~x`  `P:( T_A`6"!== h6$284 % ځۜgiD j)!)Z> aFm@ ALa5* x|ah@ h >d"bPf @ RFs!*)@ @la$A`9 PT=-)iA~ xЀ D¡-1@ ~R>@J|!$Nf !l`Z$eM_d V Ȁ xڠja&Ha#0 s S>WE./B@L؎v* D!rA:`0N#8sN2J `vah(` N lfN<6` xda gAV!6il B`  @ L\#JblhgX!C /Qf..\BXZT3K--TX.aAP vZD`Z @\ RAjN ƀX#a._Zg~b$~Z"aL5fas4 0!T`AX_!d7^2 @l`!zdV! zjS=.=T*"f&v>עv`j 6az`[&Vf NLa&4RfÐ!J@b Bcf- 5~!6 exa3 !,3A gaZ *ܠRXc^@Alöz\*a dt"a"}G0!o(Qp#JDXg:aV@/H@`bBij! bMA3y 43_VA1@*!P1JnZw\i@HF} B&}@x}w\Jb`Z@~`pK 22aqHa.V@!6A bG+d5fcz6V`aR ^ @jQ!r?@ 1/|J@NAXl)5j ThWAV}Ηl 80Omd@c@efE4q:aK,ڠ`f~AYQ6R cM@c# _c2 1bn]P@9ưRPR\X `Xb`t.1` @ >XP}Xc xO!chGiV(Ž B@ΉBE~h u!,`J(6ab0y52a61+@/R` X\# Q޳TE` ~ ` p' @3N_FڥԀ'֖o֩.dBF3rKAƠ EWP`JJ3) 6@P@ bz#Ud_:DLCE>r&Ŷ=` zk R ~F@`0ay'[/! Z@Lhd2ir.st`%eWAn x'qs_a x`b$4AA8c䇏&/ ;=µ=š` Xꢳ ̷ "G)ԁlAۥA pbe` AfJ2!|Ixe&ܠ(tKDXg uh yz8 fW"V`tW\a]-cP@"jfK{~!PmF5Ȝ.MAS_1X \t@:fuR"!z!(UTA AA$@W9~@T1W۳д>˛?_a!|f @x0ͲL^qD&r=PD8FUGDg$L-ڐL)8]2l]]i-3a:'RD0AC豒Kr,&lAgGVi-b^| R< L0Q*E&Ϭ80*5L-֭&zȝ6 3|󋣰2%  7$0Y%PBq2 2S .s*g򪯰lA2|#>=0L-%@6 PeN>?e@ti+J1r6$͛h2-@CL7 1L 8x+|N=^_;A)  /=S 0(X &ݜt3N$OD&d3 tx y8ҍ2N?92s2+ ڶ΋o7c 3QA 9N~ `H7u>8,TG|Cki`]|Y 0Z4y (Ux| l ⠀_0 .8SKXD&ԡ<9Y[1akrO@"Є*t mC шJtE/ьjtG? Ґt$-IOҔt0 ;python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/favicon.ico0000644000175000017500000000212011057541271030641 0ustar zackzack9GIF89aM]]]Cqqq|||ӑan/-.ͣӴ򓓓̾ίuRƴZ0T%`ԇmbn01onniiiؐiV}QQQGGGC&ÛT*fff```sA.{-M"WWWSfAZ2wgc[Hp$>l1l:pPI򞾜jlqL̞沶䌍beiۅ=ߏ썐nqvwzľHuuvՂLMMSUYzCt6\_cxxx󅉏NPS]`d2.on:::QSWֶ˦PHfhmABBöƻ՝ISSSbcéssgggXXXZZ[ȿ|$##ͺ!Created with The GIMP,4a!y3PeHfD$ <͊HXMc ʁp0:Hctq wbE'Ę4iRx{oXBQUGZP#UR]  t-( ϗ.5Q:TPiŚ(ьlj16ls"@˹kexK#F 2>}\bD*$h5 碆1cb6q#U0$#_NH D蛬`-a@;F89python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/css/0000755000175000017500000000000011061527367027322 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/public/css/style.css0000644000175000017500000000712011060264416031164 0ustar zackzack/* * Quick mash-up of CSS for the TG quick start page. */ html, body { color: black; background-color: #ddd; font: x-small "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif; font-size: 83%; margin: 0; padding: 0; } td, th {padding:3px;border:none;} tr th {text-align:left;background-color:#f0f0f0;color:#333;} tr.odd td {background-color:#edf3fe;} tr.even td {background-color:#fff;} a.link, a, a.active { color: #369; } .white { color: white; } h1,h2,h3,h4,h5,h6 { font-family: "Century Schoolbook L", Georgia, serif; font-weight: bold; } #main_content { color: black; font-size: 127%; background-color: white; width: 757px; margin: 0 auto 1em auto; border: 1px solid #aaa; border-top: 0px solid #aaa; padding: 10px; clear: both; } .sidebar { border: 1px solid #cce; background-color: #eee; margin: 0.5em; margin-left: 1.5em; padding: 1em; float: right; width: 200px; font-size: 88%; background-color:#fffe1; background-repeat:repeat-x; } .sidebar h2 { margin-top: 0; color: black; } .sidebar ul { margin-left: 1.5em; padding-left: 0; } #sb_top { clear: right; } #sb_bottom { clear: right; } #getting_started { margin-left: 20px; } #getting_started_steps a { text-decoration: none; } #getting_started_steps a:hover { text-decoration: underline; } #getting_started_steps li { font-size: 80%; margin-bottom: 0.5em; } #getting_started_steps h2 { font-size: 120%; } #getting_started_steps p { font: 100% "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif; } #getting_started_steps,.alogo,.headtext { font-family: "Century Schoolbook L", Georgia, serif; font-weight: bold; } #header { height: 125px; width: 777px; background: blue URL('../images/strype2.png') repeat-y; border-left: 1px solid #aaa; border-right: 1px solid #aaa; margin: 0 auto 0 auto; color: black; } .alogo { font-size: 36px; padding-left: 5px; padding-top: 5px; padding-right: 20px; float: left; } .headtext { color: white; font: x-small "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif; font-size: 3.2em; font-weight: bold; padding-left: 20px; padding-top: 20px; padding-right: 20px; margin-bottom: 0px; text-align: center; } .currentpage { /* color: white; font: x-small "Lucida Grande", "Lucida Sans Unicode", geneva, verdana, sans-serif; font-size: 18px; padding-top: 20px; padding-right: 20px; margin-bottom: 0px; float: left;*/ margin: 0 auto 0.5em auto; padding: 5px 5px 5px 50px; background: #cec URL('../images/ok.png') left center no-repeat; border: 1px solid #9c9; width: 400px; font-size: 120%; font-weight: bolder; } #footer { border: 0px solid #aaa; color: #888; background-color: white; padding: 10px; font-size: 90%; text-align: center; width: 600px; margin-left: 40px; } .flogo { padding-left: 20px; padding-top: 0px; padding-right: 20px; margin-bottom: 0px; float: left; } .foottext{ } .code { font-family: monospace; font-size: 127%; } span.code { font-weight: bold; background: #eee; } #status_block { margin: 0 auto 0.5em auto; padding: 5px 15px 15px 55px; background: #eef URL('../images/ok.png') left center no-repeat; border: 1px solid #cce; width: 680px; font-size: 120%; font-weight: bolder; } .notice { margin: 0.5em auto 0.5em auto; padding: 15px 10px 15px 55px; width: 680px; background: #eef URL('../images/info.png') left center no-repeat; border: 1px solid #cce; } .fielderror { color: red; font-weight: bold; } div.clearingdiv { clear:both; } python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/__init__.py0000644000175000017500000000000011057541271027346 0ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/0000755000175000017500000000000011061527367027252 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/index.html0000644000175000017500000000652611060722451031246 0ustar zackzack Welcome to the repoze.who LDAP plugin demo! ${sidebar_top()}

This is a TurboGears 2 powered project that demonstrates the use of the repoze.who LDAP plugin.

  1. Configure it!

    Open the who.ini file of the project and set the URL to your LDAP server and the base Distinguished Name for the people who are going to access the system. For example, it may look like this:

    # ...
    [plugin:ldap_auth]
    use = repoze.who.plugins.ldap:LDAPAuthenticatorPlugin
    ldap_connection = ldap://ldap.gnu.org
    base_dn = ou=developers,dc=gnu,dc=org
    # ...
    

    It's that simple!

  2. See it in action!

    Now try to access the private page "about" and enter your LDAP credentials on the LDAP server in use. Note that you should only enter your UID, not the whole Distinguished Name — your DN will be made by joining together the UID you provided in the login form and the base_dn parameter you defined above in the who.ini file.

    For example, if you give rms as the user name in the login form, the LDAP will know your DN is the following one thanks to the base DN you defined in who.ini.

    uid=rms,ou=developers,dc=gnu,dc=org
  3. Enable LDAP authentication in your WSGI applications!

    Keep this project as a reference on how to implement the repoze.who LDAP plugin in your applications. Then learn more about:

    1. repoze.who.plugins.ldap, if you want to make the most out of your LDAP authentication!
    2. repoze.who, if you want more information about this authentication middleware. If you want to customize the login form, it's all possible, but that's out of the scope of the LDAP plugin — check the documentation for the FormPlugin plugin.
python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/about.html0000644000175000017500000000234411061277046031251 0ustar zackzack You were authenticated via LDAP! ${sidebar_top()}

You were authenticated via LDAP!

If you can see this page, it means that you could be authenticated successfully on the LDAP server you provided in the who.ini file.

Your attributes

We got the attributes below thanks to the LDAPAttributesPlugin plugin:

  • ${attr}: ${value}

Go back.

python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/__init__.py0000644000175000017500000000000011057541272031345 0ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/header.html0000644000175000017500000000053511060264127031362 0ustar zackzack python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/master.html0000644000175000017500000000174111057541272031432 0ustar zackzack Your title goes here ${header()}
${footer()}
python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/login.html0000644000175000017500000000133111057541272031242 0ustar zackzack Login Form
Login:
Password:
././@LongLink0000000000000000000000000000014500000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/sidebars.htmlpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/sidebars.html0000644000175000017500000000117511060271434031726 0ustar zackzack python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/footer.html0000644000175000017500000000102111057541272031424 0ustar zackzack python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/templates/debug.html0000644000175000017500000000122711057541272031224 0ustar zackzack Sample Template, for looking at template locals

All objects from locals():

${item}: ${repr(locals()['data'][item])}
python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/model/0000755000175000017500000000000011061527367026354 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/model/identity.py0000644000175000017500000001315111057614454030557 0ustar zackzackimport md5 import sha from datetime import datetime from tg import config from sqlalchemy import Table, ForeignKey, Column from sqlalchemy.types import String, Unicode, UnicodeText, Integer, DateTime, \ Boolean, Float from sqlalchemy.orm import relation, backref, synonym from ldapauth.model import DeclarativeBase, metadata, DBSession # This is the association table for the many-to-many relationship between # groups and permissions. group_permission_table = Table('tg_group_permission', metadata, Column('group_id', Integer, ForeignKey('tg_group.group_id', onupdate="CASCADE", ondelete="CASCADE")), Column('permission_id', Integer, ForeignKey('tg_permission.permission_id', onupdate="CASCADE", ondelete="CASCADE")) ) # This is the association table for the many-to-many relationship between # groups and members - this is, the memberships. user_group_table = Table('tg_user_group', metadata, Column('user_id', Integer, ForeignKey('tg_user.user_id', onupdate="CASCADE", ondelete="CASCADE")), Column('group_id', Integer, ForeignKey('tg_group.group_id', onupdate="CASCADE", ondelete="CASCADE")) ) # identity model class Group(DeclarativeBase): """An ultra-simple group definition. """ __tablename__ = 'tg_group' group_id = Column(Integer, autoincrement=True, primary_key=True) group_name = Column(Unicode(16), unique=True) display_name = Column(Unicode(255)) created = Column(DateTime, default=datetime.now) users = relation('User', secondary=user_group_table, backref='groups') def __repr__(self): return '' % self.group_name class User(DeclarativeBase): """Reasonably basic User definition. Probably would want additional attributes. """ __tablename__ = 'tg_user' user_id = Column(Integer, autoincrement=True, primary_key=True) user_name = Column(Unicode(16), unique=True) email_address = Column(Unicode(255), unique=True) display_name = Column(Unicode(255)) _password = Column('password', Unicode(40)) created = Column(DateTime, default=datetime.now) def __repr__(self): return '' % ( self.email_address, self.display_name) @property def permissions(self): perms = set() for g in self.groups: perms = perms | set(g.permissions) return perms @classmethod def by_email_address(cls, email): """A class method that can be used to search users based on their email addresses since it is unique. """ return DBSession.query(cls).filter(cls.email_address==email).first() @classmethod def by_user_name(cls, username): """A class method that permits to search users based on their user_name attribute. """ return DBSession.query(cls).filter(cls.user_name==username).first() def _set_password(self, password): """encrypts password on the fly using the encryption algo defined in the configuration """ algorithm = config.get('authorize.hashmethod', None) self._password = self.__encrypt_password(algorithm, password) def _get_password(self): """returns password """ return self._password password = synonym('password', descriptor=property(_get_password, _set_password)) def __encrypt_password(self, algorithm, password): """Hash the given password with the specified algorithm. Valid values for algorithm are 'md5' and 'sha1'. All other algorithm values will be essentially a no-op.""" hashed_password = password if isinstance(password, unicode): password_8bit = password.encode('UTF-8') else: password_8bit = password if "md5" == algorithm: hashed_password = md5.new(password_8bit).hexdigest() elif "sha1" == algorithm: hashed_password = sha.new(password_8bit).hexdigest() # TODO: re-add the possibility to provide own hasing algo # here... just get the real config... #elif "custom" == algorithm: # custom_encryption_path = turbogears.config.get( # "identity.custom_encryption", None ) # # if custom_encryption_path: # custom_encryption = turbogears.util.load_class( # custom_encryption_path) # if custom_encryption: # hashed_password = custom_encryption(password_8bit) # make sure the hased password is an UTF-8 object at the end of the # process because SQLAlchemy _wants_ a unicode object for Unicode columns if not isinstance(hashed_password, unicode): hashed_password = hashed_password.decode('UTF-8') return hashed_password def validate_password(self, password): """Check the password against existing credentials. """ identity = config.get('identity', None) if identity is None: return password algorithm = identity.get('password_encryption_method', None) return self.password == self.__encrypt_password(algorithm, password) class Permission(DeclarativeBase): """A relationship that determines what each Group can do """ __tablename__ = 'tg_permission' permission_id = Column(Integer, autoincrement=True, primary_key=True) permission_name = Column(Unicode(16), unique=True) description = Column(Unicode(255)) groups = relation(Group, secondary=group_permission_table, backref='permissions') python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/model/__init__.py0000644000175000017500000000365211057615616030473 0ustar zackzack"""The application's model objects""" from zope.sqlalchemy import ZopeTransactionExtension from sqlalchemy.orm import scoped_session, sessionmaker #from sqlalchemy import MetaData from sqlalchemy.ext.declarative import declarative_base # Global session manager. DBSession() returns the session object # appropriate for the current web request. maker = sessionmaker(autoflush=True, autocommit=False, extension=ZopeTransactionExtension()) DBSession = scoped_session(maker) # By default, the data model is defined with SQLAlchemy's declarative # extension, but if you need more control, you can switch to the traditional # method. DeclarativeBase = declarative_base() # Global metadata. # The default metadata is the one from the declarative base. metadata = DeclarativeBase.metadata # If you have multiple databases with overlapping table names, you'll need a # metadata for each database. Feel free to rename 'metadata2'. #metadata2 = MetaData() ##### # Generally you will not want to define your table's mappers, and data objects # here in __init__ but will want to create modules them in the model directory # and import them at the bottom of this file. # ###### def init_model(engine): """Call me before using any of the tables or classes in the model.""" DBSession.configure(bind=engine) # If you are using reflection to introspect your database and create # table objects for you, your tables must be defined and mapped inside # the init_model function, so that the engine is available if you # use the model outside tg2, you need to make sure this is called before # you use the model. # # See the following example: #global t_reflected #t_reflected = Table("Reflected", metadata, # autoload=True, autoload_with=engine) #mapper(Reflected, t_reflected) # Import your model modules here. from ldapauth.model.identity import User, Group, Permission python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/config/0000755000175000017500000000000011061527367026521 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/config/app_cfg.py0000644000175000017500000000074511057541271030473 0ustar zackzackfrom tg.configuration import AppConfig, Bunch import ldapauth from ldapauth import model from ldapauth.lib import app_globals, helpers base_config = AppConfig() base_config.renderers = [] base_config.package = ldapauth #Set the default renderer base_config.default_renderer = 'genshi' base_config.renderers.append('genshi') #Configure the base SQLALchemy Setup base_config.use_sqlalchemy = True base_config.model = ldapauth.model base_config.DBSession = ldapauth.model.DBSession python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/config/middleware.py0000644000175000017500000000146411057620525031210 0ustar zackzack"""TurboGears middleware initialization""" from repoze.who.config import make_middleware_with_config as make_who_with_config from ldapauth.config.app_cfg import base_config from ldapauth.config.environment import load_environment #Use base_config to setup the nessisary WSGI App factory. #make_base_app will wrap the TG2 app with all the middleware it needs. make_base_app = base_config.setup_tg_wsgi_app(load_environment) def make_app(global_conf, full_stack=True, **app_conf): app = make_base_app(global_conf, full_stack=True, **app_conf) #Wrap your base turbogears app with custom middleware app = make_who_with_config(app, global_conf, app_conf['who.config_file'], app_conf['who.log_file'], app_conf['who.log_level']) return app python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/config/__init__.py0000644000175000017500000000000011057541271030613 0ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/config/environment.py0000644000175000017500000000024211057541271031430 0ustar zackzackfrom ldapauth.config.app_cfg import base_config #Use base_config to setup the environment loader function load_environment = base_config.make_load_environment() python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/lib/0000755000175000017500000000000011061527367026022 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/lib/helpers.py0000644000175000017500000000010511057541271030025 0ustar zackzackfrom webhelpers import date, feedgenerator, html, number, misc, text python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/lib/__init__.py0000644000175000017500000000000011057541271030114 0ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/lib/app_globals.py0000644000175000017500000000056311057541271030656 0ustar zackzack"""The application's Globals object""" class Globals(object): """Globals acts as a container for objects available throughout the life of the application """ def __init__(self): """One instance of Globals is created during application initialization and is available during requests via the 'g' variable """ pass python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/lib/base.py0000644000175000017500000000177611057541271027314 0ustar zackzack"""The base Controller API Provides the BaseController class for subclassing. """ from tg import TGController, tmpl_context from tg.render import render import ldapauth.model as model from pylons.i18n import _, ungettext, N_ from tw.api import WidgetBunch class Controller(object): """Base class for a web application's controller. Currently, this provides positional parameters functionality via a standard default method. """ class BaseController(TGController): """Base class for the root of a web application. Your web application should have one of these. The root of your application is used to compute URLs used by your app. """ def __call__(self, environ, start_response): """Invoke the Controller""" # TGController.__call__ dispatches to the Controller method # the request is routed to. This routing information is # available in environ['pylons.routes_dict'] return TGController.__call__(self, environ, start_response) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/controllers/0000755000175000017500000000000011061527367027622 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/controllers/root.py0000644000175000017500000000133411061276615031155 0ustar zackzack"""Main Controller""" from tg import expose, flash from pylons import request from paste.httpexceptions import HTTPUnauthorized from ldapauth.lib.base import BaseController class RootController(BaseController): @expose('ldapauth.templates.index') def index(self): return dict() @expose('ldapauth.templates.about') def about(self): if request.environ.get('repoze.who.identity') == None: raise HTTPUnauthorized() user = request.environ['repoze.who.identity']['repoze.who.userid'] flash('Your Distinguished Name (DN) is "%s"' % user) # Passing the metadata metadata = request.environ['repoze.who.identity'] return dict(metadata=metadata.items()) ././@LongLink0000000000000000000000000000014500000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/controllers/__init__.pypython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/controllers/__init__.py0000644000175000017500000000000011057541271031714 0ustar zackzack././@LongLink0000000000000000000000000000014500000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/controllers/template.pypython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/controllers/template.py0000644000175000017500000000152711057541271032007 0ustar zackzackfrom ldapauth.lib.base import * class TemplateController(BaseController): def view(self, url): """By default, the final controller tried to fulfill the request when no other routes match. It may be used to display a template when all else fails, e.g.:: def view(self, url): return render('/%s' % url) Or if you're using Mako and want to explicitly send a 404 (Not Found) response code when the requested template doesn't exist:: import mako.exceptions def view(self, url): try: return render('/%s' % url) except mako.exceptions.TopLevelLookupException: abort(404) By default this controller aborts the request with a 404 (Not Found) """ abort(404) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/controllers/error.py0000644000175000017500000000264511057541271031327 0ustar zackzackimport os.path import paste.fileapp from tg import request from pylons.controllers.util import forward from pylons.middleware import error_document_template, media_path from ldapauth.lib.base import BaseController class ErrorController(BaseController): """Generates error documents as and when they are required. The ErrorDocuments middleware forwards to ErrorController when error related status codes are returned from the application. This behaviour can be altered by changing the parameters to the ErrorDocuments middleware in your config/middleware.py file. """ def document(self): """Render the error document""" resp = request.environ.get('pylons.original_response') page = error_document_template % \ dict(prefix=request.environ.get('SCRIPT_NAME', ''), code=request.params.get('code', resp.status_int), message=request.params.get('message', resp.body)) return page def img(self, id): """Serve stock images""" return self._serve_file(os.path.join(media_path, 'img', id)) def style(self, id): """Serve stock stylesheets""" return self._serve_file(os.path.join(media_path, 'style', id)) def _serve_file(self, path): """Call Paste's FileApp (a WSGI application) to serve the file at the specified path """ return forward(paste.fileapp.FileApp(path)) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/ldapauth/websetup.py0000644000175000017500000000161511060262770027460 0ustar zackzack"""Setup the LDAPAuth application""" import logging import transaction from paste.deploy import appconfig from tg import config from ldapauth.config.environment import load_environment log = logging.getLogger(__name__) def setup_config(command, filename, section, vars): """Place any commands to setup ldapauth here""" conf = appconfig('config:' + filename) load_environment(conf.global_conf, conf.local_conf) # Load the models #from ldapauth import model #print "Creating tables" #model.metadata.create_all(bind=config['pylons.app_globals'].sa_engine) print """ ================= Demo Project for repoze.who.plugins.ldap ==================== You should setup this demo by creating the relevant users and groups in your LDAP server, and then adjusting the relevant parameters in the "who.ini" file. """ #transaction.commit() print "Successfully setup" python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/__init__.py0000644000175000017500000000007411057542033025554 0ustar zackzack"""Sample repoze.who.plugins.ldap powered web application"""python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/who.ini0000644000175000017500000000140411061273756024747 0ustar zackzack[plugin:form] use = repoze.who.plugins.form:make_plugin rememberer_name = auth_tkt [plugin:auth_tkt] use = repoze.who.plugins.auth_tkt:make_plugin secret = something [plugin:ldap_auth] use = repoze.who.plugins.ldap:LDAPAuthenticatorPlugin ldap_connection = ldap://localhost base_dn = ou=people,dc=gustavo,dc=local [plugin:ldap_attributes] use = repoze.who.plugins.ldap:LDAPAttributesPlugin ldap_connection = ldap://localhost [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider [identifiers] plugins = form;browser auth_tkt [authenticators] plugins = ldap_auth [challengers] plugins = form;browser [mdproviders] plugins = ldap_attributes python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/development.ini0000644000175000017500000000551111057613525026474 0ustar zackzack# # LDAPAuth - Pylons development environment configuration # # The %(here)s variable will be replaced with the parent directory of this file # # This file is for deployment specific config options -- other configuration # that is always required for the app is done in the config directory, # and generally should not be modified by end users. [DEFAULT] debug = true # Uncomment and replace with the address which should receive any error reports #email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 127.0.0.1 port = 8080 [app:main] use = egg:LDAPAuth full_stack = true #lang = ru cache_dir = %(here)s/data beaker.session.key = ldapauth beaker.session.secret = somesecret who.config_file = %(here)s/who.ini who.log_level = debug who.log_file = stdout # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings # here: #beaker.cache.data_dir = %(here)s/data/cache #beaker.session.data_dir = %(here)s/data/sessions # pick the form for your database # %(here) may include a ':' character on Windows environments; this can # invalidate the URI when specifying a SQLite db via path name # sqlalchemy.url=postgres://username:password:port@hostname/databasename # sqlalchemy.url=mysql://username:password@hostname:port/databasename # If you have sqlite, here's a simple default to get you started # in development sqlalchemy.url = sqlite:///%(here)s/devdata.db sqlalchemy.echo = true sqlalchemy.echo_pool = false sqlalchemy.pool_recycle = 3600 # 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 # Logging configuration # Add additional loggers, handlers, formatters here # Uses python's logging config file format # http://docs.python.org/lib/logging-config-fileformat.html [loggers] keys = root, ldapauth, sqlalchemy [handlers] keys = console [formatters] keys = generic # If you create additional loggers, add them as a key to [loggers] [logger_root] level = INFO handlers = console [logger_ldapauth] level = DEBUG handlers = qualname = ldapauth [logger_sqlalchemy] level = INFO handlers = qualname = sqlalchemy.engine # "level = INFO" logs SQL queries. # "level = DEBUG" logs SQL queries and results. # "level = WARN" logs neither. (Recommended for production systems.) # If you create additional handlers, add them as a key to [handlers] [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic # If you create additional formatters, add them as a key to [formatters] [formatter_generic] format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s datefmt = %H:%M:%S python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/README0000644000175000017500000000126411057541272024331 0ustar zackzackThis file is for you to describe the LDAPAuth application. Typically you would include information such as the information below: Installation and Setup ====================== Install ``LDAPAuth`` using the setup.py script:: $ cd LDAPAuth $ python setup.py install Create the project database for any model classes defined:: $ paster setup-app development.ini Start the paste http server:: $ paster serve development.ini While developing you may want the server to reload after changes in package files (or its dependencies) are saved. This can be achieved easily by adding the --reload option:: $ paster serve --reload development.ini Then you are ready to go. python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/setup.py0000644000175000017500000000230511057615310025153 0ustar zackzacktry: from setuptools import setup, find_packages except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages setup( name='LDAPAuth', version='0.1', description='TG2 Demo application for the repoze.who LDAP plugin', author='', author_email='', #url='', install_requires=[ "TurboGears2", "ToscaWidgets >= 0.9.1", "zope.sqlalchemy", "repoze.who.plugins.ldap" ], packages=find_packages(exclude=['ez_setup']), include_package_data=True, test_suite='nose.collector', tests_require=['webtest', 'beautifulsoup'], package_data={'ldapauth': ['i18n/*/LC_MESSAGES/*.mo', 'templates/*/*', 'public/*/*']}, #message_extractors = {'ldapauth': [ # ('**.py', 'python', None), # ('templates/**.mako', 'mako', None), # ('templates/**.html', 'genshi', None), # ('public/**', 'ignore', None)]}, entry_points=""" [paste.app_factory] main = ldapauth.config.middleware:make_app [paste.app_install] main = pylons.util:PylonsInstaller """, ) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/demo/setup.cfg0000644000175000017500000000107311057541272025270 0ustar zackzack[egg_info] tag_build = dev tag_svn_revision = true [easy_install] find_links = http://www.pylonshq.com/download/ [nosetests] with-pylons=test.ini # Babel configuration [compile_catalog] domain = ldapauth directory = ldapauth/i18n statistics = true [extract_messages] add_comments = TRANSLATORS: output_file = ldapauth/i18n/ldapauth.pot width = 80 [init_catalog] domain = ldapauth input_file = ldapauth/i18n/ldapauth.pot output_dir = ldapauth/i18n [update_catalog] domain = ldapauth input_file = ldapauth/i18n/ldapauth.pot output_dir = ldapauth/i18n previous = true python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/CHANGELOG0000644000175000017500000000104311061527075023732 0ustar zackzackrepoze.who.plugins.ldap Changelog ================================= 1.0 (2008-09-11) ------------------------------- The initial release. - Provided the LDAP authenticator, which is compatible with identifiers that define the 'login' item in the identity dict. - Included the plugin to load metadata about the authenticated user from the LDAP server. - Documented how to install and use the plugins. - Included Turbogears 2 demo project, using the plugin. There is also a section in the documentation to explain how the demo works. python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/0000755000175000017500000000000011061527367023456 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/epydoc.conf0000644000175000017500000001055711060777244025620 0ustar zackzack[epydoc] # Epydoc section marker (required by ConfigParser) # The list of objects to document. Objects can be named using # dotted names, module filenames, or package directory names. # Alases for this option include "objects" and "values". modules: repoze/who/plugins/ldap/ # The type of output that should be generated. Should be one # of: html, text, latex, dvi, ps, pdf. output: html # The path to the output directory. May be relative or absolute. target: docs/api/ # An integer indicating how verbose epydoc should be. The default # value is 0; negative values will supress warnings and errors; # positive values will give more verbose output. verbosity: 0 # A boolean value indicating that Epydoc should show a tracaback # in case of unexpected error. By default don't show tracebacks debug: 0 # If True, don't try to use colors or cursor control when doing # textual output. The default False assumes a rich text prompt simple-term: 0 ### Generation options # The default markup language for docstrings, for modules that do # not define __docformat__. Defaults to epytext. docformat: epytext # Whether or not parsing should be used to examine objects. parse: yes # Whether or not introspection should be used to examine objects. introspect: yes # Don't examine in any way the modules whose dotted name match this # regular expression pattern. #exclude # Don't perform introspection on the modules whose dotted name match this # regular expression pattern. #exclude-introspect # Don't perform parsing on the modules whose dotted name match this # regular expression pattern. #exclude-parse # The format for showing inheritance objects. # It should be one of: 'grouped', 'listed', 'included'. inheritance: listed # Whether or not to inclue private variables. (Even if included, # private variables will be hidden by default.) private: yes # Whether or not to list each module's imports. imports: no # Whether or not to include syntax highlighted source code in # the output (HTML only). sourcecode: yes # Whether or not to includea a page with Epydoc log, containing # effective option at the time of generation and the reported logs. include-log: no ### Output options # The documented project's name. name: repoze.who LDAP plugin # The CSS stylesheet for HTML output. Can be the name of a builtin # stylesheet, or the name of a file. css: white # The documented project's URL. url: http://code.gustavonarea.net/repoze.who.plugins.ldap/ # HTML code for the project link in the navigation bar. If left # unspecified, the project link will be generated based on the # project's name and URL. #link: My Cool Project # The "top" page for the documentation. Can be a URL, the name # of a module or class, or one of the special names "trees.html", # "indices.html", or "help.html" top: repoze.who.plugins.ldap # An alternative help file. The named file should contain the # body of an HTML file; navigation bars will be added to it. #help: my_helpfile.html # Whether or not to include a frames-based table of contents. frames: no # Whether each class should be listed in its own section when # generating LaTeX or PDF output. separate-classes: no ### API linking options # Define a new API document. A new interpreted text role # will be created #external-api: epydoc # Use the records in this file to resolve objects in the API named NAME. #external-api-file: epydoc:api-objects.txt # Use this URL prefix to configure the string returned for external API. #external-api-root: epydoc:http://epydoc.sourceforge.net/api ### Graph options # The list of graph types that should be automatically included # in the output. Graphs are generated using the Graphviz "dot" # executable. Graph types include: "classtree", "callgraph", # "umlclass". Use "all" to include all graph types graph: all # The path to the Graphviz "dot" executable, used to generate # graphs. dotpath: /usr/bin/dot # The name of one or more pstat files (generated by the profile # or hotshot module). These are used to generate call graphs. pstat: profile.out # Specify the font used to generate Graphviz graphs. # (e.g., helvetica or times). graph-font: Helvetica # Specify the font size used to generate Graphviz graphs. graph-font-size: 10 ### Return value options # The condition upon which Epydoc should exit with a non-zero # exit status. Possible values are error, warning, docstring_warning #fail-on: errorpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/0000755000175000017500000000000011061527367024756 5ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/img/0000755000175000017500000000000011061527367025532 5ustar zackzack././@LongLink0000000000000000000000000000015100000000000011562 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/img/demo_screenshot_index.pngpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/img/demo_screenshot_index0000644000175000017500000030552611061321077032026 0ustar zackzackPNG  IHDR5&O pHYs!3 IDATx}wűwUl޽N9@$%6l?'p6clAH$PΧS:r{gcwv'$myfSuuo!Qr(G9QF"ir(G9QPgQr(G9N"e[ldZ1[FdN:ڄh4IUfoèuhWE)3#jBhY8 E&eOc23p]0˖C dLf(,L^xI?ʳ%f`Ve OPP}RV= U5U8F"jL6i:;7ZcD 1VyxsUE+_IijHBb:wLMCiPV;]aU&5fblNY{af8w\"-cJj6X7h3Kke3cc=h报F%FUf6NPL 3 i1f6:wcS ?sL9$+j<`DU5< @R5n;`TRd-"'ˢJcPL{Jc*P2SC?8.MRd\%͞1QU)ftmn=RN'ⴍi 0irIA)3ʱ2&3k@k~u בϨt0XRR.A=H@mƼcWs匪[qvcu$bR$UҠnZۦ#3%>•NlM6[M1T~<^x6fļ.T]lR|RØq6pWʿ^&i%jl\vguHi˛F$&r$,Q,MjdppoԄȀjrdjɑI$*T*n F;hQS(mumMyfcx9K2Km 7MxO>13noVGTNC0 .gʃm{g2&̘՘'#dmBcjbD6ȋ.t^S QTpEkє{Mka>I+&̶N7y%Or4%jS`iuW3i`i5!-|Ae)9t @^5,-5AT)Yj"ƏTSѸ3! d2_N6iÓIB.BRI(nDiE 6v$ikSIMy}⌫ M'f4p?fNi0:iBkHфI0G'8Co+Z`.\ AjͬS'5PgL5G7O f1-$ōƷ,|Hӭ+^Q? >6=3WBt\JW bZig2FFھ<0?d&ݝfǣ陝%? zj#k0-kٗ["p:a2l:zW@+Mu yk_G%32<fBA:UAHZhBVY,eQ}L jNk6QgCm +}d#of)&c3Ҭbm* {EIZ=H!P):kMFSEqLvN?Z-&+fa 3`fʙѱ0|v[ǚ&Γ~d!33D,~dǒ 0 %c$@$HS2D A@Ts (E%ʨAqYHT A+X,IE3d1m-;<3ix6 Nn̘/?b9X.[,x @Vt1\ L$52Ó gn}(.Kfxh\}E74:UͶ1!pѳY!$8Kl3d 8}>ta <@`EJ c,) $ Ad]L~Uv9 x*˴rp<3NlZشn}*<WI(6<'nWw6nOasш7ef01B` |eG7;mlS =$ndߧP (:JXMRJcRD`( .\Q  #@QP)H 05ueÀ0੐[0}⌫ M'f4p?fNÀBr Q#4[p\#C0E蠍P -Օ|%twwǪ>0JPg Ũ"kѴ_d b&6MYa͂ R0FҺD\0jek[ 45OÀgCM pH+tSor{ ]CZPzP`8_/..=k'`E@02uVSygJ! *t(!2k,)A@)dDV9 5{?n+d 3. C d™À[h9˒,Wgnѥ2X(Mj)j~9 ySFfgOb |2z[j}ݲO'm8o>n<0DDz{%%%~$7  BLJ`h۹ kWL^:eoR s6AGWcN fl55Tib43À04)À0o8>҃2xS DEIro㞵%* c1B@XWPh!-Z kwtE"јT(EZ"Gׯ:Y#h#,mb4yjml*Àk̴D 09 'MH~"yBZ09:~0ɻ(bZ[ZC]PMmA^K`>65qR]KKO,!"HWcA Pܴ#RAYEQ×,p/:e!}6u^%{ɂ!}fխG50Oayfav0auzÀ 5g ׂ#auijGu%/tp) B_g;]@v TA&x}Ѿ1%R' s* fO.E D%nqN*2]Ive`0Ɯ"!v%k3 Eo~S%!sĎ"m1'ef6Nl4 ÀjSEaspOasш7efB xPoSg^=2,hUUAaqwddXe " 0ELٟԸaM?pD kXD AwBkֶ7 䖯~i:qpNLdwo!i;poIgd=m8|$5F\9 f+ћÀ3^aMf qf Ҭbm* {aSy0P?gCd (0Ds:¡p{{?R^/),2`d$p ߨ[t"b88{Q!v%A"-V7m_{USK[7?y9%&ѸJ-;wϘUyTY?_}='G#Sd`~v:\3 ޲'okq}P%>ׁya0 L$52Ó gn}(.Kfxh\}E0Fi:UspLITa22H=GG$ @&) K}>"˴poHYIⵟ/|_TPw_l4.8s $ERE9\|wj )u~i'2E!u\\Z$)󺻕֏l24 +.Јr}ޯ9t϶+8c_ld򫲋0aS\5Àvp]$tbӪLƦ]tWᩎJJG鬰9awspq[|d@ÀF!(0ccNIe,S u8mC#aenڮ.- E?{q)pH88\_VXVL@ B x< wu/BʒUr덵ExcjFd J\ LTɵXuew4~E oR2j֖Ȣ QfLƬR9 LKظ@Ày.|S0j~ӄǑD(񩑵V \'asta0%b;LSrm;~BSGHBa7 ɌA<cCAȋ3P^W:~o[!„:_<IdB@nZ߹a׿V<gZ;zؗV@6SEmG`6U`#L0-.TnvPP BvER6>7{[(3׬T9 8 9 x09 .Xsp#;0 d™ÀÀ3`@8q; 6WUl1 Bpt v7;u'Mf 09 h2€[cFM .i>(GAdLV SD-ŕz!j N- rw%Y^h_<+YOHL(sLFpD6" )HX9ں mw#KO)b%b@AE%6Q :W6*XL– Ei* iDh4D`e),T]+iV:d)7s8Cj˂@BG CGPOeK p53 V,yPQ(RBRe!@M &POT  iK,>Zbe5 Ɣ;HveIz:k7 =,Sv׋@AHd @`1+2~ᅾ_#΀!< <-.~%gqcl8hEDLR~q}Q)@]NV[hs b1&$@TFꌒ!p]/'IXԁe[|0hɨ2Í:碙 L)Sk6SfTDvk[UltC[`Ԩz?WTy]Ԑ"z̽ <"瘐縨~{MGB쓼@fzZjb ۇS ?FvFۍ##η,;FQF ?-W9vz,)@ o]c@)+Nxw--=jMG'ij^oU9 ^8=5]QY" WcM` 0a/ܺ0NټWyDVS>4O13/];oV~3>{3uZ#wtKK~d0L<;%*%wxf.'4q$?<}V"${dT1JN/6S)qyc[$˫/SQP_/-1&zBUMϟg6o[r 0F͆E1?=*&J`Sii%|GZ΅ \LHEk + i.(x/jݭ~sY[#QYm(a;ҳ]`̒mlK(#.芣'MBb $7.ql-;{݁|H%E0cΩ[1Lv#񦷚 0`\-ZkK昂n }'yiYj^9 xZI:$бZ4c7鿈@`M)EJY<ˌ?. cFFF?oLUyiX0D(=<_4$gӦ@3vTpdh=)-UV\;2J)& gwu?i{Y!hM#۷s9QDau1eK J$_ 5xq{:a 0lE"S5 ѩwt{C/XK6IF)Ը^镽6A`Qhm?"Ɲخ*^8n g8>p@Rv}n>V'O*v)nɭ1H#2ɟZO)=4cf]kSi&jI458Xs=*qpd\s$eugsODJ gA-yuyVҖWz=" Y;؉*[V҆jst8"PSOgx|`ShSx_'Z3a "zGb2ABM7@dΙxI=yg~{͊9%J )z_0Hi(ԜLbG??6 0]'=xѤ_8 m":G+! nܴ)@:{&Ƀ<L1":dD| Jg-Y`\};z 2(3έKL`(lFi8/bL𪚼ߟ\_V}3q}2'D8d$e&m%!88_y|cX3Ɨ9LlYAI،Z2vOL'Ʊ: Z3Ĩ1PƐreLH;WthZ5?k PcNlidZ&Vhs1cƀS~iKZ၃;JA@]jӫ^yʪ}{<cbQ O__ߞ3NTQbډzW($dg_?D) fٓ3D(uNyɗ~҂'ɔ"3uY#ivܼZ)E?Z{TN߁a6rO+um6 F I\i6~fi3ZÃR%+uy@"Ҏ? c_M!UdZM`fəSUzK}v!i_wh[1+ Olv-^k_Rsnt[ ck{"M=/K킆eV6q}0ud͞Ww$wˤk)H{G߽tvۅ 生5dz簉8;oÎ@̬VtH\?ڐK߲~qneڐ Bxr>c+hhMS &N)9l}(cd;b2o^,}Jj>s*=>vTnU=S6^:: Fz^{b <ڧp\a( ]ߝwɅMmmB1I{+>;R,>o^y5q"gW떙w߳_ϟW$`8@ %2C؟17o6 >Oi,Jrߌm |Q,*Rџ~ E].럷ܴJ|Nw+ڃjt;|fjN)7uܶEnۢo-zt{=vFe7tNU⬱=ΣYmg1AGӊ]Q_K~||S͝Voj(aZK䌔F&8'8?37"fk3+1 ǐƀ;rް͝ORq44 hDab2Qȏ]c c W6{1)}jθgpE];-؊=c^VƎ߬oJMތpa7.9cTOFD)"\Pb}׾d\wάݶ_v\:wkgjzfqsgnS{$nTǁrfOu\CTTl~EO ~q|"B?pbEϾWtbwW{x]/_9h^}:Gg|τ0pS,svpDs8D?l@n`jp\{\ ;5dJ;Zs+|aI˦kaRgR=#4?Z {~{$ЏZxpF閭2AkBC{sOXMdghWZ~ڼdq`tfJCaIVx< a烁e -*%*5~J PDC4:*2P7~i^Ӿw7vw~ _޵}hHǖ7=3 (eNG|!`l$ܻ2vV5>p (FO@\mίlj ~zzDG64|a# ]8JXP}1?{{x˥Q24کl^9~K4_,^Rf/5d/vjlymwR7I^BϊI^kb^((LJ #)YM3JN/n4~B n?{u-ªEyW??.Qp#gܾjJr#O~Q5XM _9v+A1 ·sx7}t}}Qά(Ay{lbWKRt?6&ILTrcO(gVcw1y4>@C]e(kK;ŭ0/r }Jϫs n$A(ڟaΌ*ŴTI3{t`XTMPl *mTa畹71g`|@ Ŕ|GJ/P\ꁓgnL3a(Kb:bEꙴ;*~R0sJZ x<קtw@rJ)Q K.Bb"wpJ[__=__Щw> *,2<Eȵd D$1T*hʙi28ҭ$4@Dڮ?'RQk4%5*36 Whj0%h^wE<<6s'duRt{NH_ Dԏ 8lO?)Ô&qH`Ƅv]PTC0t]x_DB$ n Ą!a%#B_HJDEi\zL/0%E}?9.Ô*߮Lދ &CZ7OaJkUԍ N1<}+)AoT(m_|w>{bv!xnNC:ܟ.9QXݨbf4%d^ȂɄ&Q4y6gf<(Ps<եwh̬lrvum 0Nɛ/Vfzo ]p6/|hGwZe/d6ErQn-xgrMi,1I xE^с\g>w|ۭ#oKy}}pV}+9o?lt{wFq3{{:}ޣ@=cFzN}{gVxqJmgg!:\KYvX7׸Z޳j|Vתm䝃c >3r?s;1R\\vLͺ}f p;*Mp?-;cc>$nOxvi꧌-_8sVf}?>cvVjkT.e_ٯ:R0#9\sM|> GRvQ ;{Қ#?irYCR]+3xrK*$ b4 k++9-H/^Qa.5ӪڼՇzta9]b~m3xrAO3#a[]]C LKqX^GG?ֿa;o"!Sjf,ڼu[;AYS(WӰ^LJh :L}e.Ā'Bb]ss.miNo>KWN(qZچ=2:@Gcwmhm$ۗTO6wzSξಚ-}(PIc.X;tmw s#$\gaS ]Mo;- T~yjm F$8G7" 1 MOpx~_#;vEo?}LwrNΦo<@wXNTtSuϝH<9Z^i|]`\=ۤC?8f~=k5=p mxjc4'u|6l^}dw䦟:?!$iίjWL.‰8ifCIIwvI7?HXB(ujڄ|nI# @\,y `L1hlfήѯ60=!cb~w˭% \Efd_ߎOew,J&N@8 Jc}{aݝ. KK,eʣ57C SGc*T7Ib*JWf'dN(l;n;I_Z\ڡt1ZM#|<`cgڗɆ͏ݡk^9'0*#\*qgsz` ``t/} VW+0?k M\?CǽzСX &y/$9JH!JiH4H#E>~~W^<+g,, ~} XHdK빹~4}Y# ^;g@+O^>4nӬ64qB޾necw}8^:=X,桋ضu1}xx2t_Z3 Éo{7)\Z IDATg`vY&1{2L07?w[K U=L</uuf֚app0ƛ5 K/q'Bt>~Ʋؽ_~K۽u$D_s T])4~7ȋ1X` F~RYNץ I UR&g&b ;䩴!g9]WZDm9͠&]_kA:} *@RvffZs7NI s񙨒1O&#dwooo46i&割r X_ҩ͇6lmy7sġ(* c$EȊjaLaLfUTa:9:iT6%cRh2kOI<VdzUO+_Q$+ S2ed+'$$R"+0"0Y9g~?LQ@AN+zWOoɼ'A3I2>ۥdIRA$Ⲡ(gF|gݑH$6v]vtl_L!,p$cq~1u xV]^G %LR9ժPe&'*Ot(w[RGx<"KY>g LR !=ٓ ](ٱs;:L0eC F=\Mrj󡷏UQm99ӊ+9 U-Iq25E<[yִbd 2cHCh\ :6JÛszxߛnlKYx`ʪuŃk =ff2ƥ :ޟkԦ7vPK/rMu=IHy`Uwo rU ,30f nY'xA${]ʪ[T cQj1Q edecg`$-0zڝ&0UZx5;p0J~uXpnmfI}RdVjӿSkvJ2=:4 K`59: 8C=# qm:tC'v "!̛W2qZ>,`Qi7濯[<,BYHMyGjհR|7( +TW1%|O3mYGd(> Iʖ~620|[`7zd2l4NL2Bm3=IJߣ&n>$xW)[7p]'Hm=ٺGE€#aJ+kﰣH@QV}7ߴ~恝MWսZC~5钾ݓV׿[|C_1~ERu3RL$=\s15=O`E{*Y<:iǀkO޿ǀo喔{Jȅujz`x~޾ط6uYwZ ؚ/^ c W\Q_s%0جiܹe!ex؎GٱwP|gw*:GVʓi;4 emf-^L*Ethr.H"qlS%36 uy,a&_)J/'߃1j..gu\ylf ğy@ ~n2+ 8 3C:8GJMei[i'Q^G;2SǞ5 dԛ'{vD|0,5I$zTHQ"2:5EĖ O>h6dkIgڙL8z7"W*k$c(`q( k"R9WAcq|睹qetʡ$?-< _݃ i !:;$Y1uqh۠yDásey16mzR3V 5< Ywdsy9C'E~˄FSz⊽ʃ>`) qB=M€gNP<0J3`9vOyi 4F[:(:N\6`CC)U?lBU Ќ]&HuAk9&tXˊ}&uMV۟Rwjgl ""T8e]>'C>6eM;mJ'^5D(u!3{CkT*BL,tX;]4$ .oME/pMt(TKy2g1GnCL[zu·N2yW8qDڶH2.ժ5C#.2o(T|y0T >4i1[E+u$k=G)keLV1!〗,$yy0.kz-qU7{}494Ά֭^n˜mxߙnǢX|s.A\@WB:1ON̬ 9і4;$񮸚<|AnYS<ѽ'H^qMZ58i㻁uǼQ9-cؒ!9?VmZ;ٿms=#sc9u z8^n;1gՇ 'pÌwb[.ml:0GHZM+oZ2_?Y C.<L( L5}khCk';M*[?w/iXy_ͦ,[n4.3XczOpy(Tyb džsZ[5D + Rǘ=7kp47/|" Hs$#O`f+1"ҴO*}rs}=Y opℋ5C&TBi45qOH83Q@-nsAXg`+⽗79`қ9eA u.ecvlg~tp8T ŧ/oG&c/ԲF HӶt8ǫ'$+8C@LhoԘŀ`@rչb  h;' h6G\ Љ'13@ϊD1vVwu!s!Uwtu n;hg̮s(' ;e%줬H.񴚢?]7YKO,{7UcM7nKcx:cuz}w3]gڪu;d4ͬ Z;`>wpc€rXuv+F_d'OU~` `b+ҟΙxC#90 {vrg*t=R/BZŦd* "cKSXO6~sD<{ꜟ(}þ$"ӓѼtW'?e{UEp0D[1(0+X>98w 쩾ek"\.r|`};ɦ4HTS,=¶X:a<q錣1J(ȴ^6dFEQcW4VXM:ۦg]hYGT .6\t'#fivMȲ L&Kv .Fǀgl>2x0ffK"SlcʋڰܑR Y€1>G_h ēϳ?GVl/oYx&p awEn4(s me-=Cr>@G>Ӝ$Syd{`$iğ, w|lȷϝfooh3!Bmýò]8t x\o6bfmhoGׇunYa KO*5@†C+vu{E&ka z3(Z<$ ΀84hoK߁X=~Bq7 $Ģ0='Tr4M#Qa攏QƃDǩ/,:m۳@0'DR+ bH< #`[~z-ٞ_;iɝ#\Re> ;M*~4Ϧk> 7V<2n<'W4ZrϦyae^fqR+5;7E<d =isI,?7[1֏&|B{T0=_Xkd+V<ۦ*CesN6!$bM(`蕝8>Յx傚4?${)̣co~3gz%_(@uΫf8ų\Vᅭ=W>jw}d[o L+=K x;6'eO2H- W-{hM!{ݱL#\^Od/͵ǚlj5i@,K5l7 2$&U|d]}v5xa[s~uY1m02x-d3[{HstTn54cΦחGk:EǀSloH#9jwVU&ĶS 8.dX90Zv1D%[}E&hIt $4 Dj ^ L5M:x҅< &GL1FGPQIIIyq ˘0]z{V+'Ϻqc #+O14_?֝r cgNL$3&d}ic}Yѻ g|-wsNiJ/GM9luō_M/6gL.ION}ŀmgN}d{莞Ahus}ubxmNL/ "I'[!oL 84>2jƁҎlabp3O~lc`^ 8~'\ږAY7^8a]L(o̓WZ?xrc_^Gv4U1ko'6tPYtlwZ;Voo]9ϮI(/mͽjW~zeغNKy.[{#̯un80dt`gZ;1b=8$ 8NgKܢo=/gV٥֗9G'X^5Iحn{sW&E܇WۆG^4_ߗ8 wE/cI H`G:SǏPEUqYy(:8>84VgTb:MD(0F?48"lfr큖>9* h4 KՔה %UAWt``kO:5Z|p^E]9"\֑s:{4] 3Yَ きN{r;wDv3y}8j3B_8E1Z76 8 lO<4p *.sSꤰ8~#y7C}m$ܺ,vܽ2_x~=0 a"v2wO /8pׇs9u =8 efV[󕭀-c8D?uo HO`lM(=ȦrK-{D̂< va|s[WYݦ㧖}{G"#"-I^;ҥRtf+ ܢM5-62xܚJ) IDATr4&۟kvF>w6tD6!k9d6cÀ–أMM.{['$AspNn}9}ƀl,~s_v92r[ @|'vNc "H >"SZ>8ibUYyQeuyI8ۿwrQ`)j`pNDbK 9YpБb 99! %VdJ~B#ZW-;c{~5jv`UrhDTQcFslFbG[5`@-z{rGͰ`ևz|\B%vG3߼c~ut3'j`mKgmk:Up='@7}ћX+St>? s+k5,t&޸jzM}ŀ3=wNV\Ed:)}o;zO 59RtBrNɽ 'Ψ?Ykte?u> F7=ckl~̢e>eUXovD XZ39Ao7 (<;m}€?o\ѴpBxgfǶ~'vc ԟToYӱb`vhR`imqu/P]#jxcG,jNY`Jؿyp@JZDm?qbUysfYfPMz:EY.gZ3:eUĐ1&+T4 M\|6K%Ծ- zk/_?$3*zk ]ƟSJc$pZwc/+8c1Ӱ}of#>C|G)*\%Ʉ:uB‹4Dd-P"UMwxl_I%raj(!W<}]p|ן8ǍM1 MyC߉`s?$+O;1lՃO|\1O8KgKuu˷myq%lN˗>㮛Wg۞?ٱ+s>L 8{F>pMHKMdk$NJ> eNp*\ж/'}4ٴ7t9F^V)~Ɣ.GL Z~O/p=}|g&JMo=sM H_錦n}vSld-#ӡJbS[{Cc[^j9yְOeg~\ހ$hvPIc2_o5On-ӏ˭} *~˙@?Qn~aP 17n~#Nĉ qEd `}}b 12طehcwެRU$]qLiKʃ""DSjS$-̞>}[2h߀ٽCc!̥sL\i"I0y[_ՃCRi&dn'xSƗ̪$ NoK?jkT`QD})7a nf3Ld &\DUKϫ9mZycuQEѤ/5}$g}]qN_]n̊Yub IeoO6%;7Ws5ҟ6 ͫV]Y5膮m1 ^]R$&7}cߕ51jV}"ChB=ށ'7v8߷Ͳ37m4ۿYP/wq๗3BXoi`xDg*sX3m@wB~aw_Ua c2{袢b4`:-"pEcbf>m9:rnߦyv6`0XYFPaFƓ[2@I@;Ry〛X C3fُuKrΞ\(w/AW)s+b P x2a!rrrM3\:3=Vk6LoYF5vF)BV{\\!`Œ^5 Y p+q)|q|&>y4=>;Y<\il:JݤZ}&sl1nfih!{{ddkfx!k!;, \}Y>Y<gY3oIW&l'|b dHyZEbLjbIMAP5%{o?q givAbG()H}00V, `^0 u@X9<@ ]yMBzZyupf"Z{ - R$o2r613tMZ[r0HzĐD%"KhAc=8F`Yl!Z Bٶ(Ϲt/ xj Ҵ0Ȣ?bvK^F[ E8/"42]Xߡ_2_1yEvL )%za l |a1l B1c4ayg잳sǹbYW \pGY F%DTtmCW9$={ԳcB̹׆GH*'& 6q-$o/ /fKdLXR+p?rD. l4q:Zjt}fɏ,?=`ֿͧ&՗fbʥX6ًVrgN2..OY*y.dU\0 *p@Cme~j:7?%Z 0T~777$:wcDcŇ"ͻ)ަc0l μ\Ť8 Z]"瑌8f9皢 E}天()ܒa3m%ݲ5rt|$XLYd[32U@H ʕKrے뮭m;rq6 &[KN\L4zwIff05̫QeY$ zi"Co!u]!c+:| !a"܍0ʘ]{Y12Dg(ەhI%j1u+3pqUDi5C-4zp`pn?ױkk;\"yy-#+^84 HAУ:ej\93eM47OAL{moQTD4jWI/%LwABVP4A_ JRZ~E'e 8X<&76#ƙ-k#]i~zO 9=ꜭp7Y}DȞyP3+3(1*ŀ %>sEk2ۆzt[܌ckWfrs.JÍ@Ne٬Zͮq]̾;;\K0wmf%^o&υlUʚ{c]յK]4_"cEܵnSLqrmr h%l΀ *Sm6l)0`^ Hv2&=_p$,<C-[Mp4&tAVE 88Lܕ,|K^14N+ճ`=hiQyiQ H)`-{`u,ι r0uutᯯK0ඔ2iREeU(@<"OIkH*C T>5[-Ս +Y9}^2D !Sy: ezΩ}k91__+ʠ%'NdkcS[$ f41@)xj&V1?{x^=TץwJZ!1[_^2Lc€%fsq35p26Rc>IpU֭g}*'C.19#S @u+W>u1_yd eGDO(c HiHL=?EI!c9>BHK&pQ;7h}`*Y[#o_<~ֱrˉ#2N!W7\rN~\4HG-X޶Sm) Hg0 Hđy*8ScFn$pw<Ա}u!<5f&?z>Fλ]0v"Ө#k[ilp1ì*|k>t#ᄏƆܽ`e@9e q IMa}ɮLt`5p8 oWg6׼EN>AU%-I>@&0Dih(P^㟞9w~1Q/Sjeeux[}?E p7?ųsCek4<šʲE$Aָ_, u9& fh|na'gn6ӘlYbL-6\0 4)P0cH;͎Rp+|hB:?n80IhW/ zy3< @@DFT(b ylgMVcK/|n"owC%:RgTjK+""N!|ivGKPiыg_)o]8Lrnw q`:RFBeYֶY: yk7R_\ro?7FSs<{(oC@I&&N/` QJ4IT/ZSw(NiYjpzbD2RIie"絛9rYkdVԬ ʸ+`i\oN07]cbz:ul#Û}pr^2Z?*`0hŀ n{K=-[oYt\7vkqV-({s='Og:GM3ݧ"뽹a ?Wm=j5Jā.>\Id$d@DPPD HZis_pa?75 IA0 GiāRI@pILx: rZ .;{(]{*5 8VQIRi$E=%SD!:q\ӈ(3oE52& Ɉ:]9OjxsNDS &3ZCi+` <阪 xƴ"~uyk;:X"$'j}*%g(@ $4A{ᡡ7GCф_P9K_x۴1}GOQUFiQj`_ )'Vx\âR8偦η]]? 2U%UA(zOn㖔vqB@ԔtIiEx}2'0 ( sGM?DD!Żz=uLV6*eE(`MܴD(` p'Ϩ_Z0 _P2H >\'ݜ$)Ej|GȎ{|{q4NLs+2L2A ˓wJwiGCUM.>?jzvEo~2㌳g,4.56Oh=,UM`bP I̔}@@2r ѧ.^ZUZM]D"IVT]6ȣ@u>c-[U<)>ʵ|Ͻ[rYL4YHB$@{H86"r=1`@`J49(H|.2CV xnKF U YܒDL7֦=+,%I8D@D ]Xc]}Mx=lw?}($@Bp * ;y{aZ2-ԋ}kokb%xeoC_2eV}#dwUbi<%_Bl@{ 3c?Wέ]m1M~p9oɉ"DQ4XT>6#5\/)VFS#? pk?Ш`nwG*iytfA~/`%ƻCf!YŢX%D?weZUP]CǗ7Gwn?XA! J}w;ur뎁Kh( ҤW'jkotox?#\ 4>zKoMKE{~ JW\:;.<Ř@c훻oٺ`X8~.;ܷٖ/~؆%keFj9ybu庁b@vG%ӑwt8 `Ds۵RPn]qvLnΡ}t1I,v\v:0zblR70HjQlcy/*=XhG{k_v-QR$Ma ]7$ԟxFMu_oȂ(4n)Tロ]yV;,rt+6xӾXR"59@djKc?gT;{hg`6'ZF^2F,n_1y2Y xjrMJncJPV{q˜] F# Sc^u0XI^ç «80qeCUU4i*ԇ;ckވL8GBD>2ttjCG&چέ܊PE|~U>;ab;i<#_wU?͝XiȸHZzEK_xz5=m]<ן-PhRCm]U<є"+pҒjƹ8cȀ|@,ʪө+CA2b擼 N*ᇍ]۴wVU+sjw p+30;X)9G{wَcF;Š Å}_0yF̦?}͹+⡠Y v-Ϳ߱ ^]& Hz59"2@BIf5U{7tFiܙ*I;]Ͽ=d )\z0r&0$Hlvm'W_Z2u-SOӈL&H3W>;gT&3N^^,S ?ܺ򍞹p> RZ~^= \{@)9TiI9'Ado޺_YsB c|bCys'|otpL)vgwKvL0_O hfq,qv$ʥ dg/Z0A`Z93}KX~c h8Vwnˡt~䦜j {Ϗ.zX<=* 1;| 47hޮ@$8s*#A#NDPvtwYߞ/=[nwH0a1\ZFoϐo҄k1-PTrщ? JȚX*w_}q?>⭝C!Rx5u_[}Cz.ϺdUq1웻 qp`u]% g̚ʤSUH @ś8a*>J~y2@F`ޕ3DC_r)E%rdT0"dMuʅwhO3p zW7מ`ǐH]S@阪<uʎp)AӜd>^:=-M;$k4+#TH(/Gi$!4 _ g?8pݱ).)sYo6?B qǦwNiDXDMЗnFZ5x¿lY틯t*%JhjYt aP9@I iy3}~?*1D d"UILb4E$~k Jş¼OZA$(@3far0D3YY 1JYs3eXdU.uœ*`sVӔkV pW |P1j:,/ӑbmD!׺`>:ؽ~]sLu?VH(AgV̫}mvyUJT0)];ߚQ3Yqh#iV%|uN7AT7:vb< Cozޜ9%'pYf& iuUQ& r$ C@H 6uc.$S1ӈF$Iezr0@=Sraw p+30'Kg^,;׵ JwU$7Ş=a}י궁}dzɌ-`N;Ww [2'\nѤҊbi8mimmNJͽԙ}5Cv{ǰ}'M/K rY6W23|-+60̭N*)qXzS^kKK0ඔ0ͨ( "PE,4{meOY 0,KZ>'>pj5Sݵb%mo˟>Irw飛.{ɋF^}ZMIE>Ul5k֮0/9‰2c"p8(1&F*J{7{&Mi"EP(j,Fc Qc?Fcb4$M쨉%X@Ŋ"*v9w&7oξ}; Zd! XDRbBb.0Es}M~" ;$1B y-`pW5T}۴ʹ캓\Eec%8u817r@ZF5 v`mtjruք7 Xi:{6jfӤh k}lcEk;;-߭s8c7W c0`Ph@DQ}S e[YhL([xhxL'5堤S*y5mrdéEw4hJK?{AS*.;kd48.00D<¿L&1&e0'Āhǀ9L *9ۨf=QR {F I|9iմWnJabhV.H03PI Mk;LZc)|/!H 8FY0cg 9foaHH+Jw!H]]bCIz\ubU+M#oUƲ-@\uAwMJQ";99UM02 ,Å(iɄ( 2p)@;cD#qIȐAGKb.HQK],'[)ۨ.jOwL1F{5l@X]m;w/-(cPV O2?Jd`y/t`dNmZ"VBbJ^ >Njv[im2Bá^VU,; j.且2wʖ>5 1&=ZK-Ƌv(ߩ'T.EGدKm3imibɲ->+ey25m 7hZTY-M1 :#B s%w IDAT%ۤ ?yˇF5@jzrx̷ޘE񐲃:QwG!}`_RewNܧҕ F@`ڮnӲɦqSNrN 060ez dhgn1Ā /1b'0Q>.D8.x~3O{@έ}.?̯:|Bgߓ(c40OlR? z#֢Auy4O@hp4ƿE=zIbzBU]/4 ux8=)DLby)$\@"SJ弪1h 2 C{o{.Go9Jwp\\y_OAoȓUXUIa?/-!WN$_~m<==FJp"l:L#4ހm@O6j;Um;'mԅW/) ) ttnz;VT'  P eJY׮>n(l[W n")1N Y!1Bfʹ?@&qWH x_p := 7+g?o+ݝUtw&~K*E;F"Gfn`!ㄨlp[-2zD1Tyz1bYߩ^m!ýf;ra:aZ^VWY~5!$51*`79BX%ش (yE48x0ٽAO4Ug 5 C8O#d" POY H5rH {&%rA5m1gdw\g[uqB:lHF)290!(uJRa:eLp <-g>` f{ThۤhnR$">јY)ˏ.g;f}?x_VtA~Oζ 7;E]y{ xsjhѓS2K*4 YOzLfI&yU_ !Ns^̓(?K/!H 8FY0c^l 8P0s/#M mk>O'O\.@RQwMIMQZ'tA( B1@/0FF&-z]F]} Ž.P*Z7$_ˀhd%!z@ .4)*.Nk[C۠ASQ67sHɿ y-ar'ˍeznaBDAs,KbG@bI I:#(~n|Ӟvn6g&vw25K'og Jb gL5oLnNPLtE'Jw*c'!1S=1J0x/VOzxIeCxrt*Ahjkg5vJ+<2 znD+B@pַ}Ѽ;.a QzF[sv:.T#;@2J1 zVcJCR?[e;GEu먖ɛGẹ<$<z$TH x8HbnePv'\نsuƀ~Z\~U J‰9cvF{](⃿wfK!12HR*絊&s(4:S1i 4!< y4"ad`Q4(s;(w^SQF; %di" \v ʴP@E3X#E+)_g ?R&a6 %7'﬐pd Qw_߸+I$bl xFnjK+5TrU[ Mb=A} @+P-!ev4TmĮj6QmTRܱch+pd2Nʦ e"7O.W2b. ؁Ssh 0B2`€15/`sƀkO`Þ%480H3l3.v R%t!x15qfgLnlvbk HK0\\FNbQo$6L1~!߄=~v ***p RKkb7VWAVf0PeW>ʯ{;ÅBX)&ƀƸ\qZqMMEuJܑ % +9V"!&scf ȭ d2\I&_ GQsGb(YEzE|$$ P8PE . qO$ęا*L"3.%r 9$v 3;()ӎx纩'?q^iN%0NL8v -EVF tӌR*s՘ 8b[h^\20cE LF-2UkYiI?N UOl|Vd챽ڒ"WP{KBz16}BI 0Ā3toXsŧ1= |Z,1jL0C`!Jc x.jn9j;gwsw35H 8pS L@$ȫnq-H?@s>4> ΍LB*2w]B,gN'iYpH4$\Pm3/^/EbY*ߥ팒 UשɜMZ|%n>)Ccl ^-"`pw$YMogǹf]oYXјY)ˏ.g;fI5r88)xѱ );^y=auQqCb-ݥ4bjUnD'x'\ :6t5wP{6> j+TU%t+'1༣$\iIk;d1/spq/gLM qܞǤVg K'}$B^ o@HI  $9Āݞ[?$W\l_+suJKf,Ǥ&)B-faGrl%t~U*:~dl7@@6oGE@̻? E(sOQs)$;fpPpA0k[i(a&"Z$UEt:C&eq)DGs{fy4VL/3%GS.8 r?u[Z݌Ow˘kbE&*IuJhiwj *2jNSƺ3 jtK3L=̋.wxl TR[ap͸|/tpHZ!E}!z/>-=& ťl 8oهyC&I7)ŋl={Lr :mBuQȩ!<5wSh(Gs&)緳2م6)6J[A=laoi ;/4v}R07Sen>)vk> b!(iT ;c~qd>A)?no)ban+[k]Ej-^p`Ib`);2y ő,fl*MŰX.ӣH 8gOJFbyb2-6mە4}k`NmY[/mDj3E֔]xg TbF!U0.X~3ĀBDv@lFC\tn* )ec^ & 0rwy#`3any>1(Jpp6/2*ĀEO#cl63/eX頖i^X?p=è3#WwNZ-0iSӰXW$Bb%d./"Y.ᒲulDyŬC&Y(H nk6ƫT?DQzy'NT)= fʹ?@&q܏+nd13ب0>D{n!PT 2d~b+.L-6Х3I:Wrq_pOh2EY)6ЋkPi6>=02-N /?HA )< )1gp,dptћ'v牬.Ml 1p=t&dl^_7;Ͷ$1c?ښU%0PH!kٖm>Ȧ Ƿ϶wN%g`-! #KX_lUhH 8k4,`+  u}+KĀ3hBcw*Hqs}Z>0Y">јY)ˏ]g;fINO85lb$þtE`m1ܞl.8pw`^Nh Y7;_Wevg2o KF_Bc"8 %bƙ.&i>^Oő,d 5ƦbX,Q$'d%q^}1 5 1<*$؞2Ӆ4k+E%dg[3J3pN_p?.%e|=Qɇ,,[q4e4 X})=N_(?\9I 8/ĀK\^D"]rJ^eH 8sĀo:9w_߸Lcy1% K:jVUGZ6. pF0[sq'ngi& _-O/'4"IUŵWE|4LvI 8[61Ty*ܸСCE,"@ױcU4#@ ¿ 3@ XL@ !>@ eD `3@ XL@ !>@ eD `3@ XL@ !>@ eD `3@ XL@ !>@ e/@ lJD *- #%9(!@ ,C|&@ @ 2g"@ @ ,C|&@ @ 2g"@ @ ,C|&@ @ 23 >pB]|`^''իW[_Vea!vzeL\( 6` ǦL\q'5k˜_O$>gCWf>\{ߌիk֬Ktokؽ{ۥӫVf/q+\rYT'@ T7z'OܫW/f{Μ9ޓ'Ofvv*+o C:mln:Ϸ,A0Jsfv'YG.&YbJT ,?صo_MJ[sráaiEXXO޽hV?׈U9VІzgܹsΝ˗׫W/""ʼTYV =[5d7WCv {)3*a[m"zDYV?33IY&ҥK]tիWJJgee;͍ڵkϜ9SUIdwlCvO#\LZlؔ_Ӳ 5pʷæǛqLo`$v]dKkronu@/ut1svмJ~1M'vhse$Ϝbi=*qVVo8kKsbIIIχT*ww>mXV;k,OOO\qF&]vdBcƌav<z*4dFol[nJeRRdVVȑ#Go~Zk+KR`F$FGժUKTk.::"##݋cŮ .Yq$]fMDDŋtP(<<<֯_/} 7nbcc3`2 3be%jdq,H~Æ z1cL  7ߤڵkŊל9sx={Dm޼yշn=zB+ǎ/r޽cƌYb;?/|;ut@[%p~n}tYFlOlbfM IDATo8u]xY0=oۆäJJ^r9o[6GH#wE'0_m`bS kb'D:{T{iysםեvM_ `Sn=ȒQy9jԨKΜ9I0a?0uԝ;wFDDlٲzM43g,nnn-Z ̜9sȐ!vڵl?É'fΜ"&i0{y]ɓ'_},J0[~ܹsmfggcǎwݻt+Hin<>gΜ:GiԨĉB/_o=v_ͫ=#VV\F ֟v1Ϩ@ Tx6g ޑEuʕ+7n }-[77ݻwcwJ| 3ȑ#ꞁmsب2 ]Syc:@V~ lӱk '~Xйo~W3=NG;t]<{>\'hR`ر |4m0X7棇%?-,)xXۆ>`s=`*kITGN4x۶m׭[2dR\|y 6m?N: pQF󫯾yfcbbuvĉ'OԮ]=z(믿3gs#뭷z'L@QdeeIH_.\طo_~`РA͚5<}puuu=JqUIt֭}ǎC ^zyzzBA\U>>>t+Hk`hl2h֬>bĈO>lmm۵k\Bm۶M&uqqq ~-W=#VV\F Vf1c Q3@uo3Ƙ'okk;mڴ]veffk͚5b=zFIHHضmg[z6*o;ԟ׮!p~tպ|E[_\ʴ66H~&GQ'g }8{hGfL Sr^JzY(hZ)_+r K53Xg|S`ݏ5j^p¹s`Ĉ5EÇsNvvv6m0111ӧOWgϞh4/_ٳ';w 6,==ѣG[ٳg]]]ˤTaÆ SeUJ¤SN)3vvv<4iU]AcCCCYw1O>2ٮY&S }XYqUva< "b66~ʊKרUn7nl1~FBxUYdӦM^^^r<44tҥ;w<|\\`={63>y !@%ZfT&!|]v{_j^Ͻ;̤c /ޭ`2.#WO,d"Է\F=pb$#F)'>Φi׬-%:E56}P( eenjalٲ~zLڶm[ضm[EMqdd###cݺuڵN::ty0[V(III+1cZ.!ٱcǜvY VT<JYcRZɒݻwKE%ʆQkDPַB+,,ng?#R*L KOO8pq`͚5̝͚5C͟?OOO_hQ^^ޭ[RSS+0f=OM]lT HTtNWšz_ON~">WK[_<_r :Wc2Yujr#%=t uS9{F'G;;ݛ/0փGrA~;C’]-her2y٭y=Utho۪-se}ZP5x!x}9?e1s̜{۷oL:u={0Ο?Ͼ:cƌ߽{700066vҥg6}/!ٹs:5j>>>k׮-))jnݺƤ}FDD̛7{͚5_F9s&44T0v-n=\PDXbXP<$eFb5r,Xn|||,YRF x3B *k35lpΝ3fR ;q℻7h`ƍs]vmƍϟ1cƌ|ykO8کF,E|38d7x#;8N)9~^6Bh~D6MvF%/ƸO͹;엚]=m`{ӷ2='ݳ2}4_pNfvDݜ25װ:K'Xsur԰<,jgZ~ڵkG',XI_W^qFkS={_]HHȵk:t]d5k}„ JjCR֘۶mO/^|1 ˠAףGOre&.Yqo|1l(.ײX#X_qY9Xso׏?~Ĉ...V={1~F^.gTTTBBӧOZ_ ֲeD[/X(%Λ7oƌ/ŞJNNءClc/]m:ϫӦM.D 3b%K[ Wnݢ؃jvݸq͛G^vmС~U+襛g^rURR 8r;f@xM_oϗ#G>xٝ;wӹ/ZϘ1ѣ̮V}Z*:::$$E2VK7cɒ%pݻ"^^p#//oݩ =$$$ۗuƏVpww߲eˠA]FJ/))aX{T[Aj,l2gΜw}EǏO2NGܺukڵ_~ɓIRfoѢ o#Xj1nӦΞ=믿2GG7zǏ3{( ɻw ۶m[֭[O0ѣl<4=nܸSNܹs'''gÇ7#ƍg͚ݒӧO3t3 3*Tv}N:dz*=JqȑѣG  ٷoŘ>ƌf9~86c+_?{l~wYr%1޽{iӴZmPPǭ/H3ZNLLdдZm6m=zĈEDD|7܌9r$--Z_=zfqdkkhUh]v1i`C˶ 8}]KoL\rE^w;svؗ;O=?ټ8,ˌ E@xbO"󥝟~ q?R ^? wwŊ111ЫWqĈRXvmOOOv K窲x 1zSTL:3c Uhƭ[?>rHaeѯ//?i͚5Ň}{%,mة3npV~sw'ؿ|RN-LKb٪&O؆nJ!;߆+Ҕ?wlUʮAT h?BD0h42lSǎr'Ox, 6lݺ5d޽l:Bh޼ynII p:4ԫW111111P(BBBx#FpAROMF3YP*dt6yԾϛa qdVhRÌ|^t;s8}ݬX?l茼@Vs.`mXq[|1i[!;@[' [:}XF ݻYcY۷o|&ZjqwهVZl:wA``͛'M},Ţh&Lp%fJ222,Y.ݢE _~֭V*k:kZ3"x 2H53M0~:uΝ;CCC#""l֭>|x۶m޽T҉otjQM.nV@׳KM9ch[[>S+ rҥK{s@8p޽۱cǍ72̙3O81u`6ص*cF՚W" *Q>ӽ{6mڴbŊ?Zn].\^^^ 4஋loVب>n5( qZ7֥i6 @qiن# >8KK#;h7ҍ V Ij>oL>nxKS O14l\V]ZӥS jwΓeP*dV]PMI{ffOCjSϰA°gB6+M9z)NX+f<azcuk+_ì_Ftֵi{Ӵ,߽Mڃ;n9L"ΝMٷoMӂwLGޙtܹi&67)”1>~xJJ١cƌqrrznzׯ]6,,NAY,hl'|*eggKXC6j}n޼5_GuL̙`BÇsN:GuXס:'g }BY!~9bs ZճQɭ'ԟ۶e0-Z7 ={x/bgPz٪W0ݚ)tN 3,L0yqbr:huA `#IVeh3rKV"ɩ9ߑ0|둨e^NbY$-,uWH&v#B~jF࣏>rvvt f6m888pdhѢŬY.]^vyvLs̙_2a 4(>}a.a3f'o啔@ݺu{2wb WWWv^`ժU&M}c嵘l;skG/&W_[~'K'ྖoyaMs 5{iQ=>>`N iêL.6aܡY^-x0C;7Uȩc,jDy2PaItqI.6<ưE}VFGJ@  ƍwƍ+W۷N:qqq+WܰaCQQ̙3/]Ʀy悯#Mvʕٳg9;;#(}N>#_K˖-;wne2J YfͬYUTj'Ny+VlݺkΜ9111M4TAcƌԩwܸqgϞ%cۉC;7e~~|~)ڃ_ gn-7#r;J1OiR9z%?ر141%<] 3'd j4[eS9x>Az&P+p~Ɠ)ocVxr̗ X;sXJ5; <z}oP~򦴣,Ȋ+>gȑ#Δ9sL\)yk ݻw޽988\rʕJUz3*یE 4hРA懢+kE5/^:==ƍt `woشmmO+p< dt"ӷvV9g[3GvrqSqs.L|{XܦcƏH/ܳKK_]`x9s;;ب5@ eD `3@ XL@ !>@ eD `3@ XL@ !>@ eD `3u[?~|QQQ~.f]%<;g"kIffѣZݻ.\(&yѥKp6mگ_?g* >@ ^KKKKsΉIZjҥ&2dƍW~qr /3ᵤI& +))p႘//oK- >@ ^K<<<~gGGGԩŶo߮jŔp*␿l@"0`@iiÇ-Z$!" {Y^(d@ 1r\a2 ݻwnnnu%Td@ /q޼y&Md?S3&44bŊΝ;Gꉊ;v,k:t(#;;;AS1{Yn]\\\qqgϞ=g͚e͛nYnQQQddÇsssU*Uzt2n8???i׮wM6lXxx8BÇgjߣGWWO?t;x(cƾK[(,i銢EZTW[mVJ]ZTѕBPBc2v3=LB78ܾ]TT[doNN˗/VG$'Lؘ&JKK#a>>>t_LL R]]u~7n[k׮hii766޸q(22288ח+))dggc a@M+Vǣ;wt̙3tUU\tO[[&F ]|9ZSUU` Q<Bjj*-66vL*d###4YZZ5f̘W^agxiP{=4i۷1116l@;vӓmJKז566Fp?@M6aW^Ex<~hzEEEZZh:::LFt۶m^rʋ/$$$G[꭯_f b0 h"壝;w?'f@ 01~x3334c']ΝCׯ__gBMA0 H^^}TWWi :{)1E"$%JikkKJJ"mZ~& >>>/^@]]]qqq/b%''WUU)++ .9;;JfʔC F &$$p8vg}gg' !''gɒ%CO 2|٩TVV"K.y{{]zUUUI/^ܱcǓ'O>ᅮHaPkXgo]]]h`={̦R 2|+Wѷo^r# `̙裋/?^@@{Vooo/6*((.d`@ @ Êmpųg,ZYC믿ݻ.%%5 o(pEw, llk׮DxԖKKK aVO]]OVVۻT@@`Ŋ}}}ìҐ2 d\x1|rIIDQTTD35k ".:`0}pLn 222X~̘1.] ,6ӣGttt,Yoooŋd+++uua?|`ddFR.\`mmo|J?ƍéPcMpSNݻwp۶må,Bt:???2D pt:NKKk`0ynhh455 Lwwݺu` }ս+ SLϟ3g'O|3gδ888"PLLL,Xzۇ^666Jׯ_EC6JE &(|||G>>ǎC.Y]aÆ8p*::ZWWv:ujtt4FpŸÇ''']vD"o֭O&LO>KwɅ/Lvba 8h4ŕ+W-ZtIG}}=Y_{? Mә<qᔎ֛'**쌬Y`` z{'PTTD:::N81&;@ 0#Lvvvꡡ<NWX8H$;}]]pƞA8nhzGG/s:m̓]hcGN鎎8nڴiW\}𡚚zU@@u2 0|;?t@p8===4JAA鸸h8''( ^=[TT޿&N tjɒ%ӧO/..FmnQTTD!'ɼ(0[ f~0LG۸coo뫪}vDDZLS#XF5Dbo۶-66Y;|Ycc#zn̙μŏ-B0>/ qqݻwC#`6ײjժ[RR[gϞ4icfܸq8 j9aq96(++gee-Y$==ӧ-222>>>\L^d0%..Δ2fOMtD޷qƊӆOsZs@ dH??ʈzG e߾}?F&RRRnh!r}H@NNmzzsssz $\SSo]旔ԬY0Ngں`0VX-&3LXaa֧N޷o߀@ [m&c %?zÇ={`׶=/tz*={6r{())?G/_|QJJ͛7믄Cy7իBBBܰsHغ8QߏR2m۶;v`-K~ABBǏ̙@B 2l`ksȏ.saooNEp8絴c;;;jwwgb.ܻwo…@|DBBb޼ytuu-[,!!aĉ׮]6 Ч%bP|ZH6ON]]ݾ}9NAA Pm&uҤIaaa!//`0 :K& lLW@ ,c䔐@up8! ,ZB 2r)((͞=]IIFܻw֭[BBBΝ^@ d6ӏTNNӧSSS׮]G deeuuuc@ d6BpppppV@  @ @ ?pm@ Cɓ'Up"""&&&[aT*رcݿɒ%J#bbbJJJǏ={b%88L!!!Vy$55u핕;vr ƍ555 v@[g===d2e֬YVVVSN9s&566SZZZdddII9l  +VU__ى}4i$[[ۉ'3.???!!ʕ+zzzh444ZZZ:::9s`1?4;C%%Fw)))Q@ a`~>|4ٳWWWF/ ?kIUUU5Sаݻwڵ܋$$$xyyQ@&QK`DDDoh~~[nqfl IDAT[HOO?>L?^p!?~ӧO>8@~0Q'8^N$7mtA4b…&$$ٳȕ\bbp8Ϝ9sДހԘt!tttѨwTf6@3DWX.//~'8s 6,555F KKK-50epqqqePPh6@Fx<\?Ŗ̜Hb3)**2 ao4F ~L&ߺuKZZ{3@ @ #3f`t:{{{lÇ}N)\5bprrRTTҺp@P@ :49+++԰Ν;j3hkks|)nܸ޾wI&*))͜9رc###((h޽3f@2|}}NI}||FMd0۷oHKKkkkc01113gͯLb#&:;;/^V\ pww'؁ѱuV6)II#Gp)[___l}/^˗/KII!_|Wgg琐[}>sLN gbb"]dɵkvڕa?~YHHcLLʾ}8m:uNe{uWWJJVVV~;Gejjz ~ɅU4-66VGGBRP__w'N4.O8=z ֯QXXݽc=B q,//޹s_ݱcohh@-T_ 2F;իW<޽ॺ޳gϞ;w.::ɓ't:]YYbӦM)))<^i֬Y`ooh!\mF(hHRYWи?`zL_}N<@F ب2SǏQKK {nzzzحBfffV'&&VVV- #0/LD6GRLүX4\2ph H&vY(ӄg@ Gq:ƅ.>~%4Ě}p`3δV__pNnee5nnذ!өïi}Q'@^cʻwRf͚~155ñzZ 4-**j@3Mϰ9V?R(ƍzQ?uԓ'O*++9K$QBBɓ߸ǜ-f@ < بp'v899a3GEEQԁa)رc Fa'M `ŋ?}TOOojpf@~B9ک#`ƍX!UUU׮]Fo߾Ck*F~~>6JRШ>>-0Ǯ566zSAd'DEEwܩJ D1?>p @ }_L&M:f0T*H$|̘1fJLLliiA؝~~~}Oe0h3A d8F+++{zz)FSBR##W*tCn߾ d$[`6%++ÇScT*ikPhnn򪪪b0O>ݱca…X(7XXX/ߟ6mV`7~Ȯ/'zxxܽ{Is:"&Ss~/@gϞݽ{B$%%aӿ|bgg7yd[VVs&HHHSq2:n8QYYn"H>>>۷oG/KNN޿?v{App0xʕ'O05aҥgϞ:ujmmm[[[jj*)Jupp?~dR̙3gSN͟?bӃRFڼyĉ:YVVVVVk׮M6/^޽c UV޿Ϝ9}>_ǎh"$ߤI t^p0;L^~ud~zao؁ks|Ғgmm$..LLLfϞI$?A"lٲyp6@ ߟǏ7#!!!fl޼ƍYYYeeeѨ(+++$9sd@{{;c'$$$&x===1n8^4QAAmұcdž 6.e˖&~G􂃃 kjjdrffKV^L`L6l8qDHHHee(֋'233MMM}}}y|H&3p-Wܷ^INNP(MMM4m۶m[FP=<<>}>immxզɓ[nv!a$LΝKIIqttTPP0aµkFue˖9::" !Eo22t@ .dݻVVVkx<~Ո ט'':^SSdɒǏ766/H7oHKK755ܹ[ŃJJJ?|LPl$ERR.%%믿vvv3gΜ:u PTThѢ={;::2{aaኊGDD S$s||ػw$%}|qWhݺu ȭ4t{;;;oݺVmٲΝ;aaah?zSY@JJʡC߿|\\\^^^yyynnnxxxqq1[XN*z}j߾}RTTw(zxctttVxllDŽ $%%O>>z꘲בpႇGGGGOOOXXiggA|# "e԰ !7o~/_z%%%UTTgXߎ???'_SSc``경9r$%%E\\\PPݛŪ}}=̄@puu-++suumjjruu 鷽iܹpb)677/[ܹs֭,.**jnnB& "z~-h^Z\\{|uGGGѕ+WW/&&&%%*2<|~ 2pa$3=|;`0"""͛(//Gt:LqwwGh.--MIIAJ(8 Dloo!]]]3gdffw vOWWח/_X[[[4<SY5LՖE\(Jgg'Р [222UUUHԳ-쌌 EV?̤*Jii/RTTۈ~:MPP644 QEEEuuuuuu###WWWp'%%! s\R]]HRRrH766޽{KHH@VJJJHY*o  "xe l3XbΜ9wܩuss? +++**-88XTTT__͛iiiT*5$$dڵ<~<W'OƏJ"Ν;e$Offĉl# ER/]{I^^31>j;::޸q۶m0VϰS:2i)""B""""zzz8ԩS;wdR͛MMM$`H$.ewל#G,\(ގF[ZZ>t{{>XZZ/HXXd29Z577^pСC[nZZZ*(((((L>]FF1 [M899!|o C8@ f$L'Nlذa֭JJJgx9例kפbccxZDDHrrrtt4/U+((ݮѯ@))dH$JII544d2Y[[]xСCϟ `ժU_P(Leѧںaaannnd2922r(z[SLٳgƍQ m%˖-kkkLMM|2ۦYYYjiiؠ|}}544FũL |c>}ZWWFAAaӦM)++?{ =z05GMMKPRRrtt$ _x!/jeii)))xK.!;ٯ]`2pNNNL2vXqqqd` 0@vv6k˷*d)..vrr*.." 133 ފ@l \(cu#{8䧢i: 2hhhٳ{A^`۳gOeePWHLL$ݥ3 £G(2|]vimmMѴ޽{)..677 PQQ>"rrr'OD'%z9R`+++6СC222Ǐgm&H\r#֞,..655]rArrr5^]`AkkĉcbbFE"Ν[SS(((1c֭[ǍA"DGG/[ёH$q)ª(\611AnC;|0H̚5ҥK^_Swq̬mx}ܲm ŋ***K.E.eMam#'I%V`~~~\:::XQb =(A ^6Sbb[CC F+-- ~Q{{ehh( ...//<777<xDPPii馦&;w^~7oLֺ>|PVVP([l mTJJʡC߿]veeeݹsȶ'^P(222֭KII)((8|MM ^{]CCúu뢣TUU}|| ٝnB`bqrrjhhppppqqlՇ8*{ճfz˗$͛7_F|em'+vq|ELڶmٲΝ;aaa)l`I*$֭[-V8 N@xaLSLYlXPPPGGG~~RAA.NCG쬮VWW񣦦۷mllDDD8EPT$--͚I&ۂ7n񶶶7n\@VV6 ǖ%))JFnce IDATۨ7o]vrrr["""'O\n]{{;ȩ'UUU-ZD$MMM]]]Gc޾}RTTwE"ǎa$122FGKKKAA"a-m`dee-[ŋyyyFRVV phwqWx^#q;zxxL0ARRɓ&&&)S9$vΜ9---uuu\$GSΘ1dr>J#9tO]]9r$%%E\\\PP:ZVVRSShjjB)L"ZPVV^SSciiUUUY0 ֺ8IfmT}}=Ǵ… AAAp= @gp86I^勚jll(((pbcuuulp҇,/U[XX~IVV:**H$ZYYqj[TTT+vqj2>nܖ$nLL SJZZkY?҇bEEESRRd3 \?Jr 3222wG pb3#V^^fiii/RTTۈwwҔN"$%%bSXe- (##DM:"lb+Q?f.?~ٳ={UVVL&#a )%%ŶEXxl/6^:N&kq622:yԩS222?L  ƽFNv~{[dddpNNNTT6~.SɥيOJJB8EW/"""(A NpիW/_nkk !HMMM===7oz{{755H$A"n޼FRCBB֮]-\[[{…CmݺU&NR|ق#G ߿'---aaaLE8F9;;:u*33~Ν\LGk׮}!Q=^zxƍϟo۶FHHXN]ouYYYQQQmmmlp҇8UϚ5ܹsSNEv߿ȽC}ܕa;޾pJ@[򚛛jkkYSضs'e`cX{{> o\x\0{@ \6 ݻwulٲ6YYYOO˗/ ***jhh=KMM-""KDD$999::UÇ/]*`nn5ӧXXXŋ Y2 Ҳ梛رc0۞RꆅIKKH.2tiY[[_vMJJ*!!!66dz-IW=k֬dUBWWW\\SwH/2lW.r oJMMk2eʔ={(**mܸ5mY??)Kljhh())9::"] AFF˗/gpWRRvqq@B=z8LLvw)aY[[oEl@W!ᦴsܔ̉Xccc4:~D꼼zzzΜ9?@opb|ǏgΜu@ sL>MWWWYYo[#HfXgΜyAx< I9 6 @ #Χe}O+ R‹+ ܎A];HFNY^~m&XWq>G?ɫ<FIZDg % c )"0N'drJ^Vf 3< #h3A DPڻ]މ(.L$*yWqt jʟgTQ7Eضw.uw1!펛>36k)Jl[siDqY\'?}[?(|Jm=HsvF=gFAfCnF 6uĥرdʾ_ZCn錠/OY]fQZrp黡gϞ"n@ 5k˒;/KШDujK@zA%?y"(Q\5h}u?V4DUqMkgonS*;ð7+l#=t4M*N-0SnlB []L|?hP{i MtFINYwa-CRDRDCD0iBSk7r/*jj/yhkkH$+++ы/N6`h'޹s@  Ŧ+W600HNNDGG/[ёH$rϟ?ڵ `mmMѴ:::GkgÇwc[iLL̨QH$ܹskjj$"{NEEŋ\gKb0NJYrx`` RiӦMl@Fi>JWMNuq-@.!c3%%%7N@@d3fԩ!9;{t?p8l #>>>uuƩޯKl577OIIu떳3/_!aIII!!F4_m28\]]]]]n<.4`  qZ政,*ݽ'n\r?,-rjU"cUq_IySxY1WUmiLU9QH\f^7\{ezE~~Ub{J(KQQQNN۷o#""U.C^z0Q.y.&D=}%e͉G䥳tя%?pe'wLJc3gZncY<BȷݗOnw2ʫߣm dsiK&hڶxrև%E~\\ܽ{~FڜÞ={,Y2eʔׯ_S5kDDD|yݺu靝"""$)""Yꗬ 9rDTTT__۷\ř"[PTIa{{ݻwDdGGG33e˖n۶FHHm"wk޽6l8q"2sro͛7SRRH$ Htaaa@ olDBKf5A }d3IP^Qz I𝜤@D-3 Ug2‡c`+R9r:|ga/w"+.2 e?E 9s4BSU@.SRRV HgRSSINN:txڴiK./_^lY[[gjj˗nmm}5))Xv=t“===]Bd!4.]th"N |;#y8@ `m&@ 6@ N=}Bh3A  @@ >o_;W=t)ar^U ~ KY4m{=S7:K m'n H"̙ր%SlE'"V86ۺh+zZK$ElWRfCUSV OQk{y/Kmwܵ429TG}ћY%⻗*J NTLOXTTM1h^[N` |g<@R{o6^`z53<>U7Rڦ;Lٷ|KkȍW>siDqY\'?}[.}woh%iQb\/,ktPk:Z:{e cwj#O8w?|mٿ}c>/ΧنCKA_h)ڻ̢9@Gw4$Nm=wIIϵ-w3?t*H"p2q|ayC^Dn%%+.G{_A8JOWMZEFDػ(gvXz HSbEEhޘ\zcc4M(Ql`7Q@"EP˲ffY~?Μy݇ٙEՐmi[ם5u'KGk÷:dig?Myu!,j :zXz8{9[?m \N/Ozћz FJΡ)klR[hx.X^:Zy%O`iN&¦VBUJ{L=lrUdg_TV-虵e5!GsK Z,B?aUCO+j !9wwl#םRsg7`_^VT U,붩J|ަ9i!  rՌ6 ^}iǺ}̻?sD&wgmǒo>ޣMG{1lmE׳ݓcRE\sכ[Zjj9z5LJ,R>2iYh )wy⨶jgYeUYӘ_1.? @~߾yδv 6i_`C z@'׀Ϛ5+66VGG?'O4zgDuuu<3f022֖~u>Δfll<`BwPPPKKg}U6+{ڰXb|UlHd:kv='JjuȢ7]2zUdIEm+ jD3999?~_~ŋ===nݺ>}'O fgg%dU~_SuǮpwwFq=v\Uԕ:g}߰a ƍ$[xݺufJJJR%dD4fW^߹p vtՠPL*++˫kmVڝMsikkKb֠ᛨ׳5}- xMW$\Nq喟* {kO2,@Qv[9yS5 Mޮ&xi>9VSS%U ĄN+-_1feXL?-9:xH{BƖ'noHZ?{׋G.;CG v'jntكő\廫y$4-7,]L b&:4mmhh Ռi㟏//*ޱЗtʀT7kk2 ;{Tq IDATow{SBB8.eilxg !des%Vƺ9 gWPPҪ!B9Z.#/d!$ek=濏`S"Eq,%R" ϥ1ekW9SW ^ul[?=v&hBJe 92c|YԪ0 ׵6wsj<LVƺ;Y<,bg66_K,ʪ}z]+++ ZBYj]zUwwwmm'.\PI+:cǎ={ `֚SNUtnnn4xeeI&YYY }}QFedd" uuuMII!7a͛7]\\ƺq&)Zd9bff_-\WUUXVV6|p@sohΝ3f#""F5|UUU&L(//:t(!D*x^^^PPUBBB||ĉ޽qm9svc˗>x@<ѫ"; #[3}b:bOOO??pEOGqqqeee|>ovؑJg.Z_2`؜?=ܹsDMM͆ XYYeggڶEr*jg}vǏϟ?_zovڴiK.ݽ{74 ֹ9ޞ[QQQX$߽@JJJ!|>ƽ>}B !V[0 B+-ZdllmffBiSm/BdXW̙!ccc_ijUUUl1ƆM "E9\)5ʊ좰XXXo/^I#[䯃O7 NxnnѢEӧK-qrrw.\.755-,,Wꬓ_ҵr .9g |+9VOQUVI> zKVn &Lx{{˽+[n Ä˝0P(6m!Y@,;882gWdF__̘1n^0۳ =RUN{vq6A477wtǕ+W;"ύGyyѣG}||-ݩ˗/ Z_~oٳgGE Ƚ+%Pn=;-@:LZBYj!O>&&&C}!$77wĉFFF~~~R_=z'X$=V@oY(>-uVmmӧEفBӄ(;m61 rJBȿoa$" 8꣨r[B^=qą J)< BwѳgOɪ*b/_vss[\4i$+++@?j(Q͜:u*0۶m(+))ÇoVss|~CS~?=#VXqɤ$BHii) GoBƍ0͛rXv,Ɨļ SSS ^zVs퓲o@Nc:5uΙX7+:sϛ7fQQQ ,x񢟟_yyԺ3f!cĉrJ!C(С%K8::9r~FWWW1"&&sҤIIII={\lٲe;jZ[[XÇB]Fz____ !⨏˶H. u;wU;&L5`B뽽㥆[f*e``7BJJJnܸAill;;;> td߀&w8^n Z'҃zrww߲e 0.,,ӧOhh(!ݻqqqG]xׯ4Dw9|B#F 믣ϝ;駟ܹ8+V T۷kkk^|ƍD7tP~;mڴK޽;55>R߂,8@qqqeee|>_-MMM;vU;mllΟ?G}9vibbfCC lmm9h"E=H=Ǐ !'N>|x@@@XXXxx8zYf͚5NxyyEEEq"۟}?~<w3ٱOϷHNN > νOo@N;/bNKL} o8q\ѣɓ'At+rss888t% BEeE4=L?tH$ NNN Q%> wȑ/rz ْ>>>SSSB}lG}mTRRB7 !nnn_N~TQrUQEž}9SCCÒ%K~vG6sᒙ !tgƍ&Nvc~u릫x @8|wK9gԤUUUBPP[^^~Νm۶EGGСC GӧO?ydذavvvÇm߾]CCc̙k֬پ};KCC G}6E9ƆnH$|!E=Yo޼Yn= !8k !aaapvv9rH$Hn3\]yVUUղe!^^^'O$߿lɓ'ӗ+V9s&cz})c*RURpƲcB/Q[[KiiisP$%TIy%tjqrrw.\.:{ƍG111 ܿkV\\|̙={Hm9-*Z[[Z[[GFF>|x޽zwPPGu]~=22& ^VVΠAbbb/fR݄I SS[XX$$$(^)rXQ=,X`iiI;_ 466FEEП9$$D2g/C>[n6l;ǎYYY{Јhll400`ܹs';;;,,͙p_) .\q@Nŷvvv{ҥK9\*>>/tNMͯ߻wÙ3gJKKMηijjڽ{5k͛tR}}/N8ԩSk׮=~k$sРAd_~_~ >}eRSSbbbLСCvڵk۷U?!ӓ痞.-[BijkkmSL111NNNׯ_w?~S$#o߾QKK+((Q颜 & ‘#G)ڴ(oڴI7Q1I%%%e<|eApp0!$$$VppԩS_>p777jEEXL6rJowpp ."""GBWə*++Ɔ:aO~ !z\}֭y~ UWW痹.^xȑQQQO> 1bĴiIIIlݻZ>4~߾}]T]]Mo6l2>oaaQVV&w7n ?~ٲeΝUL()/srf##p闳TVV>|ɓ+U6gyGΤttttgΜ]P]͙9BiiiţF"חҧG'$$xB >|baaQ]]M477B#͛F /"(>}_ZZ*5r֖;vl$۫lT__/̙rT'3ukKJJ!... o߾좆EkkkBjjjߊF>}gBAQQ!ٙORЙT>}!URae_~Brssn7bC{C0|w}w޴̉'|||!W4l(UHو#T CW>߀Eaaapvv9r$W-蘞N1663z,##>Lnmmx:Hndܾkvujjrmmm ,fϞMHNLLTs+WyzzҴĉ3f̠%_\N>ɓaÆBE"Ȑ^ԤUUUBgIW:dBNRh3egg/X2&&]D ad_hQlllll,!D8{ݥKZ[[Z[[GFF緯 666׊Ϝ9gE)† .\*+~z;ѫyC?=x ##&HN [둑vvvHHH ӽ{ϟoffvɥU*挌\\\߶۳gK!^DKHHؾ};%{ujЩI=}:>|8˴dɒ䄄cccEgrNM r`Fj;2yxxܹ3""B__? `ƍ\uss!&@ms *xj3 adVbF>F |ǎ5XAs&9掮@OH9ӧO^WWGgN4J 5>O>&&&C}!$77wĉFFF~~~c]|M[[{ĉi;vS(755FxQbْفBӄJa%,_aUVr{`Μ9 l߾yٶm=&lGmݺAj:888;;+G+///((TCCW^IIIJ{ի4… %ϝ)zz|'еDDD}oiiI6l#K,BqO?tݫ^lϷ(++\ee%}СC'L@dt}ժU}-Uӧ#F6m0 ))R[[+-[FYrWΝ# 8P,755r턂Eg˸oݺݝ5auttgΜ)wUj;;;BŋG>|*=`*|'"""r$t{ ۷/ ~7n\xx_MILLlhhX`UvvvQQ=a``ЫW/ww-[0 s>}B޽wŋKm(..|>4455ر#55U*>6::ܹs<a~i߾}!!!zS=8G壣… wt;wTVV;ZST#F󣣦l؝;w/`[XX$''BZZZ^YYm||4!=Vi9;;hnnnҡW[*"DΔ蘕E1668?rHH$Balllyy;wm}AUFPQֲO`H2J0?{xމs蹕VIgjjjUUUBPB"ےg޴iÇ &L]^iiiitԔe3Ky†kkk]]]3@{6669"=`*|'Ui@%ry3^BVJa1**ٳ7nokk\PP w.\pqqR׭[GyuYYY 8qࠥ5nܸiӦ=@."\p d'(QS1X,V4zɓ|וޕ+W\]]&L@e˖IHs -wKPoR׀"uz^RRR!mjz!/!!aqڪ 3zJJJjhhطo JA,Yᑜ`llLϋo:/L'uX!5U 2icΝ7nnϋo:/k?{[_@%73(%rBanܸq@ LxDt)sssOOSNutQk׮]a:>:a˗/2g5kVllN ݻwGW :u>$,--ꂃ~oذaƍurr-nݺYf%%%uΙ!^^^YYY555^^^]#z&BVSS%U ]gx.3( @9L!gP9rșCr&3( @9L!gP9rșCr&3(9ױc!%444 {Z2++UŭU8 ܸqKhRRSSQ//T9-----=z_o.33F$ٳgϞ}mjj7o5k䖬+V044=}4-c+++@陑!Jmm--#9!$++7ޘ7oɓ'݅BE!s?~f~nݺ%Ր3gθjii5wOșΜ9srrG;v֭['rss˄aÆرCr]]]#>!fffK,9{lzz !0aBiiqLʮX\\WșX&&&:88dgg;::-llll2_]]MIOOwwwomm500`,߽{ɓ'kjjAAANNNݺu7w\/*==]1&&f̘1cǎ۴iӍ7***!@ <wmPPP^^^PPPyyyPPЖ-[455e|B0t0۶m;{%jkk$鄃=277sO8 tLTnn333o޼y}-&}_FSP?Xj՘1c._ٽ{knذR]]+zDDD裏ihhx.^Y!-ڽ{SZZZ222V\r6 +b־[aaa3gΤ3Ǝ>g:ɓ'rY1+KQ3;Ec_FSPL&L h̙۷or飏>`>N.]lIgz뭷222~׌ +++Ebxխ%+Ңo(t j3UTTЉwy',,Lx07n$t2a###ə/駟KW*|-5j7Ř1c^vէ9SnDqqٳz߿???2tÇKQꫯ^u]]]@ȽE;]TT]t++-MH^jb4@P($o@ ӧϾ}bҞlnn>xomkk)lll=*@QSSǎkffFo|7MMMJ"Vmm--!ϷĄէO.ՇtiPPǷmF_=-RRR !<oڵǏi3f?K?cBȐ!C^DDD5ϙD"رc9&BȜ9si:tÇ]]ݼTdݺurÇ y:盛O:522rlysssGʏ?HKjjjtիIIIlᢢ"`lӧOLE-a֭\lnnݻ79uT洵!/V@IL\XXؤI߯_SN ;v$:th˖-tz͚5tztښZR]]]U"h"H4dȐ~iɡt'OJJJ87Ojjj4=Ceff+Wd} BPZ'TVVw}N@:}ӧOQc`]tʕ/ȑ#Ǐ*+/qqqoyjǎ"sBFiiiiiiII rYB%9dy&h%0}er3gܹs]]]͛D+**b e?ٯ_?:=qD:UVVFGG񝝝Ecͷ tKiibŊݻ߽{͛J~olluVJJʍ7.^CUUU5669 YYY*|HT}mm777ǯ^˩"H;wؘ`xQF 0@[[b/9766fF7o]]$N3 }499KJGH~zzHŇzhfvE]]]jBhzWyЍ7>zH(~6mJKK[bTI6$%BaKM@d$7#5pݺunnn#N3UWWkii-Yӧ7n޽ҵ444׬Y#ybwݳgORR0vvv--- oCJ˫szbeٲe?|q/_7s}䁜 6߿nHi3]\\؞l$yyxbiiŋ$0:WP(9rȑٳg?x@*;vLn===BȈ# LLLFI緶J]$}z-%GCC222hU_ݻwi+++0B3DO%!#*;;{쉳v4Ą؆:3fPuRΜ93f̘ݻwϝ;>L9S\\\XX۷Nz[nϳ7Es&BȌ3"##\zU l޼ٸq#{NmѢEtyʕ {K*"y+H*)y3gϟ??zESHѴZ6BYYÇbܹs'!!Yz5N2a4SQo߮C}!G~~~Bv4_{1;!瞖......:::׮] #(Z~w:88Ы'Mtԩ_IrQ"e7lذqY[[3 }'NH||gΜbccO>m믿J/?y؞={?sΉD"lI++ccHV?ћnBƌ3zh--- 6f*K.+W_}믿5|뭷hv4֭ &p "zOqI}fgg_vMOOoՊ֚9sS~~~}}?0vXsssa455{駟ȮO>O>BaKK)S\vYJJ=Eݼy-o߾^zx<Yf#ښ5kd{zz]E3KJJ{=KKKattt<==wА:$?cwww---almm,XPTT$Yn3USGG|```tt))Shjj(+)7NRRRL""KtV;Ye⻲r",t-9!avtE=z_k6< U uj.]x+f͚uEv͛7>}jdd6!!444O?MIIy鵖3ԃQ/#9y>#Ez뭷 ! 8y$!$$$DSS2r?ZcƌD={LNNO]\\ΝB [NrslZ-Jڱc@ Ƞ3:ԭ[7mm3gɝsWWW--QFB͛ghhfsdkX__/WXahhhkk{iZ@6>+<<|Μ9Ǐׯ߭[!lܿ?'۷2uTM쮶nWQٳ444Aizژ8ѿn4lgn%a-SSq>cx{o%?|M\iU!$-񻛣RvzΓ5GJncOM6^vNLLi9YYY͝;']Bȑ#GfΜ9uԣGʮbjj:iҤ^{͛KARcv;w{A8ުr;YѾA=˻ &sr?OT{%XUe'e=rD][*ɉb--ݻĈÇ;w^xӧ|qb?>L,O4IOOիMMMa|~sssZZŋܹsq___ŋnc``Q__|sT33?|Ĉ7oCSNUWW2dX,>|WiiinnQfffffBo.v#gWUU]W^"Hg===b={ƏO4hЯ*leڱ]_naaq֭Ǐ+mHZQ]򴶱Iհ~yXԲk_ O$`o?kJ'5ae?}X[ߴ!?BO9lp_H$!=!33S(C3`X|SSׯ?zhرFq\r4;i !O<͛7ٗ7n011kDݺuuVZZaCCoiiLSRRϞ=F}IGnvt/^;7oݻXޛNVQV/mPdtW>^v0 xIDATw}7uT:A(>|ۭOf̘jjj5JQX+++ BϞ=?i$>_)rح[7:am۶={P[[‚R\\A >|XjΥKN8r蘒 :e9477g)..oll̮.=f{emmݧO . F&ڱ]}8l0Z{*}=PO[SKC`Mgۘ 6կi(`mGgh 5U5D,' D&֎A fQnQ0m$kڎ .-Ý'{ot)~w{έpo?&L5z5nF_iGp{9[Y~2B5.Ec+rAx 9U0HvH ggg$gmXO , x|njA$1t:Ɔ}Al۶6\5E3qN!)dA[ b˘OR[%MAb;?謧EӤ$OtzC왶o~…ÇoܸqH$h4srrؿOOOyy9u--1Z^/~L#E4U(2$k׮q L&bq\`ٳ~r!p8bXkkkCCCFFj=y$I`LnS>b /^T*Bl6kZX^P(>|8.++3G"N ߳gsssBW\),,:vunl;vH${x<ŃJRRY[[na[c~9c+++FD"h4N!,_P.pUo2yyy*cΝN EVچ+o52kxW>][nw>qoU}S?_gEWl^nϭJ!c29<1&F³{uu.;;gb B]vYV'N>MpԩucNİ318:8_/Uv)Ť\omP8]DOxBǙ"MEa,-HLxWpIy^r"%@BȢ d>S ASSSGY 際+++7ڐwJ^\\tAܽ{i||af%Yw_lF'&&|>_EEFF %I2_zzHx4[9_3R@`0=zTVeff;wn- @!yp6f` LFϙ=0=rRPsIENDB`././@LongLink0000000000000000000000000000016100000000000011563 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/img/demo_screenshot_authenticated.pngpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/img/demo_screenshot_authe0000644000175000017500000026737511061320663032036 0ustar zackzackPNG  IHDR$0 pHYs!3 IDATx}w|uz,ɲ-Bi0 J(!t% )$y ! R % f\܋,wNvwl9Ɇ8vgg3~o yS<)OyyS<)O1<)OyS&J2eviD<3Ƨ :9]DՆqwaB]kemʌn-~Q "XhGK%*wgc9r3t[2&:rʅ)f,,NQ󸱮4y2Wd+εyh{ )AޖN st3y/36D޻ؼ}M(r8uQMtw"p(zQD0 `fڌ]U:3[l.5ë]Z']^^3eEL_zI3x:a 'B9sfy"qLt ̹egf<*HESKp\-ǘeLܟ5'2q6*i[C81@Q4crVq㤸ZdhCٺژ8o4f09&suwh 8QdB#YYzkԛdnY&1r+hk*2;q&xq ]k/N0a:bnܵqAqPYew\N3m:Gr.bnFθ9y#r&E+=_)ȲKYDrѕSpĞNv@mnN|u~_b4J(0طv` :emtu`\1X24=ip&PX#8+ S!aV .IC!֗ ܃ħg1PHGKI^!z*l1: .e|+,T谇W|r_g*[0u1'ewbsq t;WHQ ,)`F!f7shsޡ͙rǶN7N) =rgn=?G6(Bh☵\?7w gǓm9fW%݀ܯss5 ]/:oUgE!֛3A/(YV-'?ڲv-f_Ohv wUX6Rm00z!_e0۶̱gjf.\.+C\ڙh BO3 o"Ѹ 0BCV#AY?)\ 1`V^{vg6V\HD轵yWU:p.q8Y;l!T%»h {F9f' OA0K=/qnt. -?nmc5GKeY9Qm&>OrMCH 2a  R3U|w1å,, !!jƛ (E-ŨR$ A YH h Ahx<1!8fݣÞ98s-?93Y̶r8ẈYǽle%9mdab#9\8qsg E2ǕG< 4=TB\2mmi0Gyl\:RۢXQt}`!E!NM h4|@FtVMB-+kg8~Y7^kt1;fs˴3g WHdLߊg #HlxhAQk0DH0 2ԇ1v 2EС٥D'Wt5 0z57R=۝oy8m!nGC]B8A53/rt{V8P_AD~ұi=wH2aYX<OFs_=v*hɤ%P(g4O< 9Z@/К<tn]岒d`01ՑW.y"ʣsyC*#ĝlhj4#\[_' XvWw@QC?ښзweG$(XEyqEEH?,K2% k႞]mt[Qyͬlu#e`Bwtta>=Ktt9 OYG*yw‘&7Ī[Ba(L*-zf*}ɁdsEN+Sz3Y إ8~2yocf"sqJz`8@)e >82c1 b(h$'̦ӖEmLTF g@ +wOM1$d1Oyg%T+Gs*`wP$s\yt=@cHc-4b=n{I}!z}'rGﱿ,Y!j*0Y5KR8ܽe/ŎӿhoO'G[0ϕL6&:rʅ3>wP$s\yt=@cH`q6y8qwWFig  IUҊh*`:Z^)V|bPR_0ikc=s\<J'RQMSM5}u>SB (4,yO/ 2E [ӣ=fËN.)5 @B)Ԣ߽4/"CãJj>7~taIrOBs&yOj|yYryHEZ®M͵Ahps 0,/v\7{Y3ΝC%E8ef$FAJ 0df֒?H|8-JAIp=yq].9NZ%:qa''"Ơo_Op0UT@T |rC}xlq$R>Ni sOWPciJ{v8?d~}g/aJ@nykwXX't` {jufG]:'8() 1&S)HTI"QH0 4%B,ٛS$B)dqk⹒&TG.s\pd+εyh{ L!f35GSQ^!.2L#B1@@G " e|!)(iF5ŀƆӲcѴ)uEe|?%PR25Ʀ,a͝Ur|ÃPU8qnB~Nz]~2?J(-A"R: vOFpA?SG(0H4DdB$ Œq?zr*IQ$^Ŝt'Wt5 wɒ2BWP%o0V{-Ry88Kf72 vxh{Gr܆^E*J({{7;{ݛO)p۝_ydPX[mԲ:/K ҮXzS{=/`x Am?L _>wZ}_{ukou0Fq /]|fR" FS΋~ O=+fz( Ki&EGEKɱ )xO;z&U׎^޲,no(&-ZX؛#<շ%>NUU0*|'ݻm:( W̺pi"% " ( cIh1Y$6.&@]GG&w A YrL"⫈~{7vߺ栢lsV|]w$\]d2444r~>/q͹!B(0[S{^07: ʵMLmc5GKuC7Pb@RIbC02J }Zl$- ̚Q|˭D2(5_. IDAT+lyh~ߞ8/I241|w}S+nkqek?I]wσa<Е ٗ*,+N_.H'8 i&9Vѕov Y}U`ACHtT!j *`Ʀ+%&P56t2Iq`ϡ \.]C=A*g>(꼦_9ͼ=[S"3'@)S=y"NKcH`!Λ€i=5Eh*go;S'ՕwF-y } _5 tSrټsNZ)[M1~q"nlofcw{eigz cEܒ*+XƘRjqRER iS!/~[fjJ d^f)3d_;4}? WrZ61HP@3 YAօ% "؛T |$!2DKH~ U>ˮ]u1j;Cϋ\r1gn6=]=!M9nүtFNy88a_iȚe{S菥4$ƱV@"!FGgo!5 dM#ơ3V~_!^UX:){Q/|8qŜSji_9`w!.k"x89'A3 P d~``cԬؾ5Sa?E 1Q~\kSVi("g|kےJO[Q*6uoolp,Z zrko²_؉g t}{,@5tۇ(c5eSc* VNY閣EriJ@d(0eH$q$@4=/}]I0.9!^ ]w-6f&1/)W;3z> N- <_^6xZА{ ͓cZL ~'J> PJ$)VquwvwPi ͝y#*_~iN L`/N{;mєj6@3l_m.d(-$!`$Y i C #qj[>bLMr`$iHϘc ȶ^S|m^}ĺӛJɿd%\9s ¾Jw'^;=)[l^'Z.ZPyɢYUnx:k{ 5˛K*ѠVi`6t;0"05 Z=?l['GW\P56ZVK+two|{$-5G6LO̻;pJ ͽCO|qYȗ7;򌂦WF)',. Mm?M9^U.YZ{HaHN+m \xܶB|Gڧ#k-4L٪_?}O 7]69R\h]ky%^+͔[x#;wO,UI(ܮu%^87B|JQpe̤Ͽ=C\pFI g ^|[d`FO^6lAy(#m?Zm05"k_icT:]4Zn6BW7,Jwҏ=D.^~'*qDE0=؆x, H<1"#)TMM5U""R咏̘2Z?W'.-ZT\r+wy94.X\utWϟY,!_HJ eQ0,јYg'?Σ|kz~?SS'iDkZ{P*PZ/<\ iG)E31M几nӁ]̺⠼:pZi70 !̫ ϫ _7iap958p킪M%}tgN $ gf.^rc; _cjkLK%,{n7Ns!a_y}eS-\\`(Ts24lF(3'Niuw0, O+߷[l~? \i$m.?x鼲 S >, gϬX5[Ni>^3 [pSEcjnB^Zw]UUKyfkoN(|L}5VA~ikOz}"gFNQm}0X.s % ]5Dz`ps~傀 !'(=oSؤhu9eʷk# f̨)|7|wdt'kq5f4G[{Oo9oAv38[T}c8NΟ_n)c:#g)TјSЎhn_# N8g\0zj* 5?_~r&9ꁈ>-|>PX^b( %xf+JҬDX& JC JC^1/hwz``rnGcxbP]>R`>I$R,"_ɐ L/2YRaӨrɓ?}Is_7{Vׇ'[%m_ @D"c1SfCc .?}-۴j VD ^TK"~mW3WLȔB✉NI0%Fڨ8 0O!pEgO$(kO&#{mYCOn4u: 2t~S. ~pܸjKX90(/H]IDl8MKS0ϠZaOg7]wd@b4"b}q9zG~xƴ7XGtNefaSdDDl( =r>ls;Z>l20(s7xYo9lt܊G]Tv@}Y-&v~'ܩE\s]` NX/5UGshWws$\]M<}pAWLs:Wއga>/W\ь3go_-Rlz7 1ynU2 eq!bSbI3lrV¥W[ˇM&E>4딩[:Px,ɤ%_PGz򚂛eo:h)x \7M@29"iZ &l!!կ-)*4y5[0Ah^1lFf cѴ* 5f.Pʟ^Rq8n tZW?u֞&w9=gQ.RBȜø# ,gϙ^έP]N[\뾢@/@Ru_B=wБ{fcqU%OG%Wc;ufn=h^G!~fcڎѝC)ړ3cνOpsÚ9wosFMB8C:gnW_j*ٿp6 q$ V21'~2~V?v͓[Abu"Q-!k[Bo\Ѹ0[䉅KYg$CםXϳf3'ά|;FVm_}`Uϋ7P0x'#`΁_n}Bm:1K3nKdacA}9qw PM6}zFGU}F>7^c`GWOYSpų࿐[F\eDi'[<=^wubE* ˛[ e:I簇5=`>9SlaS\c!t|u$͘W'T:JDSNnmvR[)IRPhsOYQ{ʊI'ӸcSgM,͘\VeW~_,Դ4c_SI*E(#3/c4_zSvYRP%~䡧~SY2ةW?q2@dY+d@; ٛ1Vuԯs6x$wVݳG/m><0 etBЋ~oVU%[o{sTѪ#;>иڴ_Y xpwzs*7Yi%wm{?a$!%,.sݾwǀ@Q!)lL_G@+&ȦT_QI_l}=(PQZve#S,چn݈9"|dkZް/-z♵#CI5h@iM'[~wCpׯw_k;iFWUh,;QD;+}z8*1E5~u?KG!5bBhXS@\P< M+Ϛ[xTΨtD0\("}Ңo1{Jyu$eiwYBѾQ \9`uܪ?o6_]xB>0T'L}P`Cнi fXZ`;?!&yXJW /޺3[: $I"{.q{Tt}̻1]UߍkvW(:#23v8 ~1{֗wc)җDu,)h. }^9PDMܛQ`wB5xRQ@h}z`ze#fW/QWt!v fC̲eHٱa㶻;Y!Ac?Q z0bCMtA@r['-}v]4`Fymg9AoSϱ'|{Uċmceʶ$~Ռ&GgTF# O}":^PeeSKJ-ow*:|O3>;Ytz-5̔kkiJ ;fvs{SfT$kҭiͯe }2g캙RG{N|PhâɒF~񕝃cfmX@mI:aFiC}x?}vov^t80y+Ax{" "  mOn3ӣ2WtZ5 ~].=>+̯gxC/w^*p{O`Kemwj/]a21Wzc" 'ӎ߶ p#_lrluz+7t#ަ^aRXv7 ˲]a$:#'e?JgTS5kDF!ĘH~`f]c"KZb4=2"!.^yVP"[_SJm?4<^yxd1}q 4UBLcLe1:T1Mc"ht T>zYD~mhlMej34׌^_S1ͩJt)K U]↸ts24hڲB3]LSAD! Nd o**h T-WSkD1Ear$k#uLQ2*S5C/ XFɰLQ657fX&QXFk(OZ 嵥Ͷ왢Z2 (V讞Q)twGxqz^OeUm}(*(**?~Ët7=[(DNbOmH5w{x b۱#{{FdNF$4utDg/7> VfmTbN˥PQw%s%n{O~eg>[%'6X4jNl[j@WvXIa`ڌʐ l;G0o(ʀޡ]1uz>ZNi]6grKVu,9!Qw7l^TZ3 ϦS'tle?8igִC:CVzkYR&͘(F}[ C`z$M]xrs)܆1}8oqc)E o6c.jK;'qurUs8qʄ1hWhOw,!0~(DӀ3OO;(] ^8iR[_kYs߃?uͬ| ,rc }g8?HӌW)+! ‹xFĺ`z6. 'S5uf{aG&Ua薁TbD7m&0s]csK2Y_:pvv ٕPuxC6̫-{"c!Ne1[#DxXy9A˾lځLa_|A0hv3= IDATYUDk bC0+ YeL(ؕª=C*O-wtqNu;c)@*.o( E>k'pQBэݿyTU_'Hk(+ ُFaIQ7WuZES`c<)/$2{RDxffzb[; [:biz[ѐʼn;OPOi)ވXfCQdf"rSLG5{c=qj 0%ND^WTq ۞xa{cšQq!ʨ {S@\;"BTU+Z6A=C#r5s V]Scd+WТ E[UIaOTZ[ zLU̚¶v^v"a`yj810(w񔢻ր _`xOX",h(>|y馺2u7m#auQp4erygBT$ nU e϶DEcvǣ[z/O0.߰%6&GAg$dhe*uĉ:vz[[қ4mM({Vww3&1M՟)c%;첧7ټyS\ViIb@ĨzIu痽a~j{RK$Ш@'ڒq+x!>w#.e{!gU $=D aEޅAߍ5# g EdI%v /`<$K6&} jUjahWf93юڗS R&L{.:ġRH[ Շ(- $]9Dv  @= %+nReA:څ|q5;u~]1ᖃ㦕ZUF847Ⱦ- )hL|kؿ!>O՛ɒ5\h86}ۊ|p~=CRw7RFjuO( A3Kf [翾8!C)|y0լdoz8 2 tYUH+qMQ'Jɖͱ?߻sښNZ0GB(BVT9"UKI$[B>)gdtRR^֡gҩ}8yʨ%<1@0hD؈3Ùkpa$:eӟj9a1Ik`.)@AXȸkg]2G_CuyCl% -6wlY&`Naǒd/#p 2|fɍgMjA5M`y[,3uzspFzWW+ E?0ȩ9@"%y˭^i ]Ǵï{;BpB^p(tOJSHd0y\My~# |2"({fgOsO+6 qdziƆBnkZc-;13+4jj$mr9J*>Ab v߻Fӌ'{=\~s޸C,C 3ħ"Mh*莥WdajCLOfM‹1PXcs7/oyrMl:Oɩޡ/_:I3Ziچ`Fud4L;LOlJuy&{-5ŻGRM~sSU؆\֩R8{(ʖ%9g7Ξg}s8G~prS/-^L m!.F$ =^dOD(BS0tb)`z0c_0fc{ҪHZ6d/80ʐsbϸboNx̶aּ!ΐ&H!}r̪ӊaYSA&dl /hS$ [Z90<򕯮/$If1o亟Z|`0E)o1Gpd8x&h RƷ[;o{xk}C)q(@0vuSg| ʜo=|k͸n\ɡg3#wq>sׁH\+IԦc.ޣ S@Ӿ3Ys*7^st(,U.xŵ탉{VK~{1fmƀe9XgEU]|FƮ&cN~YW{BDH#L,J ÒiE@17ضF6 '?# <Ԝj|t\xo8hw@Sg1ZzjۯssĦp+~5ϒ1zsPIۖ.09{Szcŝ=76)y 3ʞn9,0hpE3/%u^6iLA `7 1-˥3u>_T [@n/ߺU4 @S ?Y:m>2z$YŪe&C Wڪj鴿4m@""k)|=@b?a…UD((pQٯ{у?ٖ{~ysLΙ*^!U#pdHy'{Ig BK'gc&6|k{ԙ˓_Iؿb^ٛV=>ᖚJL/+jBn'^&{ X6rZ`w>e(=8Vv}X 3NqUL$e=9"N+Ox/T.2/T=9I1ǕDeMSu"/- w1 @#Bȁħo_f-݂3{VDĈNa(Bl]ߺT!CЉOe,A';_?(tqy?~{IM*e_}h+[HK~<=A {{XԱw+#re>DoyԜ/DQmخl!U7woGvs+"s+"}~WKq# 5*sCL&?`MBb|ݏ}d^Щŧ9_lJhCo88xb9uLo|tjfƮ >{J} Ps4 8 6>Eڝ1Y6ڷD¥HEkFھ7[MB+Mf&.#AM.ZUN?#2H$ 0~%辕-?~)Iv,.[Pi&{+c2=Te% B|áEP0;1U|iMDm<:<6rplgL(fZxiA섑7 C `*"TBWT U $c4]O$?'3&@O}g +V_*Y( Kv@<IpH(n{J37MP/j[B7u^Qŕ <ǞޛӇJ'䓝iv%g_|Әndž{!ēxq}nXrٻkY2m=B<͝?6w} x2lnۻF< }{vLCk3O6oLƝ]c^q^hVcxr+~vsmH+:t6wD7]Sه* n3sqAua"L!Bq=t׿bn;u]hD͙o=nڴ e5 _׽?#? h~}.^ӆ8!Bϵ@b!tsF 9>hŶ' 2$aL* o`0!1<)+7zp6 \fz[f]&mƳ4dHXd|Ӓ. 8KV?tvy0$5q` ᇷw7Ǔ zDtYflE{t>qT/_3išOhЈsѽ}C}(:OfVillGL/(#K2;Y!n)8T]5|Jq3싪rM]ƖmEdp 2֠=B4 y~܊gZMbo=ؽ?f¢O?"'@,>2/Uhܕ:9ݯN. lWͯ9yaK_셝=[60y)#2. ןPs̲呂ؚo62V1VBc2@^_wB݅*j Cr\F_5V7 =,AG2e3PS g)^W[4m6K#nZFMh"0=X #)cL&jNJMi)z&e|l8hL%%ś\~'+'v-}Ղ,i [b/]@\r2n3͒OίZT_g JFCZ N-V,yC#3ۮj3^^ɼi]uz wBwvv.\0Ssǧ6%[" Pթᘲ/}q.%qNLK^3"Px@.=)s2guK%.T FϘ5553쿠g\[z/V7{-[wX$HD;7tYLV7a09`!1<{C#:ߝ7rM{u:O_{pp 7őؽ82!TaMނM. E_x|3z2师oNkÛj^g#Ex>\p!wwݳ1f3ެJ>9&zxLfs4A${-Clu4OMH\~ygvZ#\c:pt /meigi`e8Hl+3_U}1xT`T<'c7gwN=;yNGx8`yLU$)O@~;}+?Y_ܚ 0ZX:I\ /25Lk!D kl^^B!i[;jB%Kjg͝{*14ɏ9[/|32L1ARvq8M$Cl^cѣ-!9]Xoܼbr!Af>C0s\BVb{\\ O'2Ү_. IDATI{ +CA.EPjmZ^4#SdT"d/)kZ4lhe9CEmzgbmV# Gͻ*ަa,'9p"6dDGhX22)Riv*bzIZoylN\Ui9xgD.B iUm=[{Z]́aYzReWusfl25D!OBDz)d49Llޞp~`,FczAa$%`Mfk#OqE&5l1*"mAADcx21wu uؙSfgLyh{uG΁-ڹcabTˮF6ܪInT. `G3ř*U\IiqѸ4{.ŁhV?wGD'Ύ`r=]ƷMT- rZ,92 1ArxGy!ݧal,EOl ҉Ó2f&Quq[uӈo(ԁgt5 aoUF{rK9%$,ֲGTSX͘UUU=5uhuej0pg j~hjzc$y@`Xh$tp~$/RLޑNˋ Fef_F Wf)㨎˖kI!GyJ!Y3ߦa)]izhȡd.M:g|F!e(ԭ˦h Dh(Ds" 02(if3"T9U[A6J> 9UJ8*FziJ"QjޙG4t 2%Ik,;sy gLiFׂΕ7'ƾDe3V+S%u),&et1>)nau&%km+FN7 ']i)C`VgU7v fag "$06qd`HYEIAa޽{AR"%ܒ4ĕlpADRsW5$0YqDB1Ca9Bj,*e/*/@NQwtֱ};vwPUJqC`$n݃_`:${Lr)1s43xx:Cm\ԫhJ1qDB.Y*ܖ`A"^ vzv(#8~3m/98Z^hiXKChbg[^C.`!Kz∈AJ/w:Z1:O)`ʛmJa,l1<\Kf-cZ-!s-`R~0U`֙g9!>|< !ErȜX 13,3@w q@EqEwmL6O%&4]6xqeĂ@a#ˆU7Do鍺lҸI$t{PYR}=C EM0T u'N,/) 6̮XB=~綱,8lq󮊷>6sz7E n}72LYւ g'p|nF..Ի|8-oxBY@I ?ɳfa(iЍN)F֚5>J@bԳ.K%*&qv6𢋧Ppdz)"F @(,,>FW~4ӛQ,uB f~]1z{tx tt׋ 9.P.ȓnqigc2ДdKZFQe2#ƭ-i`&Vh i&0Q}(4m1cDaI^CA$L;i[cfИu'LR|]k/ʣ0<ڢ1rwX: z[3f3QbP&Ϲ~v{at?!bKP. !/Q$4$ xndMu#@UN$ 7G۸4MH˨C!^im)G@Da,+Js>B<#ĭ[޴F&"EE#Kw5BBG#*0{OGAPNJt_[r"92}Fpx8sxw,J%HSP!c  SA,H gT 'js@V=DA_ZpE$G#$, pN"X83izZJW!b6`qX)ꏩS#1!ikdhLirYS3MwnRv 3gٲ0rO <3ijg<$dV;1rjA`$g{w4%d~xaq 1(͜^ZU$t "EEeÝ}@w"1"2zfgLIB&I'ִewĭ\|V] _@7|" YBQUH GA0@E!]W8 iw "/XӪ@Q$Z" }Oq:G2;1s\LO :,=,I` 48>" cp|߶5?5PWWP^Q(VX),hk3{Xp08L)_ LfWm&Z*!Fl&Ct}A%`Κr霻p20ԁ"銕ȴ{KD!nVc q>B޹mKB,6Ņz/$DŽ#hb,F'M*-+O0֧LI8$%@NB8GA@hbFeꅻ =?E#H9=/v-/ tD2LwUQb,֟²0ǂS/|ͽQzuͳ 3 1_Af`AȾ+C-gCk{4AG  rn׺HdrpB *WqC#ަ$?7TjQɴ>XdQR&fI#ĭE~7|{GTa~!8n9oM:[!E`uVG/.,_z Pt Tc'tP T P|<,0AWMk+CM:!2I9CK.lٔ}[:U J<ɩhƘg/ޱ`ۡnk_*xiH!BKk[FK@ f~]+s{w̒^[^ܼdQ} q+ gdg1KO]s ^Ga]} }#'G5c65\-vxzo[*g9}iӦ6Tx@捄PEV< ǗY0:utH:l7c8xmSdNnͿYR! BV_.hc@~8Ht1Ƒ1 %߾yO7s\4] !c`|NJLQ'?ݐ_%Ue ]zmh<^VZ$fj6@p4jDn#Lw#HDqYE.ȑf,D ^ Xy$Δ̜q v/bxeq@x7Uطij f[#D7-٪FtpQqT6'so6:9"{pf>9]7dM;8%$i#ވL;BMqQl+3)WC7K3lŀU{b%-ރ҈O?vG`zԳ;`… ӱG|qu8 N:{Aߵ%>B7ӎՠ_  PTAf0QJъ!{隦?ɒyӭO$0@>41{^Rph%IQDEE}M_ .c_#&GL%uI 4"~켉s`ҧSRmX{n=Shk)D4Y;3%^exb3cJ>BcsanMqg S,#čXO[,yL*yD!>nU6`}8XlsGg`rKL@tm;. !02@R@ $M%?K 4ڡӹiW7}0g;9: ̯+S^޳ U#c JBHŔH;ϫYb?2X@݃/0*%xL `(4g5!0`‹k/z@(ywty׶Kmf)§mVu (yx#}-m{đuO8^SbDn$x!u<4n&ßȣsrDgkq!nd#كF%ډ!n A0\ O+yu6 :fr/1 `@PIű`׷yXH "Qv B A 30!8}+ҬDáYPXSUMrqƘ$ Օ|gϝGJ9DV-(\FÏ4kV-O=s%Bƒ#vM־<& lqUd+Jő᱾|ݜK_N= Sq qrH{f̷!gXJ`>B wpGwDef@pV}"'n0t`޶y3".H2 @@BF50 KLӵK}~WON{-=% /(hzi4 r U,I()DO?ؚo^T=EN$`P* {vUQ(YxgSM~`˝8H'±17*juWE]48߻tŠ0.I1Qr a^h4Ꟗ]96wwgVX7m-r&-x 6dV;1rjA`$~803j@5޸,/,'8yjqm5=\׼LN*d' 2&? WAcRXgN|uADja}YcYUDiϞ=}4ݱ5}Syyy u%DĔXXK^~o~{1arS3wN?z[>xs;v(bqlk4wPA-A|O|t @9I_>?͇.i)E%o\Lux3g[lB7,, 83mv(ܰnx 4Z%$a3ϲsgb3ij$-ރ҈Ϻ w6[gUI zǦL*+-0N!\80<68gkkPI A`!l$.~_A.q.ʠx@" E)7|%Vo^5K\rY?@Ɓ$"LPa\Wߟ y{g\nXtm5"@M%}dIa!b ,Bt]蹗yZS<ǥ:y{|fZ)~A=7')E[+981̜]K$ӏ[Fr_d `o.YZ[u)\5oEUcebUZV9I`AR$I޹u91ɕ轧w/q78Z`BޤK%-q[ j9ߖ:WndQ:KmבE@wPwƇgHȏLٶ[+/TMUH ]vaMa A(: ujN\ A%@  IDAT#g<cS  CEB@'GA#DVXl}(/p~wփ-a՚I%gS,kBu8ß6%OY:Z?啅*'_C!z8,wUę$D\= U>B|ۭ#}x-鸊|#>BG9:وr`BnvD'`oKg}#%ReeQYIp%W[ 1C!*@ J tc qU,9hǿ3ˇeIBDDE@B '] Xmtœ.8Q:Ь-#dùg]?r/6yN g'@0׿ͯ/sP8Cc~B@ ?2!QMD90"!(q!Ѿ}D 'N]ڮܺsm{?W/g =d&d79( A:ex׎%A}՟s.}͛H裚&@)!Ikz~l u/.+"]_h]ڴM =֊8wbԪI|> B<F?N)BJ >B<խ|EE#KwJ`\(})Q - H$ ʖ ִkOZCuH)rt]`Hb $ bSw&G'[{͵es's@=lu{O&(h2J9A$oM<.>--k.U{~uʔ"xSM1.sk_ןxBtDTL1_[Eca]?o@5],/|hSCLW/ttZX'OsόU+}G9o}UU#VY5V&Veә>B >BGsxҥ݇zwCLL #C_v_4t%^{'>~}'>Z),"FPa(6lim1s&f~"$`Lx୷(HERb,^5t$Bl84az,ZR&_z=-w ˏ1.` kڰ$큽o髯cXaat"XsX\S?5mw*D!G¡G,[*9\(}ٓ\O\XKn}O 6c&3CBh%bҊy @vw%HGf{p9RA#Is3̮LfLE@%elq>Bܪ\wpGwDef'B.Gd{Xo7p WUu.{mWEHC1OuFcWוIbwE~'Va y7OuCw;$ÚT6#m/}yl޸nTTuMEKJ:o%b?wfU} UѸCy }nmʯ`K"NW^3o1&U?*@}w7B~Δ6o#]4\DӚgkF cvm-6[kU9JLV2eME;7)dr>BG;ir.Ӕ0nz׌G{'FN:,G#A +]/bPT]e5kVMJ@BD1:w&+ԥWE|CWqy!!R}_t<;i(\0$F)ܳ*}ё< ĩy/*yޝ;z+j**k^7{uh"\^{=hO./^Y5WNZTSRԅH0GXϮ..Kc()'AV]PSE$^5)jo]awPiq+15>Bm,>BI>B*ur|Z.fY#쮉[~cۺRQQ^H: )}=)'V^2SG@FYa{7t JUt#ϕb앷:'Nڇ+_ BG , dڴk{8b₩SoۺY; *$\N}zٓ{|cE"zI\qb ϝFj Æ~~ "i= bWZR]yʩ'Mx@ 1 _(Om0kޘ!! 2PȋcwY|[cY=9زEƴI1u00Z0A R9 S%Z%5sr)J&՗O\tG>ZJQE owuk.) Cg HM#K>TUaDҽ+)s8{g_h&~O`LAĞJ㚠䁙nk,-4ރ좹2#-ePuI# 9UJ8*,J$l:i}}Uݹ#&a:@)TY9]+ߟt&lBpC򷿶]|qY(~'p7 閛-4$B=[o OnJ]DUC8*u㈠ͱʪ<{4lYՋ/w8(TeZq|Β]Q7sA~%@1y3 h1Ā "Mk&CtU2Ά oO}n'?7KzF D(q;{̼Q(RD[ˁZY1Y+5GܤDq!ɹNS¸]3EwO9 t!B1$z'LX亁 7~c9g/QyPyUp&lhSV62}B䌳 h+`R;o|mFzG&5 Vexopk_}=6ئN`:A' (mv'^9%EaΥ@IƐcH27wn1_?I8cԯDLsl(l (qDf᰻>BJ >B<ճ|EE#KwJ`\(}x&kqg#V{n)w./նJaloKI_^ RIR1mnfTq|rtBEʓ:}cO\‰B§6/So[/7}G7_vI6,3tu0 .GZxrb%=>կY6//:碉 cӍD[J d2йT 8"/2Senюafdn^`z2+ӦOYz!nVNe4#͑`}8Xl #큀`8aVh/odh\B 9@Rcc $BSk􅩘=Z.)|΃xqmAmUWVDª{ԻºrԇH i kf 7ʃi@ 羚>%oI{#5(o`oɮn2KJYƈ@'t1هZ:EyUSV?=F;FtU9N N $#s}u?*匷EȖTr\Pq'Y$ǕG<4=TG;Dh#3[]FA$Sf5g^Oo yp&4.3E`0< RP@Tc@ ]YZi6BܪQ!ɞaqMdK2|wpGwDef&qg J>"9N9eG}/H4yL?ȥ 4*/-V]tiű3ó&OO[Poux„r&G9y@bu􇻏=!@y׭źL-1Bd:tQB(uU^z1 X,C,S=X?XND n #ЙX(,UF)'WŇS'N^/xqɮ1A=tg"d%#\T!nsM.K#}&"8M wxI>wbԪI|>'q BU]|2"="˓ok_dr 8ޙp}[OF"B{Įh;WKKm?TUEwJBEI$-cd̛M^V~ͽ{2w9saܚ VGt|Ǫ9Y:PIɵ-$ӲI ~f_Vņ^2I]Y1eLtC%7Gu7(G ¤YܳjZ8|N.^-doj<7pu#u|,T;Q_ǽ$rsH'Ar杉h|[vZ="AI O} Nc$ :ƳQm&2F Z@RieӼ[Y-(IP ANrA!'>xuiIБ2Fh&rqCCPtCqA7\qF Fqgȁd Bg* q!(]2j,XnN=;^9^a%׼{#+Ke48>?ĦF!ujiᑒr$ud dعK,%>>thi{KIEȀԑZ'KTw9K9!QLRH"KHR#򭓪;C?:(͕`}=䄜]|m*בA~d@P.Knpe-7mTy {n\ry5ׂS 9e[eNVڦҷ IDATޟYf~ז>dXR `<ǺB-b>2 `3$S ʇe$ Z%$BD$O,=؞.E\uqfwynnƚ̕@Cv6Rd:PP;/RG!NR r!q$d:bdAz çz9A'!.L1lD Ixk RS. OD_;S3k\g>71$hH5OHRGo'tЏ">̜Y¤)J?giI0f:&z8WFS虊/ɋBM4AD`sX0)õÇc|iʢ_/{>~ϼrJ)9=qO&2Bc8K16$zA}k[ziYW7/VpCqsΑ3;O)Β_eSC\$ X\yɂp|{=QRt)L[[VR#&#TIKzn۝Q%bqA=F!On<#㹪/3C? wKUಉfzgVBkvj>[XAot &z܄";wnQOO缼<*d*Ѵi-V[d5ԬRԟE M֑$ T*n*s3"If^tJEL zOΝ[fjTի.P[Z5w qRN>FJEډˋ/HQē?eE~ ͓l {n"ʝ:so%VZ=99?I۶m)SfQCkupp~8s植L&]xZSID3ҥrssJwҖ/_4th[ۏ o:|Ur(.'=`BR,9/yQqZ<ޫpsS*ӧ6+W.8; 84cǑk?xrt鼜lkkk9r^z>F$)'KTה2[edev˫]߾lYO]1)l {yzQ4/'= z {n"ʑ*yﭭ ͈Wi/Vm>7x<8ť7Cnjj IQI}!*IddJEt֚: ٤R3gNϟTDfSCddPAUNz˗ϳO>tu1b7C&Qaa7Ir׭[Y_AxONNJ?f[}hlYX4 xޝ?Xݽ22ҩ?urR~YSUrv=zuNNʪU yR~~M̙^gŽ{CVS 2i҈|hHܹsKz =`W^^m0|x?$srr5j$Ir%Ew]#G֭י3g̘@޹v~WWIF,\8fMONkWcQH+ގUZlT*Ϟ=!RM OB,A°l]Cii) VSГ'  cؠ12E,//O'OըOӦ-Ν;9`@7q3YHz.F?MMM;u.*]([)cpn"Vʅ:E--}v&Y^y!D:Ly0lh'.VգF}mmmϟW+W}z@fr)gԩ'.icc FӨO`oG^>|SEZݡCJ]{VM&_vɫ@DDUǏk?^׶m)UK aO_{޿;wn}.of/Xlk׽|GO?ڵWN(\^Í׼||4׷emvp:>4ի\]ݴZom>u3~GBxUSS5j>.:"N9~5l]Caa ¸ST\.?qڵ[M@x T77&Rx=yرyӸ۷oݕHCVV֗/ϏС+3)]([)cA(J.kӒD\u^[.+/>S4&fav~پ2AƩaff>ew?8tT u>ԓ>}6m{VT&񶃺R3MMUxz:4ریsJr+Uիuvm&&&0x3gKM>PjuP/x:hڧ~߾_\tVhL`Χ;tbkkwnt:[z ++ku HRi'%''mJ8^U)*Tps'L$7{\""^Kj `xBϞ}ESwb UUZ5WhӦYTTV*ԅSDܸU"] mڴfÇQaa7J^$I755ڵnݺ~UٳHb>PtlD.%e9r&:EР=Uk%蹹Qq7ߛ#sIRInОQl]%H5n *ULdzm\֢s׺̥J$uAe-oTX֭G:uҥsaa7֯_uVx4"IRr9CE*"5 J?.iF֩Sƍ+I ho߹mwoB.]:ۯ__~9bF mC! ʄ㸪:/>,M`^$h(OdxP/'gօSԥlH e\%+3A(erݕ-&qLtYu鰰 kמ ̗3 VʘD.%.O3z_o쁡{{~+׎~Rz#HWY;:$LRU Nz #[C{~FP0%&&h4$Bȟ$7>~eccZ??|xܠwz;6N+ ɿ(mEGIA}xzL۱m[ȈsNQ79Ot#Gԟ $ 9~I~?uOi!Ř2N &q\U)RR:99heD SHAf ѧOGEFk4ϟRƝBդb"%cկH& ))HnRϬHժU.\8M$ItRΝ0#R$B"=#7o^k֏Zi>S%҅-l)~$O;vyzC}75DU W؛ܗ}v};y<}ww57ϒ0ϯkz ߿۶m5)/iӾWW7 gϞjUĄm755KO f^daaɼ1211ݱccv확= K677EfϛՐ]tvv}f  S[G!CFY"0p~\\Lbb¹s'۴iԳg߅ rb d2Ħnҧԋ9T.wҹ+W.@aLI!Ř2TըގcI,U //ɓ؎heD SHAfOIy|C׷1F'%iԨƝBդ kϼPϕ&L.I~B+"]֭[U.]Z]zn 6>{DaR$B"=nff޵kO:ի(z^.3EJʖ1nAS6g)S>&Wرt<---57HNNxiR)QUnܸ/n9{Pٻt<5dMȰAbƝBդb6֭,Y5`"J( K2ssÇu#**20`4innpN=y۰/{ 051!;uTk@+,Voۀcͪ' eAN:Tf`͓#3.t.AīW/""6m ;wn9r§e6A969s|Ӯ]2)(j?|Zgib՚[g =?]}1 BA\";|=׬E͟:QFF&d.".;z$:uqVsL)UT 9ܬY֫\co!!_M)۶m055v(W&??ۻɚ5[ʤRYRH/#i׷V]zPiMھ,Vd(% P?Jե=boJYs  ‹OW~3`^ҭwlogzS(2%{g"W|N<1Ͳronvh6! R)4_{,M  R ڛ˧C]nݹYgE9M |Ju1r{/GeU4  HqQz>& bI8]$>CAӣ,'zd  R#3AA  "AA  TrBA ZN  RA AAD*FVz  ||]AA>CA ZN  RA AAD*h9! H-'AA  "AA  TrBA[f IDAT ZN  RA AAD*F~AA G#E/ZN H V >EJOAA  TrBA ZN  RA AAD*h9! H-'AA  "AA  TrBA ZN  RA AAD*d9mڴ0ŋ)ŋ]tttF+Vj%H~~i[Z]%Gڵ+5G||߿LJ>,U?VZ-\~1n m\zUVh6!E'"""++KD@R,~Z,XYY1y{ )rUV_.EyRݻsi߾}U*K-f̘qC::: ?pΝ֫)miӦիWdɒ/^pϟ?tHqq2I) | p%N QJyC i߾333ϟG=ƍ;vlРǵZm}vءRKLSff211)l(J}ʒ/.w^5|||\̼[nI *ٳ)SԪUDTzzzΚ5z 6ܳgo]nܸ_1cFڵMMMJXI۷oy߿P]j#G֭kmm-ɬ4h0aѝeX ^Bl֬ի黸"A9%Ӯ];nlǎ #mrˉ'~"/Q:::8rss(˖- 4"mf^ E }Bӵiӆ˗/datf͚iFv͚5:Nrf͚ڲʽx"%f^gJ罽5o޼(jd֩Sg$I]2dk֬M֭[r*~ߧ>ܼys򞥥%IwV(nnn.Z /_Nوm۶ԩBX`AwիWMMM ´S2cJ._ѣKUVrٳ-Z`6/\`gg-!22Yʦlҍx~xs1KΝk׮M3FVǍ,jnnq㸭 M43g3חYXٸqe˖Q2 .l߾=MZj-#Ƶmj={2{u OHHX|y֭mmme2ƍ߾}K BO0N*a\\ܴi<<TfIߧ޽{S_-V2ǖ})}QժU!C0gs$>|*NKh9$LJΜgf\.cpa*͛7̜'L@'tqq ;;(f 311ٳgdd$S[[[V[DXv-Wzz:ۭ[7֭c-.w[8y򤐶,i"o^Snì,{{FDkذ!Qbeu: 0e-ZDV:B0o<:UQzPm۶3gΌ?hËBDU{۽{7վ}{f)bۖ&R,'WD߾}ŋdf`hh;|РA!!!SN .\7o]А!C͛7o޼K*UPm۶͛7<~DJIII=zc7nL^x׬YôÝFMAu]b4;mRپ}ZjY[[3_B˩-'${.99Y$E$IʅfƌXNDzXa$mll-'$SSSYK.c333Y;, .0W^Lw^f#(a. XZZvԩbŊ޽Y-"%F\T>x:u8///V2-'ݸq7o>|ˊ.; [hLkiiimm͛GuԉU۷RY&4BŠAׯ3Nh3(=.]yʕ̄*t,k c9$u fPHJɓ'(=RĶ-M ZNo߾e*P(X^ש111...T]P 3eԨQtn}ڿ,͍955]δ3߹s'uV*tS~tT׮]h:IJ'66ՔJٳgSSSYcWƯ^ϝ;p…BbccY;j0u\!Xrexx8}HPt{{aÆ1C6oLYYYbY%jC>}t`rrٳC333fMҿϞ=ˌe7++ٳӧO[.>>>E,KZp}H6"qQBBŒ3!"ņ ;}VR1333Y4H.]+V~ԚӰaøJڶ`yq6o!@HH,[u86lֵj˖-̝XX _"0h PT;esԩSX3gΤ;991=Xݡ%e2Uƺڱ^G( ;ё5>Z?ΦU*Qkժ:X|aujcaaѥK#T ju}BTRR4}tBۖJi)j½{޿?ywݭ[|'TRSS@V{e5S ,h֬7lжmVEٳ <<xѣG=:gΜ3f̚5KEP,YdѢEaaa;vq۷߿/%-zd?yDb0_d@*U/-'#x ,v&w)!Heʔ)L)))*T ju}e˖EQOMƺq|1077ݻ"/BWmXe1iڴiӦMcbb6o|ܹׯK5P.ARBmC-{`Vt x/|hCڙJ%Y?&~K.ݺuiӦ$Inܸ8_Y)%ݶINSTT0[}֭TW-D"'ONOO_v-,V?ׯ_$={X}k 5kf0-K)HoXܕK#JIZNRK ~!"~ wFO!!!&LذaAl߾=955u߾}}پ}mOfBCC֍~ς-ə3g-ZtU) ]vvvv̧QQ.Bۆ-xamû%Pvfբ$N?[.&&9QmXD,ӧB&ԩSݩ.Eq)gt:`LGI `2:nΜ9tppꫯ˳۶<0}tz9s0effΝ;8sΥ_G]v#G>x`ooozW^}ڵk[l)i6-[~:[rbI y^^s޽{j:ݻw=:z(ulMFgԩt>wN1F3{lf}=_F[NF\ƏϼWKLLdO,w69b~8B.^*bgg7|pfHbbb۵kUTa%&&< ȑ#:n֭\1$O~xDY̍Lnݺ$ztdk#""rєf Q\>;n111[nMKK*zKvfm^N222X!ajjڜiϞ=&L0Eor\. ;w.[nѣGHHș3g֯_S:uf͚wU#GlժСC:uB Ӈ [~}V(6xΝ;oڴ!!!m_~\Ɔi1bРA-Z8<}i4VZ]6>>ĉLgC?,k$q7oO?Q۵k7hР-[8qy|_blw$I<*Ud0˳~b؁6Yf}iiik Ǚ3g ۉgliHCz*S@͟?\]] -iX[ˬYF\ 5Mݳ5dddxΡX$jDDJbnZ\|9+ 0nZmhtZ˪URy1%Y.raǝH5H`` RqXmꚑAbSiӆRd$$$$mi"eq&>3gN-)k„ 7nܠ靻cllܹs4iB`ii٤I~G6Ů]6lH}ϧe˖J666mڴ9sf&M(#++֭[oڴ7njSreLT*ƌsݔYfۛt޽{ܯP4jԈWC58Ǐ֭ujooߣGgϲ:Mi!NkX]bjj(*??^,7;w0':t߸qCàK.M-Z$ql.LoM:J2ߘh߾H>,HP?رcW\ٺuk֭S͛byGΚ5K\gbѬF0`:DmH*UT*9?b2 ˽3:||j5]^6h4"t:֗fΜms%I5k-XwIIIc.\… Ν[p!h%ot;GGGthܸϟ??j(฽;::na} @\^G޶Ia-')>UiҤI-[}+̬aÆZZrPZvŊ3L&kٲe6m ϟϛ7&_|ݯ_?VL&355e}O Ll2:رcYedd3FĀpwwg^N>ݫW/2M6}ߋ,q>}*n˄ ^||ZrܹsLeC۵kK z]N\.o߾}```jj?ˮBѮ] \һwobi4Cմm۶}e~`ey߈_?{#Fː!CwEq}޹s'MŊaPK K!--nTDD)@ )r";ŅPSrkNʎm'Nd~ےKjjΝ;O8q]jxwuС3VXqՔf͚}׽{&Yj5hZj+gϞ|INNRJ~~gTTcccwy…$ݷo/Ꭰ Gܺ(e$11qŊǏ777QF6mƎK|7ϟtZZ`Eׯ_/^/_T*^^^C8q"޽{Vr6ܳgKU,tr1Mb333/fW۷[]xɣ p£G]\\Zh1f̘Ν;l,ٛ7o޷o߽{222,--ԩ?qķo߲( NJ̞={ҥСC K\HMCxxQ[!H >> ؿ?)yYN ȿrH-AAS-'AA  "!~ IDATAA  TrBA ZN  RA AAD*h9! H-'AA  "AA  TrBA ZN 'OzzA{왔$17|g֭%^DpB0v2"c՛4i2p~͛7Y~JjذVغuH*ݽm۶ӦM|2IFKq :u*}Pn2A7ko_ڵKӽx"--{,;vk׮I&nnntE:u>|Etۻ(ZA899lٲv׮]V:o޼A5oVb*''K8]v111.]ڴiBh֬A"hڱcǦgdd4lÃ+cccӰaFݼy,,,.\ؿ.]TV-""˗wݻwohhhňk4T\|TTTJCq" H۷i¸mڴiӦͳg϶lR\ZAxyyyyyAviȑMխ[=tЩS'$${UD'N<qFQ _ݿ l޼i__\|ɓo.)|Z |5ZP*cƌa}<ĵ*QX]|+͛Çc޼yt`VVЛ}y)r*}CA)3꫼ :d~~~ .u={lggE:u֭[O>kÆ 7ni<<qww_dIÆ ;;;ggVZ1p4MLMM5jW\i׮]۷wvv߿?FM6ӓӧϴiӦMֺuk^}~7Jnݺ_~Ύ |ݻEjQBSfS׮]׭[ףGEQOΞ=?hUXXX0 lFMG]t`)));wڵ+_:u6lP*MYNM6S[B.{zz6i҄U| EÆ 5kF-%$$ܹs3gԩSddd~޽{֭677oРA͚5MLLxjժհazUP!'''!!!,,,88xذa+W^dFa6YYYI < H &Ї!!!ԏFթSI/)ŽzjVVVt*օh?>x_/.lٲiiUL"B~wqQjӇ|;OK.yxxxzzhbO>d7 pkѫ4)=˩G7oތ:߽{ѣGnݢ`… iii'Olܸ1{ܹ&Ml޼YׯǙ3g_[ݻwcbb]\\X7n|qDDDdddRRҝ;wNs?GOfpС} R;99QCCC=zr N֖-[7ȸۗ;FC`Y\,(??RR HMMZ-Q)<$;::ۗ 133c 9sĥqƇ 8~_|yܹcǖ3J2wuuiao>%Jk׮ׯ_g>=z!\.%MʕY-ZѨQ'O8p#' ȧ sFj`NR?3999''gΝÇ) MTBf-T=yiӣk/^(2$I?~"3;"~hj_f DlMËw͇`hbbcǎu>{4iR&M|||߽{7kg[.^D\'Oٳ~زej~P۷owrr7n\ ˪ u1?BSz+o>A}777 ڵkSM6-AJGG~Qϧ޽{7nܸ &888믔Bpp?sBCCwM^[)JA5jPh">I {"E;@EA>۽T; D SPDԀ@wۗFD|ٝݕ '{{{2>|"}YGsoSSSGGGcFFǏǏ?ƍoq}}}XX؂ {yy}-b8' ORUU,--|'J fmmO|v"VnnnssW~/ېĉ<^BIII]O?X_Vr$z-|21q޽ۇ_qڴi8wOC=pODNJJJDeprww'秥}gLBΟ4iҗjzvqrbDgx~}555@ _/ۉ=o&J&&&Ι3ں9)3<Uߩ|xn:~x###B9s&1##g;^{G$A~a ܹZfͳg϶oN^zImq< GR틧G)--ZMMM|xP>}&Og3SmmmrrrXXݿ###Ќ3N< >}4 ߸0;;[ihh8bĈW^"۷k|||X,V{{OMMMʒͯ\"l{FFFvvvvv6 $Н;wa؇>|cbֹ萐QWW۷o㱔O?ŲΞ=K~+((2d?QQѳgSPB˖-[jycccii)sihh}.$44.GSԖׯ_g;@F@`7f̘gK&&&< (r"ϒ#O%dggSԕ+W e֭#7o޼qqqg8/$$wV=@6" |~8ݻw*3KҥK={|144$|ŮuI|ǎۿn"#G h4NZ%<9L0AUUUYYyƌuuu_!g"Oz?|b0<ӒĬ=AEE$СCcǪ LJA%lO>k}6UA~=9j@.믿/ՉSOGGҥKɯ~:q?^ҥK?LKK͍'S(}j,USS#rNxxxGG'U/"?jZ{{{ttȑ#gM˗JBBBL%% &9sνwwqܜ''90beee_~7n9Z!I.h"}See/DHOO]~It= xZZZZ[[[__UCC|ii}QvvvEEJJJD ðl2<Ο?`0nܸ!))`Y!=<,,Ɔ~ rDN%֭S_,z///MMMWWl9̘1ח^nsȑϟٳz DN%#G__m"'YYٞjz!III^= Rx^NUq8b555UL&dZYYv[7@/?~lmm=dȐ?՝>}:M?&۷(_%''{xx뫫ܹF,XS{.Ouxݼyaô `#G̙3GUU!|iz6mڄdz(!!bmݺujjj&&&/Sm @ׯ9Ç&MJOOp8'Nptt촶 <Ν>|ϟ?xܹm۶]xQ]]޽{{ 044\xq`` ӦM;y򤾾>SQQ|R.bnݺ"bŊ/_9ׯ_߾}{ZZZBB޽{Jooѵy{{_1љ9sO?$p/^lڴ!?:u _ݻ=)))33s̘1flΜ9 ٸqt@@#Gꠠ [z90mmm"mggrJ*׿{.44T /88H[[[ )))<i="##Ν`m۶l rh4vvv&RĢGS哣" BD~{{{,5kq‚ojjyA>}ijji0VVVD:''O|𡡡Ojs9O| rЋJe81 _l rJ^^^cc/#!!߅GIHO9EwYZZ~jmo\QQ2JJJ111ӧOǛ700 hcǎ}1c>]@/_xf͚%N i٣GF577_~]`ܕ+W&&&R(h4===-+A|2<à !r4&kk7nÇIJJfW_fMbb"^OJJJII ~mNRRRII_~ "_CѴEoDNz33iӦ1x1c9 ]__,--mggwĉPܥK̙Fѣٳg'%%ݻpB8 ŋ?}ؘX~w]_bBdzxxrssΝ{ׯϝ;'O̚5b.\ 򫫫.\gϞ$Г(ćD Ib"iӦϻnSN~Fyaa!{](_>,,`zZ<~˗Ns@J%?jFXMNwoaBȧRrrrę)S}vœȹy󦺺A7tԩSϟohhH߾}rrr:zh7t| rc.\pIrNLLLLLիw `0O<|'OvJP6mڴQFu{3|-b;} q . rDN @\9[#'[[ۛ7o"eee+++$!!ёeffƳ(!!ֶGzŃ=W'IIIɬSRR"{{{nta$EӅV?2)b _Dw_cc#BWRRP( JZXXܺuðZZZC9" ig޽{Po ~cXeeeׯ7o޳gψE?~TRRz1yǏXꀀ7&&&v{uz r1cǏ RUU8pOܹ_~AHIIM<!aÆi¥Kdddl6BZZZ666,GGm۶)))=x\C[[ے%K m̘333))9;;s8f ֯_ݻ;vk `777:neeoBv@pSS(++aɒ%rrr 277^bm.z6w8u555eecǎI&IC#ߊEԓ-GF~E"F6=o~k*::Z. <G/.(( IDAT>q+))iiilڴ0Bڵk^^^ׯ__EUU}bw"[n)((ddd9SNӷoߞ5kVffU-[i6lw^YYYYYى' vF''Ç%TUUkkkׯ_}vr%7oLMM}}JJʩS̙sرA͝;!$0CeddrrrtuuY,ֆ |}}EE^2}jWWW.+ps^~ìp|]GtXNN|ĉY,ڪUbccr?"6\ l_pB%QQQ"""<<<8}'lIa,r=FF~#[GF}ğ#pK!ļ9">ɔ)Srׯ_7o׭[Z[['%%edd#3'Ld2 ***޽$//777$,,l޼yvvv:::iii\.WAA8'!##zj*:qď?f sss޽`0ۗbf 앲2VCCȋ||||}}nNhh%K i&|]'Yt^SS?iElt8u+WkhhٳСC,X妦oߊwb֊? a##Wtd9B{"p RiQ<qF}$Zϟ?WSS4h%BhJJJ .7nܧ6G;B򎎎EEE4O>ReeeYYٚ*TR^^>h A#)?ĺbhKl!%%U[[i7}||jkk0 ruu kmm[r%BS"/MHH jhhصkqww?|ZZZMMuu͛ N;LX-zEDD<|ék>N`{D?4|E555D "p{Ÿ#pKqLdc 5ӧa =<sNl6իWcƌ|c***-ZT[[vQы/ȷ|p:~"r3f%%%{/ߘQFyyyh-[(sNiin[O t?8qu3pB:oDN @\9 "`kk˓YWW'kK P ,3331{+gϊ~ Ç]V̥޲111XZZv+322ڽ{wo Lȑ#?iQFˋ4""Bz Vc rt `[2qҜR"zZ\\0o/3 LE{ 7wΩmɒ% CQQq۶mxfLL7צ=zƍ8q"xyyu ќρΝ;PHHɓB/_$NNゃ,XFӭRSS㯓޽۱cﯦ|1!VGTm6%%%==9;;s8f ֯_K.aÆ 7oF~Bk߿!d2sssMLLdee>ggg'!!annB=ZWW0---ccc999]`R3>~8dȐ$bDG-7#ǜLin]{3-77͛߿OII9uTVVVyy9s;VSS3h Pl6ӧO3o߾*DsSN%^Z<33sժU˖-!!!ӧOvuurmXl5)7J`ITUUkkkׯ_}v򢘘/##b6l+]2eeeNRQQiooooo?uԺulvzzDVV֙3gN<̜5kݻh'''W\\\QQf=J^u떂BFF2#7:6G& []W=M-ypˏ":8{B"n#Pbn֟}:w_|j֜~2g_i#2jB/3cGsF$Ub::"rNZt0zH󝺢MSʹ>zSy.Z%:yؤ̶͓?z+/Sb^6e`lƬpzZ2a6N:|o:8\P~Y]u^J{7F_];"_z@}<== ===kkk===### ҵ!--- ږO>-**6mtccci4Zeee>}ʲ555U0 6F344&XOڪ D7J`Ii4Bx۹K>ǣty5558qLI𪒓FFFW***%''[YY bݤ$|]bi:6[¿E(i.Q(_mwxjQK $:`QL`ʍΝ{Q@@@|||ss3BS&9}tXRR㛛+++AO!RRR6mڄ'Kl䥝o͉'DO:&(߯t BBA<4[$/B?`K]PIu2C!wOpXے{/Nj(C`'zZS[[+%%akxxx\\\kkʕ+ݧO~֭q!Y0u;w899!BCC?ظyfYYY99좢z|2]v/NnOMMM0|Ήg]N ͛7KKaN?(RYF,s Ԟ)FV >jOR1 aFgsB[\Wڦ2BꉶF:V<$=7̂}ӊ+dddLMM-ZԧOE1`맨_gBĉ^^^EEEΝCYZZΞ=l;;;_~]EEΝ;aaaT:M?~M&NhbbOQKv]JSSӪUBt:]EE!t]]]UUB̌B\t_^^?QSSC-]ץ,g]b8[$ .LNN(zqӫuXk-=]R7n&+?QT͐wqww%GUUYx zWOMyG|Պ.AsbxiyeuU 䯧|]?(H611in >ٹkGDD\~ ~Q&E 1,(I۽; Kw5Q;c*(@660;6,, nf߾}C1U o)vE=$FNQQQ?q2ߨ [º"q&S̒:ίA=xa>j@\9 "'s/3yE o DNny^W*OS_dѨ )mLΫlmtarW)#%~V5o1}x߳JcH$}םsk"nS\հ997Am_f?L.o`5y8Z\]߼Dc[ꫂ:J7+:mKj4_9Ð "'ߌ斱vi㤺5 / \\Ul͓ 1Ϯvlo4ި-}^fTBnzfQ4]BUA:}m[; B)x 얎 OLbx wU,c%+p *qՅDY;Ӧ@WA0aKo2'=B[~,V`~~@?7{joF}vRFQ5J5iꊲZ*G'r^fV/4PDKsJY-|w96up~7L;7ۦ+%?۞ws96,7Tts0?Nj2B!aldꋌ2xߦV{:̄Xa'uMsFps0_ /S^ۘWYjE !r!4?{i.]8e'h/8 !TR/-9*Y(_Vƹ!P3mI >DNz8o_PZ+&#֞y!,5*9rùx6ӦK \kҗ2Lt*?(}- 0ZZZHc||3&l 붶dix0 iȐ!O+,,$2tC7nx损_GGBHSSsNNNt…%Kܿĉ]+Щ}EFF"={m۶ . ?q̙.|VD07IGGg666p8...}wgjjjDDǏ544{+ @*++1 jii[6:i֭#>r8iiia2t:͛krΝ) +W $%%g"B x;Em{Ν;C||[GXvY7wOu8첳oܸB^$p>Em;r% IDAT;y$9g͚5MMMʽ{jiim۶{=|}}wD__Ӹo[*=T)SU\\w^Ν;:u&FSPPppp'̚5>}y&OU6lSQQQZZzڴi"6ð[n͞=TFFJ***nݺ(k."gҤIx[l])FZǃ9rBRR`5*44~څt+---N3f2FSQQ0aµk׸\.Xmm֭[  %%%%%ennu)SHHC\yѣGuV``aüΛ7/22֭͛KLLܼysuss yܹs]]] ﷶz˗8qmzNZjB0vvv<%?|`kk1bijgϨT*B6mBIII={lСxy<ɓѣGK.Zl ^t O*)):tU^^.p+޽;m4<^FF9q彼nܸޮi1zK♫V  jPSS!ҏ=4hPyyS^^ԩ+V.譨w;wWSLB\cjjCLziZ[[g;˾g޷ e˖BðEM-[,YM!OOO<ںxb*EʒWUUU%f;V؊D:66O1*lx믃 Bikkoݺ߻w猧h]p!9O={?~z3a...gΜmo.tȉF޽#t;vqD`/^i"!2S]ܹsV9s5k̦Mo!8q!&lݮb@7yd"=b"]RR0"B ΄q8G͛7ijjΜ9ĉcǎ-))ῌ|m=9͖< %!Ǥ$o??14B255=|G9t˗//\3̆q|UwޑgP!ku-yyy.Cx!b 򪪪믿Zvm|}} ߠs|a1O;a˗كO1cF``u<ꓺJ NKJJ4\LJJJt=];Z_ 6nXWWwA8ݻOޅ>]cWԷo_,HcVUUE|_<<>Ν;.\M믿į  vFHjkkx∭011!о}cnnnllp~3g,Xܜ+߻wO_DOFN\a03g$>邂?i}}}y21Yf׈sEٳDNNt"9MVVVRR@׺JF>WDjZZ;v&x 999WWWbiDDϺ_6003fƍ-[^QQA}z޽Duuu"r:uxùuVsssxx8&M3g+$3f͟?̙_P.^HlooO>?uqCKKk 8p`zzƍsssEk׮UWW'?Aqw#dddƎ[VVO?B/11ޞghgϞ9::E555L&ĉBfff&&&UUUۋosAz*'3̇Ξ=>hnnޯ_R&9p౗4:t޼y}ჟ_^^a8< B<ZZZSUUU'k%''ə._pMMͶmnݺUYYk׮͜9СC}IHHt[X,֎;ܹSZZJ3q 6޽㍍'O400Ww7oh4]ƌCSX 6ՈrCؼyLŋ.\b_h4)SYʶyM17&00ݻ?~d0^^^˖-()) 2dȑ111666˖-={6M~9^ۋ rз qoDN @\i=(|-4o\DN @\9 "'qA . rDN @\9 "'qA . rDN @\9 "'qA . rDNiɒ% ,ǎ?2L:ڒ233M𷕐`kk r!~)))Z?/2ӧOFv"##ϓJĈ# MFdggSHLLLD?s̒%K Ç]!bŊgϊ!AX;rJ!t$u_4zx<3:|򥝝\u222ڽ{wO|&iggǟ9l0U[[[ADm۶bX,Vii]DOII"'Fb ׯ D4_zPENFͭF=|_~kmmEǏ5 !cff&%%5~bN>`hhgHIIM<!pLLLXDNss3aׯWTT}^@tҒIOO'W8n8d2#G閖III! ߯)%%5gL㑓V8pԪU 9[ ôN8A@pp tUjj*yi Ağlk֬䓄b_ݻw1 0`@rr!C?~aQtttYYJtttCCڵkGA^իWGӧaϞ=/WWW0Fkoo'dff":p>~>MMMw(axIB+W;wZ[[cv]++Ҋ uuׯ_ 0b0[ILL|qCCäIԸ\nzz:;wWvp۷/!L?1aqqq| `[#ŋoݺDMMlٲe˖->|h?~}𴲲lMM>h4B!j.B9x`ll$%%y2ji4auu5 b\!쌧gaa'"X Q^ࠍ1"%%%//O]]9((N?^/[n]``޽{uuuEon ןBL&s222zRRR &P(͛7jkk#G.--7*//V>11!D$)99d2gΜb)**Dfdddyy B#/_gaacOOϔ&&9?ჵ5ɓ&M+LJJ"V/Hi֧5dFGG7!4|𜜜{ᓜ~‚XFLO)))EEExbl}znPP۷o/_I5#+!{DU3sLNN~M``&DoA 6l?."rXa%pX:4k֬~ !$zEt[̾$|z=Q[mmӧgйssssSRRRRR&O\PPbkkkdddhhhhhG!"HII!KII&IDaaaSNP(C Aq8X$QOqqw޼y[___TTR]]~u։09ri !0,""omJJ -GNr455K4Af9kkk߼y._GNq??+Wϟ?VSSsA<-44獍7ovqqsspe CJJڻӰa+$$ 02;W#"hENZQVA)V.(*8#-U ժ\PA&( SP!Lߞ.DUd=xsvuoe-@zzztttCCæMttt>#UUbH3ųuww(s=_&m?%Լx"y_&;rwg֭gϞI=ׯ[n6l?Hn O>|ɓ̙3qϟoٲar${峲B!EQYٳg<8ydkkkRRׯ_{i[[ڵkKJJSSӣGo끹eeeEEE/0HeN{! Ko޼iffFKYSFFƑ#G6oެ&{25dRJJ YBΉE/٣S߿?.uCBBx<@ ؽ{H0**J__fҫt'Nr$E(,,o]]ݤIF}Quu(.͛7閥9-XO>aXGϧ(sFFF1cVSS[p!! WD&y٤]zݻGQUi9Q%uN$qFX oqGG?ƍ)裏ȀK~t d)Qbcc SUU yyy<#=K.9991L33+WFVZpLLLII<0L RMIIIEEe~䉦&Y$hmmm==UU/^У%466r\ӧOeX#GΦ2__zS?0/_N!_:'Odff:;;׏7._@:BiiiQPP ю?~ĉ9;v,>>ԩSww.&&Fy}j…b-""ܼy!5aL7|GЇ'%%E]]}ڴiӧO[| 77/dXG!KKK쪫׮]ۧO+WL<ɓ'$9 sssuuu$9SqqgϬdѣ&L=zt|||TT͟?.0pzʁB+3'Suu5ttt :۷o ߿o>DƇڳgĉ\nEE?~ĉ$ĴFFFnڴ_eXG2e ܼy333S(޻w47orÇ9+WEFF@ii)L0AY^ff322/_W_ѹ B!V2sz-.../_9sӯ]v< IDAT2yibbxf,//SSS1b VVV/WQQ1qDu͌ ooﶶh3'qFzzd2e<:99ׯyǏ4!BHݻIIIcƌ ɑY2(((===??Аyyy@M /***;v+**f̘a``pA6mmmMwaeeutKK7"EEEI߸5BɯWfNwb^*>>׷H"0L]]]%`߾}޷o&s۪U|||Hٙxzzn۶?ooo_`L2@[[ܼJzUmmmVV9ɔEttt<{lJJ,,,H&B[̜+K-^~Z|X,>|J TVVZ[[Ց/EDDH$p;;;Rr򋶶MC,`n̘1 L~TBرcؐT5jTXX_|sssO:%S!3%%%Lggw8BJLLsB!z0sB!fN!B !BH^9!B 3'B!yaB!$/̜^!CX, WWWHZ-X,ޓ'O ƞ={H||IB!J9ƍ?~4ydz?ා`lٲE!މ"s駟 hffW\a0K.9r$ͶH$b byy믿Ξ=[  >ͽqk/}ry<^@@L>cd׮]bYXX~zAHPP+?~А̘1˵le˖u߹sgҤIV2d$%% :L:ѣG/B魙,YTTf7lP[[BN=zt„ G'͟?>>ǎ500ˆ 222Ο?/,-->dff655UVV_ڻwo]][B!$]MH'L }Ϟ=nmmmjjzavʕ+nZPP0|R0atjz{R/O:UfwMMMB޽{t_~w100hjjRWW'7VZӂO?t޽'N;v677+++Kwr|||>䓞={(**8p`鶶}v B!L2sz-$011(//>*** 3֦,lػw/}׊뺸b;wlr`'''xcƌꂂwwjqqH$zYhh().9EQ[[[\.lkk#眜)S3gRSS׬YckkKnughhaÆm۶/kfgg{ڵ֝;w.]`B! ?#N8ŋ  Mw!!!d8mΝ˗/3g5j@(Ν;wС#G$`ɒ%}5118p… `ٹAGGv.]x<;;;Lmذa"СC=\ڲeL,X }.\r;&O-BIc/233hB!+111z !B !BH^9!B 3'B!yaB!$/̜B!䅙B!>)>>`DFFJ ƺu^)1gr޾uV&i``vV`0[nO=zbXٹwޏ>b,ZHM۳gmzDQ5d>ŋ6*z/(**:tS 76 !@3'444ƌ-,]tmmm...<o311ٲeWsǏ'%%5447^/_ [j{uϟkhh\|}BAdNfffl6hϞ=-FFF\.wٲeO>=p[ ""|g?&!C(**P(X{:th'Oɱ:q0tvv^K7f---322^GG+V >x`Æ PTT4~ }}/IIICp8SN}L>cnQN:ehhrΝI&ZjȐ!fllL4r۷oQ6.H;Ǐ744p83f 7d^αc,X`oofݎW =QG0aEQϟ'|H Qkk+ƾX5k֖-[444FEQT||<̞=;$$o߾Ɲ2E]v-33֭[E*))tttڵk̘1EUTThii]paʕ֤|?s.u1ss<.t҂t]]ݜI&}u9TVVNNN٣tkǏ=ztmmmiiFaaaAAǻz/^lggˉf2?óg֭[7hРջBOTggcfϞMѡZZZ(YYYbO>ٱcGޯ_>vX2},ɟ]Ғ1777Xݝ$mm*[[[GQ́^o(ɉ妦>~}W/#;qdWfN] ر(eX3f ҥK)jllTSS̉[[[KQ?~wBQT\\ݻw`Ν;ɓ@gNz+WCQԏ? ׮];y/deLgNE 2WWWREIII,S|PZj]zj~([v-9(9x<555dɒN=JvaÆvvvԴرÃT,IDGG(s2w( I@QԵka~V\I.]˒WDIeN]ܥ)jnnDEI$ 6̝;(//דn޼I2'p„ :;; }||nݺG'@!{Kne?ȳ#Fv~***)TUU4hPyy9]tcbb]:"c:5pf...iiigΜSRRZp`Wd2`מ@?_#y&]K#::: I#'N$g 8qյ^֣tk...eee...beΝFFFJ]]e&ill\UU%z^-  HɓEEE>}zi{{Jru0lذaÆ?~K$!!AiT/^`'''xb222n߾}ܹPSSSOs̙s}llllnn/rd5 _\\lhhHwuppNJJ3fLxxxNN`Wϛ7499>~#WDPu3gJr%|366666&iS=ҭ޾};77:::"ojj"d}!9D}Y{/ҍ\z6555;;^xs… ./MMM|>Χo߾}ȑ W+pG̼~7 ]GTUU,--ǍwT9BB̜]sagggm۶?}TDD ۷/==w׮]ZZZgVPPܾ}ҥK xzz 5͛_~%1@^۷ۛx;;;__ߢ"Dd2uuue %%%++KΉvZkkΝ;.]Jn EM6UUUٮ>>.]RSS8q@ 666|>?11ȑ#999O>]z'OGdZ\\,={ʷKkjjrG,}gd=EQN߿?ɴ A///&KQԋ/6mdddDYfKs*.. ,L",^X[[`GEEQ%3HQK^D>bIpĉ\.}C QPPҚ?~yy9EQyyyt Ex<@{ qi{n~ƌl6[MMm…EEEEl[[ۊ]NttmmmY,ȑ#IջBOԖ-[͛GQTGG#G,X[/_N֮GIIͭIf{/VVV622|{Q^^e]ʓuNuuu&M=zѣGΝ;vޭi(իp=<==H}N!uY())Lggw½{EEE}Q@@񓕽Waaa}}qׯ_'#9vX||B[LLW>'j…b-""bʔ)zD!;9tdz}yظ6LOOd~7zD!;OB!^ !B!̜B!䅙B!0sB!919s@FFn+**_9sĄb=]rW2̙3ݫfǏO'Os8Yf׿nZQQܜƆO?o BoAdN4///ssw=7tƍ5kH`oo?g[`SSƍ+**lׯSLYd 8qfZ굂׿bY/^L,yY2eʆ sttTUU HJJꫯ*++/=^tiɒ%F LKK3gEQk``͎;D"3EQM8qÆ >8u~8444,, ϟ/=`0MMM:pqr|mmmAe BH2pzʁll6N`nnD"%%3g̞=[$C!C2sN^< :444 ZzݻK?bĈ* Ç/^^\\\]] C=}1ݾCCv0/w͚5'NxO &.EQ򋝝]hh(ÑljjRUU%ȁD"ܿFFFcc˿+MvٜgϞ]>{,11q׮]8p I:kjj<ɉ/ !z}{x.\VV⒖v̙8%% H75pf-z8p p8##Jݽd_^A.DJ$PQQurr"?~&L:ujKK˿o2`gҤIsNuur++ {XXFyyBz=X۷o;w.44ݻIIIcƌ ɑ_\\WHS %%%zzzrjii9.-߿\ EQsrE]v-33SzZϝ;G صk%EQ<իϟ?_xEQ #&&O>166~{lvEEEEEօ V\imm-KzznNNNuuI(~رcI1G--- 1/^777Xݝp{{{ȸq~gG!X}ze%0I_ֹFw}cHHH(b͘1dN;v(nݺEQȑ#ּ .Z{4o<9}/k60{ldK˕ ͭSGGe2BMMv N: b1"% IpڵW&w^DD+())b??kג=gNÆ %)ikk+G,(H$6l;w.EQ$;hnn9BBtɜ>L^8#Fmm~UTTS<X,hhh@ʐ2Dmm-hii555I !C211.0fe@0~似YfɜfAfCGGdiȱJ]]uZ 1III8q"9e``}<ٰKYYX,vqqٹs'ͦ; NNNx`߾}B+WD"˜Bu}(뜺p |qs΅v)S^^bbbҧO.Df;:::PZZ Ν3g32̞=f׮]3f̐y!IIIӧO^D举IKK33o޼ӽ|z0:a"nnnoͥ0#G233_Mg͚G9B!̉uNoЂ3ܶm޾`Wrqqioo߷o_zz]O|oooww1c455Z{Fs;gVPPܾ}ҥK49QQQ'O& %%%++.0s̗ ̙3k֬UQQyfbcc]ںsΥK2 '';w*"ٳg$.9EQ[[[\.lkk#眜)SB! 6Y/ڛ6m*//߳gܹs_YNLL\l٠A.^DN=~f27ElgĈ'NxIySN9fCCѣGS%.$$$**K$:nذa"СC##.\&&&;vLիW[[[ ?< s:tȑ#H;K,QVV۷ɂ gg熆[[_t)::l!%%%Lggw8XfMPPPyyy߾}X Ǎ/_~B!$?˜:u*&&fܸqIUUU .mmmB!aOѱm۶w=ccc:l0===&7߼!BoCUGBx0͛7o޼]!B!0sB!fN!B !BH^h攑`0n*>f0H$䔱13?5eggWTTDz!C8Μ9s_7B!y|ye*++6$ݸqc͚5tyƍMMM[ly B!Ϝ#Gu֚N88 ]QAA!((Hzڗvh".ﯢLMMM522r˖-koog\`0֭[giiw^ptt\bJ$خ\ҷo_777mmu֝>}!So͜~,ӓC4GGGeeoVΦUUUJ_}1cihhhXXإK,Y2jԨ9sݻ; Ԥ#B BH"PB!>t0&;;;O81nܸ?Xz]MM͛7.[LV^f&?=:--.o>Dҷoߓ'OÇ{]T]]]x~ //ܹ嬢bpppjjH$D">gXo>w\hh(Ioc丨@YYYkB}ze$}w `ǎ-r}z嚚7n| B!())_dff:;; BWbbb,,,藽B!ۇB!0sB!fN!B !BH^OmĒmPIENDB`python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/Install.rst0000644000175000017500000000563111061471005027106 0ustar zackzack================================== Installing repoze.who.plugins.ldap ================================== Installing the `repoze.who.plugins.ldap `_ plugin itself is rather easy, but unfortunately, installing `python-ldap `_ (the main dependency of this plugin) may be a nightmare. Hopefully this document will help you get this plugin and its dependencies working quickly. The quick install ================== If you've already installed `python-ldap` on your computer, the following command will install `repoze.who.plugins.ldap`:: easy_install repoze.who.plugins.ldap If it's not already installed on your computer, read on. **Note:** If you're on Ubuntu, don't rely on the python-ldap package provided by the package manager - `its install will be ignored by easy_install for some reason `_; this may also happen in Debian. Read on to install it manually. Installing `python-ldap` manually ================================= If you have problems to install the plugin, they are very likely to be caused by `python-ldap`: You will have to install it manually in order to set the correct path to the OpenLDAP libraries. Once you've successfully installed `python-ldap`, you'll be ready to install `repoze.who.plugins.ldap` with the command above. Installing `python-ldap` on Ubuntu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This may also work for Debian. If this procedure works in your Debian system, please let us know. First, install its dependencies:: sudo apt-get install build-essential libldap2-dev libsasl2-dev python-dev libssl-dev Then, download the source of `python-ldap`, uncompress it and `cd` to its folder. Once you're there, edit the `setup.cfg` file to correct the parameters in the `[_ldap]` section:: extra_objects = extra_compile_args = libs = ldap_r lber ssl crypto library_dirs = /usr/lib include_dirs = /usr/include /usr/lib/sasl2 Save the file and run:: python setup.py build sudo python setup.py install Installing on other systems ~~~~~~~~~~~~~~~~~~~~~~~~~~~ For more information, visit: http://python-ldap.sourceforge.net/doc/html/installing.html Please don't hesitate to let us know how you installed it on other systems, so that we can post it here. Troubleshooting ~~~~~~~~~~~~~~~~ If you have trouble to install `python-ldap` on your system, please ask in `the python-ldap mailing list `_. Installing the mainline development branch ========================================== The plugin is hosted in `a Bazaar branch hosted at Launchpad.net `_. To get the latest source code, run:: bzr branch lp:repoze.who.plugins.ldap Then run the command below, from the project folder:: python setup.py develop python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/Demo.rst0000644000175000017500000000530111061324266026364 0ustar zackzack======================= Trying the demo project ======================= `repoze.who.plugins.ldap` ships with a working `TurboGears 2 `_ project powered by this `repoze.who` plugin. It enables you to give it a try with your own LDAP server. Getting the source ================== This demo is included the the *demo/* subfolder of the project. If you installed the plugin directly, you may `get its source from the PYPI `_. You may also get the latest source code from its Bazaar branch:: bzr branch lp:repoze.who.plugins.ldap Installing the project ====================== The project is powered by TurboGears 2, so you should `install it `_ first. Once you've installed TurboGears, you will be ready to install the demo:: cd demo python setup.py develop Configuring the demo ==================== Open the `who.ini` file and set your LDAP URL and your base Distinguished Name, both found in the `[plugin:ldap_auth]` section. For example, :: [plugin:ldap_auth] use = repoze.who.plugins.ldap:LDAPAuthenticatorPlugin ldap_connection = ldap://localhost base_dn = ou=people,dc=localhost Running the application ======================= You can run the application from the *demo/* folder:: paster serve development.ini Then go to http://localhost:8080/ to use it! .. image:: img/demo_screenshot_index.png Trying to authenticate via LDAP =============================== Now go to the "private" section http://localhost:8080/about and login with your credentials in the LDAP server you're using. If you entered them correctly, you'll access the page! .. image:: img/demo_screenshot_authenticated.png How it works ============ This demo configures `repoze.who` via an INI file (`who.ini`), which is loaded in `demo/ldapauth/config/middleware.py`. Then the login form is triggered when you try to access a private page as anonymous, as in the `about` action controller (found at `ldapauth.controllers.root:RootController`):: # ... def about(self): if request.environ.get('repoze.who.identity') == None: raise HTTPUnauthorized() # ... Once the user has been authenticated, you'll be able to access her DN with the code below, for example:: from pylons import request dn = request.environ['repoze.who.identity']['repoze.who.userid'] This demo also features the :class:`LDAPAttributesPlugin` metadata provider, which is used in the private page to show all the available LDAP attributes for your entry in the LDAP server you are using. Such metadata is loaded in:: request.environ['repoze.who.identity'] python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/Using.rst0000644000175000017500000003173711061470507026601 0ustar zackzack========================================================== Using the repoze.who LDAP plugins in your WSGI Application ========================================================== Implementing authentication via `repoze.who` is a simple task that requires few lines of code. So using its LDAP plugins should not be an exception: You just have to configure `repoze.who` in your application and then add the `repoze.who.plugins.ldap` plugin(s) you want to use in your application. Setting up `repoze.who` with the LDAP authenticator =================================================== This section explains how to setup `repoze.who` in order to use the LDAP plugins in your WSGI application. It is based on `the documentation for repoze.who `_. You can configure your authentication mechanism powered by `repoze.who` with two methods: With an INI file or with Python code. In the examples below we are only going to use the main plugin provided by this package: The LDAP authenticator itself (:class:`LDAPAuthenticatorPlugin`). The other plugins don't deal with authentication, but are useful to load automatically data related to the authenticated user from the LDAP server. Using the `repoze.who` terminology, :class:`LDAPAuthenticatorPlugin` is an `authenticator plugin` and the others are `metadata provider plugins`. Configuring `repoze.who` in a INI file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can configure your `repoze.who` based authentication via a `*.ini` file, and then load such settings in your application. Say we have a file called `who.ini` with the following contents:: # These contents have been adapted from: # http://static.repoze.org/whodocs/#middleware-configuration-via-config-file [plugin:form] use = repoze.who.plugins.form:make_plugin rememberer_name = auth_tkt [plugin:auth_tkt] use = repoze.who.plugins.auth_tkt:make_plugin secret = something [plugin:ldap_auth] use = repoze.who.plugins.ldap:LDAPAuthenticatorPlugin ldap_connection = ldap://ldap.yourcompany.com base_dn = ou=developers,dc=yourcompany,dc=com [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider [identifiers] plugins = form;browser auth_tkt [authenticators] plugins = ldap_auth [challengers] plugins = form;browser With the settings above, authentication via `repoze.who` is configured this way: Visitors will login with a form, providing their user name and password; then these credentials will be checked against the LDAP server `ldap.yourcompany.com` under `ou=developers,dc=yourcompany,dc=com`. This form will be displayed when your WSGI application issues an HTTP *401* error. For example, if an user enters `jsmith` as the user name and `valencia` as their password, the LDAP authenticator will build their Distinguished Name (DN) as `uid=jsmith,ou=developers,dc=yourcompany,dc=com` and will try to authenticate them in the `ldap.yourcompany.com` LDAP server with this DN and `valencia` as the password. You may modify the way the DN is generated by subclassing :class:`LDAPAuthenticatorPlugin` to override the `_get_dn` method. Finally, you can load these settings by adding the `repoze.who` middleware to your application:: from repoze.who.config import make_middleware_with_config app_with_auth = make_middleware_with_config(app, '/path/to/who.ini') In the documentation for `repoze.who` there is `a more detailed explanation `_ for the INI file method. Configuring `repoze.who` with Python code ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The Python code below does the same as the INI file above:: # This script has been adapted from # http://static.repoze.org/whodocs/#module-repoze.who.middleware # Importing the plugins to be used from repoze.who.interfaces import IIdentifier, IChallenger from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin from repoze.who.plugins.form import FormPlugin from repoze.who.plugins.ldap import LDAPAuthenticatorPlugin # Configuring the plugins ldap_auth = LDAPAuthenticatorPlugin('ldap://ldap.yourcompany.com', 'ou=developers,dc=yourcompany,dc=com') auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt') form = FormPlugin('__do_login', rememberer_name='auth_tkt') form.classifications = { IIdentifier: ['browser'], IChallenger: ['browser'] } # only for browser identifiers = [('form', form),('auth_tkt',auth_tkt)] authenticators = [('ldap_auth', ldap_auth)] challengers = [('form',form)] mdproviders = [] # Using the default repoze.who classifiers: from repoze.who.classifiers import default_request_classifier, \ default_challenge_decider log_stream = None import os if os.environ.get('WHO_LOG'): log_stream = sys.stdout Then you can get these settings applied by adding the `repoze.who` middleware to your application:: app_with_auth = PluggableAuthenticationMiddleware( app, identifiers, authenticators, challengers, mdproviders, default_request_classifier, default_challenge_decider, log_stream = log_stream, log_level = logging.DEBUG ) In the documentation for `repoze.who` there is `a detailed explanation `_ for this method. Framework-specific documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You may want to check the following framework-specific documents to learn tips on how to implement `repoze.who` in the framework you are using: * **Pylons:** `Authentication and Authorization with repoze.who `_. * TurboGears. Using the LDAP plugins for repoze.who ===================================== Once you've setup `repoze.who`, you'll be ready to use its LDAP plugins. Below you will find how to use them in your application. .. module:: repoze.who.plugins.ldap .. class:: LDAPAuthenticatorPlugin(ldap_connection, base_dn) This is the main plugin. It's in charge of the LDAP authentication itself using the LDAP connection object provided in the constructor (**ldap_connection**) — which can be an LDAP URL or an `ldap.ldapobject.SimpleLDAPObject` instance. It connects to the specified LDAP server and tries to `bind` with the `Distinguished Name` (DN) made by joining the `login` in the `identity` dictionary as the user id (`uid`) and the **base_dn** specified in the constructor, and then it binds with the `password` found in the `identity` dictionary. For example, if the `login` provided by the identifier is `carla` and the **base_dn** provided in the constructor is `ou=employees,dc=example,dc=org`, the resulting DN will be `uid=carla,ou=employees,dc=example,dc=org`. Therefore this plugin is compatible with any `identifier plugin` that defines the `login` and `password` items in the `identity` dictionary (the `identifier plugins` provided by the built-in `repoze.who.plugins.form` plugin are some of them). It is a highly customizable plugin which can be adapted to your needs with no hassle. You could also include in the login form a `select` field for people to select the department they belong to, being the key of such departments the `Organizational Unit` in the LDAP server; then, in the **_get_dn** method you would get such value from the WSGI environment object (**environ**). You may change the way the DN is created by subclassing :class:`LDAPAuthenticatorPlugin` to override the *_get_dn* method. For example, say in your company (with `dc=yourcompany,dc.com` as its DN) everybody belongs to the `Organizational Unit` (OU) **employees** (`ou=employees`), except the shareholders who belong to the OU **shareholders** (`ou=shareholders`):: class YourCompanyLDAPAuthenticatorPlugin(LDAPAuthenticatorPlugin): """Sample LDAP authenticator adapted to your company.""" shareholders = ('lgarcia, 'mferreira', 'cnarea') """Set of shareholders of the company""" def _get_dn(self, environ, identity): try: if identity['login'] in self.shareholders: ou = 'shareholders' else: ou = 'employees' return u'uid=%s,ou=%s,%s' % (identity['login'], ou, self.base_dn) except (KeyError, TypeError): raise ValueError, ('Could not find the DN from the identity ' 'and environment') It is possibly an useless example on how to customize the way the DN is found, but it's enough to show how to override it. To configure this plugin from an INI file, you'd have to include a section like this:: [plugin:ldap_auth] use = repoze.who.plugins.ldap:LDAPAuthenticatorPlugin ldap_connection = ldap://yourcompany.com base_dn = ou=employees,dc=yourcompany,dc=com If you're using a custom LDAP authenticator, as in the example above, you would have to change the `use` directive accordingly — for example:: use = yourpackage.lib.auth:YourCompanyLDAPAuthenticatorPlugin Finally, add the plugin to the set of authenticators:: [authenticators] plugins = ldap_auth But if you're configuring `repoze.who` via Python code, you can use the code below:: ldap_auth = LDAPAuthenticatorPlugin('ldap://ldap.yourcompany.com', 'ou=developers,dc=yourcompany,dc=com') But if you're using a custom LDAP authenticator, you would have to use the code below instead:: ldap_auth = YourCompanyLDAPAuthenticatorPlugin('ldap://ldap.yourcompany.com', 'ou=employees,dc=yourcompany,dc=com') Finally, add this authenticator to the set of authenticators:: authenticators = [('ldap_auth', ldap_auth)] As in the example above. .. class:: LDAPAttributesPlugin(ldap_connection[, attributes=None[, filterstr='(objectClass=*)']]) This plugin enables you to load data for the authenticated user automatically and have it available from the WSGI environment — in the `identity` dictionary, specifically. **ldap_connection** represents the connection to the LDAP server, which, as in :class:`LDAPAuthenticatorPlugin`, can be either an LDAP URL or an instance of `ldap.ldapobject.SimpleLDAPObject`. **attributes** represents the list of user's attributes that you would like to fetch from the LDAP server; it can be an iterable, an string where the attributes are separated by commas, or *None* to fetch all the available attributes. By default it loads the attributes available for *any* entry whose *DN* is the same as the one found by :class:`LDAPAuthenticatorPlugin`, which is desired in most situations. However, if you would like to exclude some entries, you may setup a filter by means of the **filterstr** parameter, which is an string whose format is defined by `RFC 4515 - Lightweight Directory Access Protocol (LDAP): String Representation of Search Filters `_. There is no advanced usage for this plugin, and hopefully you would never need to subclass it to suit your needs. To configure this plugin from an INI file, you'd have to include a section like this:: [plugin:ldap_attributes] use = repoze.who.plugins.ldap:LDAPAttributesPlugin ldap_connection = ldap://ldap.yourcompany.com attributes = cn,sn,mail If instead of loading the *Common Name*, *surname* and *email*, as with the settings above, you'd prefer to load all the available attributes for the authenticated user, you'd just have to remove the *attributes* directive. Finally, add the plugin to the set of metadata providers:: [mdproviders] plugins = ldap_attributes But if you want to configure it via Python code, you can use the code below:: ldap_attributes = LDAPAttributesPlugin('ldap://ldap.yourcompany.com', ['cn', 'sn', 'email']) Again, if you would prefer to load all the available attributes for the user, you just have to remove the second parameter. Finally, add this authenticator to the set of metadata providers in your Python code:: mdproviders = [('ldap_attributes', ldap_attributes)] python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/index.rst0000644000175000017500000000736511061470701026617 0ustar zackzack=================================================================== repoze.who.plugins.ldap - LDAP Authentication for WSGI Applications =================================================================== :Author: `Gustavo Narea `_ :Version: |release| :License: GNU General Public License v3 :Homepage: http://code.gustavonarea.net/repoze.who.plugins.ldap/ .. module:: repoze.who.plugins.ldap :synopsis: LDAP authentication middleware for WSGI `repoze.who.plugins.ldap `_ is a Python package that provides `LDAP `_ authentication, and related utilities, in `WSGI `_ applications via `repoze.who `_. It can be used with any LDAP server and any WSGI framework (or no framework at all). It provides you with an straightforward solution to enable LDAP support in your applications. Yes, you read well: "straightforward", "LDAP" and "applications" are in the same sentence. In fact, you may make your application LDAP-aware in few minutes and with few lines of code. Another great news is that this package is *fully* documented and provides you with a working and documented demo project. Get started! ============ .. toctree:: :maxdepth: 2 Install Using Demo You can also browse `the online API documentation `_, or generate it by yourself with Epydoc from the root directory of the project:: epydoc --config=docs/epydoc.conf repoze Links ====== If you need help, the best place to ask is `the repoze project mailing list `_, because the plugin author is subscribed to this list. You may also use the `#repoze `_ IRC channel or `Launchpad.net's answers for quick questions only `_. Development-related links include: - `Homepage at Launchpad.net `_. - `Bug tracker `_. - `Feature tracker `_. - `Bazaar branches `_. Applications using the plugin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The repoze.who LDAP plugin is being used in the following applications: * `Animador `_. If your're using this plugin in your application(s), please let me know! Your implementation may be useful for somebody else. Contributing ============ Any patch is welcome, but if you can, please make sure to update the test suite accordingly and also make sure that every test passes. Also, please try to stick to PEPs `8 `_ and `257 `_, as well as use `Epydoc fields `_ where applicable. Thanks! ======= This plugin was made possible thanks to the people below: - **Chris McDonough**, for his guidance throughout the development of the plugin. Copyright notice for this documentation ======================================= Copyright (c) 2008, by Gustavo Narea. Permission is granted to copy, distribute and/or modify this document under the terms of the `GNU Free Documentation License `_, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/source/conf.py0000644000175000017500000001337311056277334026264 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who.plugins.ldap documentation build configuration file, created by # sphinx-quickstart on Sat Aug 30 18:42:51 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os here = os.path.dirname(os.path.abspath(__file__)) root = os.path.dirname(os.path.dirname(here)) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. sys.path.append(root) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_buildtemplates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = 'repoze.who.plugins.ldap' copyright = '2008, Gustavo Narea' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. release = open(os.path.join(root, 'VERSION')).readline().rstrip() # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. #exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'default.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_buildstatic'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'repozewhopluginsldapdoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'repozewhopluginsldap.tex', 'repoze.who.plugins.ldap Documentation', 'Gustavo Narea', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/docs/Makefile0000644000175000017500000000431611060775345025122 0ustar zackzack# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = a4 # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html web pickle htmlhelp latex changes linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " pickle to make pickle files (usable by e.g. sphinx-web)" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf build/* html: mkdir -p build/html build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html @echo @echo "Build finished. The HTML pages are in build/html." pickle: mkdir -p build/pickle build/doctrees $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle @echo @echo "Build finished; now you can process the pickle files or run" @echo " sphinx-web build/pickle" @echo "to start the sphinx-web server." web: pickle htmlhelp: mkdir -p build/htmlhelp build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in build/htmlhelp." latex: mkdir -p build/latex build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex @echo @echo "Build finished; the LaTeX files are in build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p build/changes build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes @echo @echo "The overview file is in build/changes." linkcheck: mkdir -p build/linkcheck build/doctrees $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in build/linkcheck/output.txt." python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/README0000644000175000017500000000142511061472477023411 0ustar zackzackrepoze.who.plugins.ldap -- LDAP Authentication for WSGI Applications repoze.who.plugins.ldap is an LDAP plugin for the identification and authentication framework for WSGI applications, repoze.who, which acts as WSGI middleware. It provides with an straightforward solution to enable LDAP support in your applications. Yes, you read well: "straightforward", "LDAP" and "applications" are in the same sentence. In fact, you may make your application LDAP-aware in few minutes and with few lines of code. Another great news is that this package is *fully* documented and provides you with a working and documented demo project. See the `docs` subdirectory of this package for more information, or browse the online documentation . python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/MANIFEST.in0000644000175000017500000000043511060775376024272 0ustar zackzackinclude README include CHANGELOG include VERSION include LICENSE include MANIFEST.in recursive-include docs/ * prune docs/build prune docs/api recursive-include demo/ * recursive-exclude demo/ *.egg *.db *.po prune demo/LDAPAuth.egg-info prune demo/ez_setup global-exclude *~ *.pyc python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/0000755000175000017500000000000011061527367030717 5ustar zackzack././@LongLink0000000000000000000000000000015500000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/requires.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/requ0000644000175000017500000000007611061527367031621 0ustar zackzackrepoze.who>=1.0.6 python-ldap>=2.3.5 setuptools zope.interface././@LongLink0000000000000000000000000000016700000000000011571 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/namespace_packages.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/name0000644000175000017500000000004511061527367031561 0ustar zackzackrepoze repoze.who repoze.who.plugins ././@LongLink0000000000000000000000000000016500000000000011567 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/dependency_links.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/depe0000644000175000017500000000000111061527367031546 0ustar zackzack ././@LongLink0000000000000000000000000000015100000000000011562 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/PKG-INFOpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/PKG-0000644000175000017500000000463411061527367031347 0ustar zackzackMetadata-Version: 1.0 Name: repoze.who.plugins.ldap Version: 1.0 Summary: LDAP plugin for repoze.who Home-page: http://code.gustavonarea.net/repoze.who.plugins.ldap/ Author: Gustavo Narea Author-email: me@gustavonarea.net License: GNU General Public License v3 Download-URL: https://launchpad.net/repoze.who.plugins.ldap/+download Description: repoze.who.plugins.ldap -- LDAP Authentication for WSGI Applications repoze.who.plugins.ldap is an LDAP plugin for the identification and authentication framework for WSGI applications, repoze.who, which acts as WSGI middleware. It provides with an straightforward solution to enable LDAP support in your applications. Yes, you read well: "straightforward", "LDAP" and "applications" are in the same sentence. In fact, you may make your application LDAP-aware in few minutes and with few lines of code. Another great news is that this package is *fully* documented and provides you with a working and documented demo project. See the `docs` subdirectory of this package for more information, or browse the online documentation . repoze.who.plugins.ldap Changelog ================================= 1.0 (2008-09-11) ------------------------------- The initial release. - Provided the LDAP authenticator, which is compatible with identifiers that define the 'login' item in the identity dict. - Included the plugin to load metadata about the authenticated user from the LDAP server. - Documented how to install and use the plugins. - Included Turbogears 2 demo project, using the plugin. There is also a section in the documentation to explain how the demo works. Keywords: ldap web application server wsgi repoze repoze.who Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP ././@LongLink0000000000000000000000000000015600000000000011567 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/top_level.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/top_0000644000175000017500000000000711061527367031600 0ustar zackzackrepoze ././@LongLink0000000000000000000000000000015500000000000011566 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/not-zip-safepython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/not-0000644000175000017500000000000111060773546031507 0ustar zackzack ././@LongLink0000000000000000000000000000015400000000000011565 Lustar rootrootpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/SOURCES.txtpython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/repoze.who.plugins.ldap.egg-info/SOUR0000644000175000017500000000464411061527367031442 0ustar zackzackCHANGELOG LICENSE MANIFEST.in README VERSION setup.cfg setup.py demo/README demo/__init__.py demo/development.ini demo/setup.cfg demo/setup.py demo/test.ini demo/who.ini demo/ldapauth/__init__.py demo/ldapauth/websetup.py demo/ldapauth/config/__init__.py demo/ldapauth/config/app_cfg.py demo/ldapauth/config/environment.py demo/ldapauth/config/middleware.py demo/ldapauth/controllers/__init__.py demo/ldapauth/controllers/error.py demo/ldapauth/controllers/root.py demo/ldapauth/controllers/template.py demo/ldapauth/lib/__init__.py demo/ldapauth/lib/app_globals.py demo/ldapauth/lib/base.py demo/ldapauth/lib/helpers.py demo/ldapauth/model/__init__.py demo/ldapauth/model/identity.py demo/ldapauth/public/favicon.ico demo/ldapauth/public/css/style.css demo/ldapauth/public/images/error.png demo/ldapauth/public/images/grad_blue_7x80.png demo/ldapauth/public/images/header_inner2.png demo/ldapauth/public/images/info.png demo/ldapauth/public/images/logo.gif demo/ldapauth/public/images/logo.png demo/ldapauth/public/images/ok.png demo/ldapauth/public/images/star.png demo/ldapauth/public/images/strype2.png demo/ldapauth/public/images/tg2_04.gif demo/ldapauth/public/images/tg_under_the_hood.png demo/ldapauth/public/images/under_the_hood_blue.png demo/ldapauth/templates/__init__.py demo/ldapauth/templates/about.html demo/ldapauth/templates/debug.html demo/ldapauth/templates/footer.html demo/ldapauth/templates/header.html demo/ldapauth/templates/index.html demo/ldapauth/templates/login.html demo/ldapauth/templates/master.html demo/ldapauth/templates/sidebars.html demo/ldapauth/tests/__init__.py demo/ldapauth/tests/test_models.py demo/ldapauth/tests/functional/__init__.py demo/ldapauth/tests/functional/test_root.py docs/Makefile docs/epydoc.conf docs/source/Demo.rst docs/source/Install.rst docs/source/Using.rst docs/source/conf.py docs/source/index.rst docs/source/img/demo_screenshot_authenticated.png docs/source/img/demo_screenshot_index.png repoze/__init__.py repoze.who.plugins.ldap.egg-info/PKG-INFO repoze.who.plugins.ldap.egg-info/SOURCES.txt repoze.who.plugins.ldap.egg-info/dependency_links.txt repoze.who.plugins.ldap.egg-info/namespace_packages.txt repoze.who.plugins.ldap.egg-info/not-zip-safe repoze.who.plugins.ldap.egg-info/requires.txt repoze.who.plugins.ldap.egg-info/top_level.txt repoze/who/__init__.py repoze/who/plugins/__init__.py repoze/who/plugins/ldap/__init__.py repoze/who/plugins/ldap/plugins.py repoze/who/plugins/ldap/tests.pypython-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/setup.py0000644000175000017500000000505011061473530024230 0ustar zackzack# -*- coding: utf-8 -*- # # repoze.who.plugins.ldap, LDAP authentication for WSGI applications. # Copyright (C) 2008 by Gustavo Narea # # This file is part of repoze.who.plugins.ldap # # # repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or any later # version. # # repoze.who.plugins.ldap 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 General # Public License for more details. # # You should have received a copy of the GNU General Public License along with # repoze.who.plugins.ldap. If not, see . import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README')).read() CHANGELOG = open(os.path.join(here, 'CHANGELOG')).read() version = open(os.path.join(here, 'VERSION')).readline().rstrip() setup( name='repoze.who.plugins.ldap', version=version, description="LDAP plugin for repoze.who", long_description='\n\n'.join([README, CHANGELOG]), classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License (GPL)", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP" ], keywords='ldap web application server wsgi repoze repoze.who', author="Gustavo Narea", author_email="me@gustavonarea.net", url="http://code.gustavonarea.net/repoze.who.plugins.ldap/", download_url="https://launchpad.net/repoze.who.plugins.ldap/+download", license="GNU General Public License v3", include_package_data=True, packages=find_packages(exclude=["*.tests", "demo", "demo.*"]), namespace_packages=['repoze', 'repoze.who', 'repoze.who.plugins'], zip_safe=False, tests_require = ['dataflake.ldapconnection>=0.3'], install_requires=[ 'repoze.who>=1.0.6', 'python-ldap>=2.3.5', 'setuptools', 'zope.interface' ], test_suite="repoze.who.plugins.ldap.tests.suite" ) python-repoze.who-plugins-20090913/repoze.who.plugins.ldap-1.0/setup.cfg0000644000175000017500000000007311061527367024347 0ustar zackzack[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 python-repoze.who-plugins-20090913/repoze.who.plugins.ldap0000777000175000017500000000000011253246102027133 2repoze.who.plugins.ldap-1.0/ustar zackzackpython-repoze.who-plugins-20090913/repoze.who.plugins.sa0000777000175000017500000000000011253246104026712 2repoze.who.plugins.sa-1.0rc2/ustar zackzack