diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..6564eec --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,13 @@ +======= +Credits +======= + +Development Lead +---------------- + +* Janus Skonieczny + +Contributors +------------ + +None yet. Why not be the first? \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..e3ae1cb --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,111 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/wooyek/flask-social-blueprint/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version. +* Any details about your local setup that might be helpful in troubleshooting. +* Detailed steps to reproduce the bug. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything tagged with "bug" +is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "feature" +is open to whoever wants to implement it. + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +Flask Social Blueprint could always use more documentation, whether as part of the +official Flask Social Blueprint docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at https://github.com/wooyek/flask-social-blueprint/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +Get Started! +------------ + +Ready to contribute? Here's how to set up `flask-social-blueprint` for local development. + +1. Fork the `flask-social-blueprint` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/flask-social-blueprint.git + +3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: + + $ mkvirtualenv flask-social-blueprint + $ cd flask-social-blueprint/ + $ python setup.py develop + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: + + $ flake8 flask-social-blueprint tests + $ python setup.py test + $ tox + + To get flake8 and tox, just pip install them into your virtualenv. + +6. Commit your changes and push your branch to GitHub:: + + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + +7. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.rst. +3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check + https://travis-ci.org/wooyek/flask-social-blueprint/pull_requests + and make sure that the tests pass for all supported Python versions. + +Tips +---- + +To run a subset of tests:: + + $ python -m unittest tests.test_flask-social-blueprint \ No newline at end of file diff --git a/README.md b/README.md index 256dfc6..1504be1 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ flask-social-blueprint An OAuth based authentication blueprint for flask. Easy to extend and override. +https://github.com/wooyek/flask-social-blueprint + ## Demo Based on `example/gae` codebase with secret `settings_prd.py` provided for @@ -68,16 +70,22 @@ Done! No This is just authentication blueprint there is no templates, models and stuff that you would want to customize yourself. +## Examples + The example has a working model and templates, has a bunch of dependencies like Flask-SLQAlchemy, you can take it as a wire frame modify and build your app with that. +Examples are made from some existing apps, they may contain more stuff that's +really needed to showcase this module. When in trouble just ask questions. + Or just drop in this solution inside your working Flask app. I should not create any conflicts with existing stuff. You maybe required to write an adapter for your User model and SocialConnection model (or similar) but that's 3 functions for the adapter. Any User model requirements come from Flask_security. + ## What to do more? 1. More providers diff --git a/example/gae/auth/models.py b/example/gae/auth/models.py index c7cab8f..5b7b743 100644 --- a/example/gae/auth/models.py +++ b/example/gae/auth/models.py @@ -245,5 +245,5 @@ def init_app(app): security = security.init_app(app, AppEngineUserDatastore(User, Role)) security.send_mail_task(send_mail) - from flask_social_blueprint import SocialBlueprint + from flask_social_blueprint.core import SocialBlueprint SocialBlueprint.init_bp(app, SocialConnection, url_prefix="/_social") diff --git a/example/sqla/auth/models.py b/example/sqla/auth/models.py index b8ceb39..d043a40 100644 --- a/example/sqla/auth/models.py +++ b/example/sqla/auth/models.py @@ -157,5 +157,5 @@ def init_app(app): security = security.init_app(app, SQLAlchemyUserDatastore(db, User, Role)) security.send_mail_task(send_mail) - from flask_social_blueprint import SocialBlueprint + from flask_social_blueprint.core import SocialBlueprint SocialBlueprint.init_bp(app, SocialConnection, url_prefix="/_social") diff --git a/example/sqla/manage.py b/example/sqla/manage.py index e744854..5f3cdc3 100644 --- a/example/sqla/manage.py +++ b/example/sqla/manage.py @@ -9,9 +9,9 @@ class InitDatabase(Command): """Initialize database""" def run(self): - logging.debug("db.reate_all") - from website.database import db - db.create_all() + import website.database + website.database.init_db() + manager = Manager(app) manager.add_command('initdb', InitDatabase()) diff --git a/example/sqla/tests.py b/example/sqla/tests.py new file mode 100644 index 0000000..b47a19c --- /dev/null +++ b/example/sqla/tests.py @@ -0,0 +1,30 @@ +# coding=utf-8 +# Created 2014 by Janusz Skonieczny +import logging +import os +import tempfile +import unittest +import flask_social_blueprint + +from main import app +from website import database + + +class TestFlaskSocialBlueprint(unittest.TestCase): + def setUp(self): + self.db_fd, self.db_file = tempfile.mkstemp() + app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///" + self.db_file + self.app = app.test_client() + with app.app_context(): + database.init_db() + + def tearDown(self): + os.close(self.db_fd) + os.unlink(self.db_file) + + def test_login_redirect(self): + # simple smoke test + rv = self.app.get('/') + logging.debug("rv: %s" % rv.headers) + self.assertEqual(302, rv.status_code) + assert rv.headers.get('Location').startswith("http://localhost/login") diff --git a/example/sqla/website/database.py b/example/sqla/website/database.py index 7e80e47..1c2799a 100644 --- a/example/sqla/website/database.py +++ b/example/sqla/website/database.py @@ -1,5 +1,11 @@ # coding=utf-8 # Created 2014 by Janusz Skonieczny +import logging from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() + + +def init_db(): + logging.debug("db.create_all") + db.create_all() diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..1bf23b6 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +# extra development dependencies +pypandoc>=0.8.2 +nose>=1.3.3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ebb7a64 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask-Babel>=0.9 +Flask-OAuth>=0.12 +facebook-sdk>=0.4.0 +google-api-python-client>=1.2 +httplib2>=0.8 +oauth2>=1.5.211 +requests>=2.2.1 diff --git a/setup.cfg b/setup.cfg index e67858b..57461fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,9 @@ [aliases] rc = egg_info --tag-date --tag-build=.rc sdist -rtm = egg_info --tag-date --tag-build=.rtm sdist +rtm = egg_info --tag-date --tag-build=.rtm bdist +[egg_info] +tag_date = true + +[wheel] +universal = 1 diff --git a/setup.py b/setup.py index e6bb152..551023e 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,23 @@ # coding=utf-8 # Copyright 2014 Janusz Skonieczny - +import sys import os from setuptools import setup, find_packages from pip.req import parse_requirements +import pypandoc -install_requires = parse_requirements(os.path.join(os.path.dirname(__file__), "requires.txt")) +ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) +SRC_DIR = os.path.join(ROOT_DIR, 'src') +sys.path.append(SRC_DIR) -import pypandoc +install_requires = parse_requirements(os.path.join(os.path.dirname(__file__), "requirements.txt")) long_description = pypandoc.convert('README.md', 'rst', format='md') +from flask_social_blueprint import __version__ as version + setup_kwargs = { 'name': "flask-social-blueprint", - 'version': "0.5.1", + 'version': version, 'packages': find_packages("src"), 'package_dir': {'': 'src'}, 'install_requires': [str(r.req) for r in install_requires], @@ -40,6 +45,7 @@ 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], + 'test_suite': 'flask_social_blueprint.tests.suite' } setup(**setup_kwargs) diff --git a/src/flask_social_blueprint/__init__.py b/src/flask_social_blueprint/__init__.py index 8b45ebc..561a3a6 100644 --- a/src/flask_social_blueprint/__init__.py +++ b/src/flask_social_blueprint/__init__.py @@ -1,115 +1,3 @@ # coding=utf-8 # Copyright 2013 Janusz Skonieczny -import logging -from flask import Blueprint, url_for, request, current_app -from flask_login import login_user, current_user -from flask_security.utils import do_flash -from flask_babel import gettext as _ - -from werkzeug.exceptions import abort -from werkzeug.local import LocalProxy -from werkzeug.utils import redirect - -logger = logging.getLogger("flask_social_blueprint") - - -class SocialBlueprint(Blueprint): - def __init__(self, name, import_name, connection_adapter=None, providers=None, *args, **kwargs): - super(SocialBlueprint, self).__init__(name, import_name, *args, **kwargs) - self.connection_adapter = connection_adapter - self.providers = providers or {} - - def get_provider(self, provider_name): - provider = self.providers[provider_name] - if not provider: - abort(404) - return provider - - def authorize(self, provider_name): - """ - Starts OAuth authorization flow, will redirect to 3rd party site. - """ - provider = self.get_provider(provider_name) - callback_url = url_for(".callback", provider=provider_name, _external=True) - return provider.authorize(callback_url) - - def callback(self, provider_name): - """ - Handles 3rd party callback and processes it's data - """ - provider = self.get_provider(provider_name) - return provider.authorized_handler(self.login)(provider=provider) - - def login(self, raw_data, provider): - logger.debug("raw_data: %s" % raw_data) - if not raw_data: - do_flash(_("OAuth authorization failed"), "danger") - abort(400) - profile = provider.get_profile(raw_data) - connection = self.connection_adapter.by_profile(profile) - if not connection: - return self.no_connection(profile, provider) - return self.login_connection(connection, profile, provider) - - def no_connection(self, profile, provider): - try: - connection = self.create_connection(profile, provider) - except Exception as ex: - logging.warn(ex, exc_info=True) - do_flash(_("Could not register: {}").format(ex.message), "warning") - return self.login_failed_redirect(profile, provider) - - return self.login_connection(connection, profile, provider) - - def login_connection(self, connection, profile, provider): - user = connection.get_user() - assert user, "Connection did not returned a User instance" - login_user(user) - return self.login_redirect(profile, provider) - - def login_redirect(self, profile, provider): - return redirect("/") - - def login_failed_redirect(self, profile, provider): - return redirect("/") - - def create_connection(self, profile, provider): - return self.connection_adapter.from_profile(current_user, profile) - - @classmethod - def create_bp(cls, name, connection_adapter, providers, *args, **kwargs): - bp = SocialBlueprint(name, __name__, connection_adapter, providers, *args, **kwargs) - bp.route('/login/')(login) - bp.route('/callback/')(callback) - return bp - - @classmethod - def setup_providers(cls, config): - providers = {} - for provider, provider_config in config.items(): - module_path, class_name = provider.rsplit('.', 1) - from importlib import import_module - module = import_module(module_path) - provider = getattr(module, class_name)(**provider_config) - providers[provider.name] = provider - return providers - - @classmethod - def init_bp(cls, app, connection_adapter, *args, **kwargs): - config = app.config.get("SOCIAL_BLUEPRINT") - providers = cls.setup_providers(config) - bp = cls.create_bp('social', connection_adapter, providers, *args, **kwargs) - app.register_blueprint(bp) - - -bp = LocalProxy(lambda: current_app.blueprints[request.blueprint]) - - -def login(provider): - return bp.authorize(provider) - - -def callback(provider): - return bp.callback(provider) - - +__version__ = '0.5.1' diff --git a/src/flask_social_blueprint/core.py b/src/flask_social_blueprint/core.py new file mode 100644 index 0000000..589b815 --- /dev/null +++ b/src/flask_social_blueprint/core.py @@ -0,0 +1,106 @@ +# coding=utf-8 +# Copyright 2013 Janusz Skonieczny +import logging +from flask import Blueprint, url_for, request, current_app +from flask_login import login_user, current_user +from flask_security.utils import do_flash +from flask_babel import gettext as _ + +from werkzeug.exceptions import abort +from werkzeug.local import LocalProxy +from werkzeug.utils import redirect + +logger = logging.getLogger("flask_social_blueprint") + + +class SocialBlueprint(Blueprint): + def __init__(self, name, import_name, connection_adapter=None, providers=None, *args, **kwargs): + super(SocialBlueprint, self).__init__(name, import_name, *args, **kwargs) + self.connection_adapter = connection_adapter + self.providers = providers or {} + + def get_provider(self, provider_name): + provider = self.providers[provider_name] + if not provider: + abort(404) + return provider + + def authenticate(self, provider): + """ + Starts OAuth authorization flow, will redirect to 3rd party site. + """ + callback_url = url_for(".callback", provider=provider, _external=True) + provider = self.get_provider(provider) + return provider.authorize(callback_url) + + def callback(self, provider): + """ + Handles 3rd party callback and processes it's data + """ + provider = self.get_provider(provider) + return provider.authorized_handler(self.login)(provider=provider) + + def login(self, raw_data, provider): + logger.debug("raw_data: %s" % raw_data) + if not raw_data: + do_flash(_("OAuth authorization failed"), "danger") + abort(400) + profile = provider.get_profile(raw_data) + connection = self.connection_adapter.by_profile(profile) + if not connection: + return self.no_connection(profile, provider) + return self.login_connection(connection, profile, provider) + + def no_connection(self, profile, provider): + try: + connection = self.create_connection(profile, provider) + except Exception as ex: + logging.warn(ex, exc_info=True) + do_flash(_("Could not register: {}").format(ex.message), "warning") + return self.login_failed_redirect(profile, provider) + + return self.login_connection(connection, profile, provider) + + def login_connection(self, connection, profile, provider): + user = connection.get_user() + assert user, "Connection did not returned a User instance" + login_user(user) + return self.login_redirect(profile, provider) + + def login_redirect(self, profile, provider): + return redirect("/") + + def login_failed_redirect(self, profile, provider): + return redirect("/") + + def create_connection(self, profile, provider): + return self.connection_adapter.from_profile(current_user, profile) + + @classmethod + def create_bp(cls, name, connection_adapter, providers, *args, **kwargs): + bp = SocialBlueprint(name, __name__, connection_adapter, providers, *args, **kwargs) + bp.route('/login/', endpoint="login")(bp.authenticate) + bp.route('/callback/', endpoint="callback")(bp.callback) + return bp + + @classmethod + def setup_providers(cls, config): + providers = {} + for provider, provider_config in config.items(): + module_path, class_name = provider.rsplit('.', 1) + from importlib import import_module + module = import_module(module_path) + provider = getattr(module, class_name)(**provider_config) + providers[provider.name] = provider + return providers + + @classmethod + def init_bp(cls, app, connection_adapter, *args, **kwargs): + config = app.config.get("SOCIAL_BLUEPRINT") + providers = cls.setup_providers(config) + bp = cls.create_bp('social', connection_adapter, providers, *args, **kwargs) + app.register_blueprint(bp) + + +bp = LocalProxy(lambda: current_app.blueprints[request.blueprint]) + diff --git a/src/flask_social_blueprint/tests.py b/src/flask_social_blueprint/tests.py new file mode 100644 index 0000000..2ff83d6 --- /dev/null +++ b/src/flask_social_blueprint/tests.py @@ -0,0 +1,12 @@ +# coding=utf-8 +# Created 2014 by Janusz Skonieczny +import os +import tempfile +import unittest +import flask_social_blueprint +import main + +from main import app + +class TestFlaskSocialBlueprint(unittest.TestCase): + pass