Your password has been changed.
+{% if security.recoverable %} +If you did not change your password, click here to reset it.
+{% endif %} diff --git a/flask_security/templates/security/email/change_notice.txt b/flask_security/templates/security/email/change_notice.txt new file mode 100644 index 0000000..e74bd80 --- /dev/null +++ b/flask_security/templates/security/email/change_notice.txt @@ -0,0 +1,5 @@ +Your password has been changed +{% if security.recoverable %} +If you did not change your password, click the link below to reset it. +{{ url_for_security('forgot_password', _external=True) }} +{% endif %} diff --git a/flask_security/utils.py b/flask_security/utils.py index 91bd65f..66d0f57 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -27,7 +27,7 @@ from werkzeug.local import LocalProxy from .signals import user_registered, user_confirmed, \ confirm_instructions_sent, login_instructions_sent, \ - password_reset, reset_password_instructions_sent + password_reset, password_changed, reset_password_instructions_sent # Convenient references _security = LocalProxy(lambda: current_app.extensions['security']) @@ -373,6 +373,7 @@ def capture_signals(): """Factory method that creates a `CaptureSignals` with all the flask_security signals.""" return CaptureSignals([user_registered, user_confirmed, confirm_instructions_sent, login_instructions_sent, - password_reset, reset_password_instructions_sent]) + password_reset, password_changed, + reset_password_instructions_sent]) diff --git a/flask_security/views.py b/flask_security/views.py index f09d8b5..b67cfc7 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -11,6 +11,7 @@ from flask import current_app, redirect, request, render_template, jsonify, \ after_this_request, Blueprint +from flask_login import current_user from werkzeug.datastructures import MultiDict from werkzeug.local import LocalProxy @@ -21,6 +22,7 @@ from .passwordless import send_login_instructions, \ login_token_status from .recoverable import reset_password_token_status, \ send_reset_password_instructions, update_password +from .changeable import change_user_password from .registerable import register_user from .utils import get_url, get_post_login_redirect, do_flash, \ get_message, login_user, logout_user, url_for_security as url_for, \ @@ -279,6 +281,33 @@ def reset_password(token): **_ctx('reset_password')) +@login_required +def change_password(): + """View function which handles a change password request.""" + + form_class = _security.change_password_form + + if request.json: + form = form_class(MultiDict(request.json)) + else: + form = form_class() + + if form.validate_on_submit(): + after_this_request(_commit) + change_user_password(current_user, form.new_password.data) + if request.json is None: + do_flash(*get_message('PASSWORD_CHANGE')) + return redirect(get_url(_security.post_change_view) or + get_url(_security.post_login_view)) + + if request.json: + return _render_json(form) + + return render_template('security/change_password.html', + change_password_form=form, + **_ctx('change_password')) + + def create_blueprint(state, import_name): """Creates the security extension blueprint""" @@ -312,6 +341,11 @@ def create_blueprint(state, import_name): methods=['GET', 'POST'], endpoint='reset_password')(reset_password) + if state.changeable: + bp.route(state.change_url, + methods=['GET', 'POST'], + endpoint='change_password')(change_password) + if state.confirmable: bp.route(state.confirm_url, methods=['GET', 'POST'], diff --git a/requirements.txt b/requirements.txt index 8307c2d..bc669f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ Flask>=0.9 -Flask-Login==0.1.3 +Flask-Login>=0.1.3 Flask-Mail>=0.7.3 Flask-Principal>=0.3.3 -Flask-Script==0.5.3 Flask-WTF>=0.8 itsdangerous>=0.17 -passlib==1.6.1 +passlib>=1.6.1 \ No newline at end of file diff --git a/setup.py b/setup.py index ea11896..580afa2 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ setup( 'nose', 'Flask-SQLAlchemy', 'Flask-MongoEngine', + 'Flask-Peewee', 'py-bcrypt', 'simplejson' ], diff --git a/tests/__init__.py b/tests/__init__.py index 53e0d9b..35ceb79 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,8 +1,13 @@ # -*- coding: utf-8 -*- +import hmac + +from hashlib import sha1 from unittest import TestCase + from tests.test_app.sqlalchemy import create_app + class SecurityTest(TestCase): APP_KWARGS = { @@ -21,6 +26,13 @@ class SecurityTest(TestCase): self.app = app self.client = app.test_client() + with self.client.session_transaction() as session: + session['csrf'] = 'csrf_token' + + csrf_hmac = hmac.new(self.app.config['SECRET_KEY'], + 'csrf_token'.encode('utf8'), digestmod=sha1) + self.csrf_token = '##' + csrf_hmac.hexdigest() + def _create_app(self, auth_config, **kwargs): return create_app(auth_config, **kwargs) @@ -30,30 +42,30 @@ class SecurityTest(TestCase): headers=headers) def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None): + if isinstance(data, dict): + data['csrf_token'] = self.csrf_token + return self.client.post(route, data=data, follow_redirects=follow_redirects, content_type=content_type or 'application/x-www-form-urlencoded', headers=headers) def register(self, email, password='password'): - data = dict(email=email, password=password) + data = dict(email=email, password=password, csrf_token=self.csrf_token) return self.client.post('/register', data=data, follow_redirects=True) def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs): data = dict(email=email, password=password, remember='y') - r = self._post(endpoint or '/login', data=data, **kwargs) - return r + return self._post(endpoint or '/login', data=data, **kwargs) def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None): - data = """ -{ - "email": "%s", - "password": "%s" -} -""" - return self._post(endpoint or '/login', - content_type="application/json", - data=data % (email, password)) + data = """{ + "email": "%s", + "password": "%s", + "csrf_token": "%s" + }""" + return self._post(endpoint or '/login', content_type="application/json", + data=data % (email, password, self.csrf_token)) def logout(self, endpoint=None): return self._get(endpoint or '/logout', follow_redirects=True) diff --git a/tests/configured_tests.py b/tests/configured_tests.py index 41c34d4..0ac4149 100644 --- a/tests/configured_tests.py +++ b/tests/configured_tests.py @@ -67,7 +67,7 @@ class ConfiguredSecurityTests(SecurityTest): self.assertIn('Post Register', r.data) def test_register_json(self): - data = '{ "email": "dude@lp.com", "password": "password" }' + data = '{ "email": "dude@lp.com", "password": "password", "csrf_token":"%s" }' % self.csrf_token r = self._post('/register', data=data, content_type='application/json') data = json.loads(r.data) self.assertEquals(data['meta']['code'], 200) @@ -145,9 +145,10 @@ class RecoverableTemplatePathTests(SecurityTest): def test_reset_password_template(self): with capture_reset_password_requests() as requests: - r = self.client.post('/reset', + r = self._post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True) + t = requests[0]['token'] r = self._get('/reset/' + t) @@ -189,7 +190,7 @@ class RegisterableTests(SecurityTest): data = dict(email='dude@lp.com', password='password', password_confirm='password') - self.client.post('/register', data=data, follow_redirects=True) + self._post('/register', data=data, follow_redirects=True) r = self.authenticate('dude@lp.com') self.assertIn('Hello dude@lp.com', r.data) @@ -217,7 +218,7 @@ class ConfirmableTests(SecurityTest): self.client.get('/confirm/' + token, follow_redirects=True) self.logout() - r = self.client.post('/confirm', data=dict(email=e)) + r = self._post('/confirm', data=dict(email=e)) self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data) def test_register_sends_confirmation_email(self): @@ -303,7 +304,7 @@ class LoginWithoutImmediateConfirmTests(SecurityTest): e = 'dude@lp.com' p = 'password' data = dict(email=e, password=p, password_confirm=p) - r = self.client.post('/register', data=data, follow_redirects=True) + r = self._post('/register', data=data, follow_redirects=True) self.assertIn(e, r.data) @@ -317,7 +318,7 @@ class RecoverableTests(SecurityTest): def test_reset_view(self): with capture_reset_password_requests() as requests: - r = self.client.post('/reset', + r = self._post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True) t = requests[0]['token'] @@ -327,23 +328,23 @@ class RecoverableTests(SecurityTest): def test_forgot_post_sends_email(self): with capture_reset_password_requests(): with self.app.extensions['mail'].record_messages() as outbox: - self.client.post('/reset', data=dict(email='joe@lp.com')) + self._post('/reset', data=dict(email='joe@lp.com')) self.assertEqual(len(outbox), 1) def test_forgot_password_json(self): - r = self.client.post('/reset', data='{"email": "matt@lp.com"}', + r = self._post('/reset', data='{"email": "matt@lp.com"}', content_type="application/json") self.assertEquals(r.status_code, 200) def test_forgot_password_invalid_email(self): - r = self.client.post('/reset', + r = self._post('/reset', data=dict(email='larry@lp.com'), follow_redirects=True) self.assertIn("Specified user does not exist", r.data) def test_reset_password_with_valid_token(self): with capture_reset_password_requests() as requests: - r = self.client.post('/reset', + r = self._post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True) t = requests[0]['token'] @@ -375,14 +376,13 @@ class ExpiredResetPasswordTest(SecurityTest): def test_reset_password_with_expired_token(self): with capture_reset_password_requests() as requests: - r = self.client.post('/reset', - data=dict(email='joe@lp.com'), - follow_redirects=True) + r = self._post('/reset', data=dict(email='joe@lp.com'), + follow_redirects=True) t = requests[0]['token'] time.sleep(1) - r = self.client.post('/reset/' + t, data={ + r = self._post('/reset/' + t, data={ 'password': 'newpassword', 'password_confirm': 'newpassword' }, follow_redirects=True) @@ -390,6 +390,95 @@ class ExpiredResetPasswordTest(SecurityTest): self.assertIn('You did not reset your password within', r.data) +class ChangePasswordTest(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_RECOVERABLE': True, + 'SECURITY_CHANGEABLE': True, + } + + def test_change_password(self): + self.authenticate() + r = self.client.get('/change', follow_redirects=True) + self.assertIn('Change password', r.data) + + def test_change_password_invalid(self): + self.authenticate() + r = self._post('/change', data={ + 'password': 'notpassword', + 'new_password': 'newpassword', + 'new_password_confirm': 'newpassword' + }, follow_redirects=True) + self.assertNotIn('You successfully changed your password', r.data) + self.assertIn('Invalid password', r.data) + + def test_change_password_mismatch(self): + self.authenticate() + r = self._post('/change', data={ + 'password': 'password', + 'new_password': 'newpassword', + 'new_password_confirm': 'notnewpassword' + }, follow_redirects=True) + self.assertNotIn('You successfully changed your password', r.data) + self.assertIn('Passwords do not match', r.data) + + def test_change_password_bad_password(self): + self.authenticate() + r = self._post('/change', data={ + 'password': 'password', + 'new_password': 'a', + 'new_password_confirm': 'a' + }, follow_redirects=True) + self.assertNotIn('You successfully changed your password', r.data) + self.assertIn('Field must be between', r.data) + + def test_change_password_success(self): + self.authenticate() + with self.app.extensions['mail'].record_messages() as outbox: + r = self._post('/change', data={ + 'password': 'password', + 'new_password': 'newpassword', + 'new_password_confirm': 'newpassword' + }, follow_redirects=True) + + self.assertIn('You successfully changed your password', r.data) + self.assertIn('Home Page', r.data) + + self.assertEqual(len(outbox), 1) + self.assertIn("Your password has been changed", outbox[0].html) + self.assertIn("/reset", outbox[0].html) + + +class ChangePasswordPostViewTest(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_CHANGEABLE': True, + 'SECURITY_POST_CHANGE_VIEW': '/profile', + } + + def test_change_password_success(self): + self.authenticate() + r = self._post('/change', data={ + 'password': 'password', + 'new_password': 'newpassword', + 'new_password_confirm': 'newpassword' + }, follow_redirects=True) + + self.assertIn('Profile Page', r.data) + + +class ChangePasswordDisabledTest(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_CHANGEABLE': False, + } + + def test_change_password_endpoint_is_404(self): + self.authenticate() + r = self.client.get('/change', follow_redirects=True) + self.assertEqual(404, r.status_code) + + class TrackableTests(SecurityTest): AUTH_CONFIG = { @@ -420,20 +509,19 @@ class PasswordlessTests(SecurityTest): def test_login_request_for_inactive_user(self): msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0] - r = self.client.post('/login', - data=dict(email='tiya@lp.com'), - follow_redirects=True) + r = self._post('/login', data=dict(email='tiya@lp.com'), + follow_redirects=True) self.assertIn(msg, r.data) def test_request_login_token_with_json_and_valid_email(self): - data = '{"email": "matt@lp.com", "password": "password"}' - r = self.client.post('/login', data=data, content_type='application/json') + data = '{"email": "matt@lp.com", "password": "password", "csrf_token":"%s"}' % self.csrf_token + r = self._post('/login', data=data, content_type='application/json') self.assertEquals(r.status_code, 200) self.assertNotIn('error', r.data) def test_request_login_token_with_json_and_invalid_email(self): data = '{"email": "nobody@lp.com", "password": "password"}' - r = self.client.post('/login', data=data, content_type='application/json') + r = self._post('/login', data=data, content_type='application/json') self.assertIn('errors', r.data) def test_request_login_token_sends_email_and_can_login(self): @@ -442,9 +530,8 @@ class PasswordlessTests(SecurityTest): with capture_passwordless_login_requests() as requests: with self.app.extensions['mail'].record_messages() as outbox: - r = self.client.post('/login', - data=dict(email=e), - follow_redirects=True) + r = self._post('/login', data=dict(email=e), + follow_redirects=True) self.assertEqual(len(outbox), 1) @@ -473,9 +560,8 @@ class PasswordlessTests(SecurityTest): def test_token_login_when_already_authenticated(self): with capture_passwordless_login_requests() as requests: - self.client.post('/login', - data=dict(email='matt@lp.com'), - follow_redirects=True) + self._post('/login', data=dict(email='matt@lp.com'), + follow_redirects=True) token = requests[0]['login_token'] r = self.client.get('/login/' + token, follow_redirects=True) @@ -503,9 +589,7 @@ class ExpiredLoginTokenTests(SecurityTest): e = 'matt@lp.com' with capture_passwordless_login_requests() as requests: - self.client.post('/login', - data=dict(email=e), - follow_redirects=True) + self._post('/login', data=dict(email=e), follow_redirects=True) token = requests[0]['login_token'] time.sleep(1.25) @@ -539,7 +623,7 @@ class AsyncMailTaskTests(SecurityTest): def send_email(msg): self.mail_sent = True - self.client.post('/reset', data=dict(email='matt@lp.com')) + self._post('/reset', data=dict(email='matt@lp.com')) self.assertTrue(self.mail_sent) @@ -614,9 +698,8 @@ class RecoverableExtendFormsTest(SecurityTest): def test_reset_password(self): with capture_reset_password_requests() as requests: - self.client.post('/reset', - data=dict(email='joe@lp.com'), - follow_redirects=True) + self._post('/reset', data=dict(email='joe@lp.com'), + follow_redirects=True) token = requests[0]['token'] r = self._get('/reset/' + token) self.assertIn("My Reset Password Submit Field", r.data) diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 8614ef0..2438282 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -35,23 +35,23 @@ class DefaultSecurityTests(SecurityTest): def test_unprovided_username(self): r = self.authenticate("") - self.assertIn("Email not provided", r.data) + self.assertIn(self.get_message('EMAIL_NOT_PROVIDED'), r.data) def test_unprovided_password(self): r = self.authenticate(password="") - self.assertIn("Password not provided", r.data) + self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED'), r.data) def test_invalid_user(self): r = self.authenticate(email="bogus@bogus.com") - self.assertIn("Specified user does not exist", r.data) + self.assertIn(self.get_message('USER_DOES_NOT_EXIST'), r.data) def test_bad_password(self): r = self.authenticate(password="bogus") - self.assertIn("Invalid password", r.data) + self.assertIn(self.get_message('INVALID_PASSWORD'), r.data) def test_inactive_user(self): r = self.authenticate("tiya@lp.com", "password") - self.assertIn("Account is disabled", r.data) + self.assertIn(self.get_message('DISABLED_ACCOUNT'), r.data) def test_logout(self): self.authenticate() @@ -169,6 +169,24 @@ class DefaultSecurityTests(SecurityTest): self.assertEquals('Basic realm="My Realm"', r.headers['WWW-Authenticate']) + def test_multi_auth_basic(self): + r = self._get('/multi_auth', headers={ + 'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:password") + }) + self.assertIn('Basic', r.data) + + def test_multi_auth_token(self): + r = self.json_authenticate() + data = json.loads(r.data) + token = data['response']['user']['authentication_token'] + r = self._get('/multi_auth?auth_token=' + token) + self.assertIn('Token', r.data) + + def test_multi_auth_session(self): + self.authenticate() + r = self._get('/multi_auth') + self.assertIn('Session', r.data) + def test_user_deleted_during_session_reverts_to_anonymous_user(self): self.authenticate() @@ -206,6 +224,13 @@ class MongoEngineSecurityTests(DefaultSecurityTests): return create_app(auth_config, **kwargs) +# class PeeweeSecurityTests(DefaultSecurityTests): + +# def _create_app(self, auth_config, **kwargs): +# from tests.test_app.peewee_app import create_app +# return create_app(auth_config, **kwargs) + + class DefaultDatastoreTests(SecurityTest): def test_add_role_to_user(self): diff --git a/tests/signals_tests.py b/tests/signals_tests.py index 51fde0f..9e7359f 100644 --- a/tests/signals_tests.py +++ b/tests/signals_tests.py @@ -3,7 +3,8 @@ from __future__ import with_statement from flask_security.utils import (capture_registrations, capture_reset_password_requests, capture_signals) from flask_security.signals import (user_registered, user_confirmed, confirm_instructions_sent, login_instructions_sent, - password_reset, reset_password_instructions_sent) + password_reset, password_changed, + reset_password_instructions_sent) from tests import SecurityTest @@ -32,7 +33,7 @@ class RegisterableSignalsTests(SecurityTest): self.assertIn('confirm_token', args[0]) self.assertEqual(kwargs['app'], self.app) - def test_register(self): + def test_register_without_password(self): e = 'dude@lp.com' with capture_signals() as mocks: self.register(e, password='') @@ -111,9 +112,8 @@ class RecoverableSignalsTests(SecurityTest): def test_reset_password_request(self): with capture_signals() as mocks: - self.client.post('/reset', - data=dict(email='joe@lp.com'), - follow_redirects=True) + self._post('/reset', data=dict(email='joe@lp.com'), + follow_redirects=True) self.assertEqual(mocks.signals_sent(), set([reset_password_instructions_sent])) user = self.app.security.datastore.find_user(email='joe@lp.com') calls = mocks[reset_password_instructions_sent] @@ -125,15 +125,12 @@ class RecoverableSignalsTests(SecurityTest): def test_reset_password(self): with capture_reset_password_requests() as requests: - self.client.post('/reset', - data=dict(email='joe@lp.com'), - follow_redirects=True) + self._post('/reset', data=dict(email='joe@lp.com'), + follow_redirects=True) token = requests[0]['token'] with capture_signals() as mocks: - self.client.post('/reset/' + token, - data=dict(password='newpassword', - password_confirm='newpassword'), - follow_redirects=True) + data = dict(password='newpassword', password_confirm='newpassword') + self._post('/reset/' + token, data, follow_redirects=True) self.assertEqual(mocks.signals_sent(), set([password_reset])) user = self.app.security.datastore.find_user(email='joe@lp.com') calls = mocks[password_reset] @@ -144,16 +141,64 @@ class RecoverableSignalsTests(SecurityTest): def test_reset_password_invalid_emails(self): with capture_signals() as mocks: - self.client.post('/reset', - data=dict(email='nobody@lp.com'), - follow_redirects=True) + self._post('/reset', data=dict(email='nobody@lp.com'), + follow_redirects=True) self.assertEqual(mocks.signals_sent(), set()) def test_reset_password_invalid_token(self): with capture_signals() as mocks: - self.client.post('/reset/bogus', - data=dict(password='newpassword', - password_confirm='newpassword'), + data = dict(password='newpassword', password_confirm='newpassword') + self._post('/reset/bogus', data, follow_redirects=True) + self.assertEqual(mocks.signals_sent(), set()) + + +class ChangeableSignalsTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_CHANGEABLE': True, + } + + def test_change_password(self): + self.authenticate('joe@lp.com') + with capture_signals() as mocks: + with self.client as client: + client.post('/change', + data=dict(password='password', + new_password='newpassword', + new_password_confirm='newpassword', + csrf_token=self.csrf_token)) + self.assertEqual(mocks.signals_sent(), set([password_changed])) + user = self.app.security.datastore.find_user(email='joe@lp.com') + calls = mocks[password_changed] + self.assertEqual(len(calls), 1) + args, kwargs = calls[0] + self.assertTrue(compare_user(args[0], user)) + self.assertEqual(kwargs['app'], self.app) + + def test_change_password_invalid_password(self): + with capture_signals() as mocks: + self.client.post('/change', + data=dict(password='notpassword', + new_password='newpassword', + new_password_confirm='newpassword'), + follow_redirects=True) + self.assertEqual(mocks.signals_sent(), set()) + + def test_change_password_bad_password(self): + with capture_signals() as mocks: + self.client.post('/change', + data=dict(password='notpassword', + new_password='a', + new_password_confirm='a'), + follow_redirects=True) + self.assertEqual(mocks.signals_sent(), set()) + + def test_change_password_mismatch_password(self): + with capture_signals() as mocks: + self.client.post('/change', + data=dict(password='password', + new_password='newpassword', + new_password_confirm='notnewpassword'), follow_redirects=True) self.assertEqual(mocks.signals_sent(), set()) @@ -166,25 +211,22 @@ class PasswordlessTests(SecurityTest): def test_login_request_for_inactive_user(self): with capture_signals() as mocks: - self.client.post('/login', - data=dict(email='tiya@lp.com'), - follow_redirects=True) + self._post('/login', data=dict(email='tiya@lp.com'), + follow_redirects=True) self.assertEqual(mocks.signals_sent(), set()) def test_login_request_for_invalid_email(self): with capture_signals() as mocks: - self.client.post('/login', - data=dict(email='nobody@lp.com'), - follow_redirects=True) + self._post('/login', data=dict(email='nobody@lp.com'), + follow_redirects=True) self.assertEqual(mocks.signals_sent(), set()) def test_request_login_token_sends_email_and_can_login(self): e = 'matt@lp.com' with capture_signals() as mocks: - self.client.post('/login', - data=dict(email=e), - follow_redirects=True) + self._post('/login', data=dict(email=e), follow_redirects=True) + self.assertEqual(mocks.signals_sent(), set([login_instructions_sent])) user = self.app.security.datastore.find_user(email='matt@lp.com') calls = mocks[login_instructions_sent] @@ -193,4 +235,3 @@ class PasswordlessTests(SecurityTest): self.assertTrue(compare_user(args[0]['user'], user)) self.assertIn('login_token', args[0]) self.assertEqual(kwargs['app'], self.app) - diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py index 5cad502..863db79 100644 --- a/tests/test_app/__init__.py +++ b/tests/test_app/__init__.py @@ -4,7 +4,7 @@ from flask import Flask, render_template, current_app from flask.ext.mail import Mail from flask.ext.security import login_required, roles_required, roles_accepted from flask.ext.security.decorators import http_auth_required, \ - auth_token_required + auth_token_required, auth_required from flask.ext.security.utils import encrypt_password from werkzeug.local import LocalProxy @@ -50,6 +50,11 @@ def create_app(config): def token(): return render_template('index.html', content='Token Authentication') + @app.route('/multi_auth') + @auth_required('session', 'token', 'basic') + def multi_auth(): + return render_template('index.html', content='Session, Token, Basic auth') + @app.route('/post_logout') def post_logout(): return render_template('index.html', content='Post Logout') diff --git a/tests/test_app/peewee_app.py b/tests/test_app/peewee_app.py new file mode 100644 index 0000000..ae404a2 --- /dev/null +++ b/tests/test_app/peewee_app.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +import sys +import os + +sys.path.pop(0) +sys.path.insert(0, os.getcwd()) + +from flask_peewee.db import Database +from peewee import * +from flask.ext.security import Security, UserMixin, RoleMixin, \ + PeeweeUserDatastore + +from tests.test_app import create_app as create_base_app, populate_data, \ + add_context_processors + +def create_app(config, **kwargs): + app = create_base_app(config) + app.config['DATABASE'] = { + 'name': 'example2.db', + 'engine': 'peewee.SqliteDatabase', + } + db = Database(app) + + class Role(db.Model, RoleMixin): + name = TextField(unique=True) + description = TextField(null=True) + + class User(db.Model, UserMixin): + email = TextField() + password = TextField() + last_login_at = DateTimeField(null=True) + current_login_at = DateTimeField(null=True) + last_login_ip = TextField(null=True) + current_login_ip = TextField(null=True) + login_count = IntegerField(null=True) + active = BooleanField(default=True) + confirmed_at = DateTimeField(null=True) + + class UserRoles(db.Model): + """ Peewee does not have built-in many-to-many support, so we have to + create this mapping class to link users to roles.""" + user = ForeignKeyField(User, related_name='roles') + role = ForeignKeyField(Role, related_name='users') + name = property(lambda self: self.role.name) + description = property(lambda self: self.role.description) + + @app.before_first_request + def before_first_request(): + for Model in (Role, User, UserRoles): + Model.drop_table(fail_silently=True) + Model.create_table(fail_silently=True) + populate_data(app.config.get('USER_COUNT', None)) + + app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs) + + add_context_processors(app.security) + + return app + +if __name__ == '__main__': + create_app({}).run() diff --git a/tests/unit_tests.py b/tests/unit_tests.py index d08db38..41e4554 100644 --- a/tests/unit_tests.py +++ b/tests/unit_tests.py @@ -55,8 +55,8 @@ class DatastoreTests(unittest.TestCase): self.assertRaises(NotImplementedError, ds.delete, None) def test_unimplemented_user_datastore_methods(self): - self.assertRaises(NotImplementedError, self.ds.find_user) - self.assertRaises(NotImplementedError, self.ds.find_role) + self.assertRaises(NotImplementedError, self.ds.find_user, None) + self.assertRaises(NotImplementedError, self.ds.find_role, None) def test_toggle_active(self): user.active = True