From a269930ec34c3b6f8a34b9167cacd7650b20b908 Mon Sep 17 00:00:00 2001 From: Matt Wright Date: Wed, 10 Oct 2012 13:36:59 -0400 Subject: [PATCH] Update docs and tests --- .travis.yml | 4 +- artwork/logo-helmet.svg | 297 +++++++++++++++++++ docs/_static/logo-full.png | Bin 0 -> 10690 bytes docs/_static/logo-helmet.png | Bin 0 -> 10017 bytes docs/_templates/sidebarintro.html | 17 ++ docs/_templates/sidebarlogo.html | 3 + docs/api.rst | 21 -- docs/conf.py | 8 +- docs/contents.rst.inc | 1 - docs/index.rst | 31 +- docs/overview.rst | 33 --- setup.cfg | 2 +- setup.py | 5 +- tests/configured_tests.py | 463 ++++++++++++++++++++++++++++++ tests/functional_tests.py | 445 +--------------------------- 15 files changed, 824 insertions(+), 506 deletions(-) create mode 100644 artwork/logo-helmet.svg create mode 100644 docs/_static/logo-full.png create mode 100644 docs/_static/logo-helmet.png create mode 100644 docs/_templates/sidebarintro.html create mode 100644 docs/_templates/sidebarlogo.html delete mode 100644 docs/overview.rst create mode 100644 tests/configured_tests.py diff --git a/.travis.yml b/.travis.yml index 4112361..42c3b47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ python: install: - pip install . --quiet --use-mirrors - - "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib simplejson --quiet --use-mirrors; fi" - - pip install nose Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors + - "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib --quiet --use-mirrors; fi" + - pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors before_script: - mysql -e 'create database flask_security_test;' diff --git a/artwork/logo-helmet.svg b/artwork/logo-helmet.svg new file mode 100644 index 0000000..2799f1f --- /dev/null +++ b/artwork/logo-helmet.svg @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo-full.png b/docs/_static/logo-full.png new file mode 100644 index 0000000000000000000000000000000000000000..724fd06c3e00b655487ca55746503a6b284b3874 GIT binary patch literal 10690 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 NF*Gx%(7zq?e*joOt6l&A literal 0 HcmV?d00001 diff --git a/docs/_static/logo-helmet.png b/docs/_static/logo-helmet.png new file mode 100644 index 0000000000000000000000000000000000000000..b4904186195d7f503f77204537f398dd108be6bd GIT binary patch literal 10017 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 A4gdfE literal 0 HcmV?d00001 diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html new file mode 100644 index 0000000..f65d1c7 --- /dev/null +++ b/docs/_templates/sidebarintro.html @@ -0,0 +1,17 @@ +

About

+

+ Flask-Security is an opinionated Flask extension which adds basic + security and authentication features to your Flask apps quickly + and easily. Flask-Social can also be used to add "social" or OAuth + login and connection management. +

+

Useful Links

+ + \ No newline at end of file diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html new file mode 100644 index 0000000..2686677 --- /dev/null +++ b/docs/_templates/sidebarlogo.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst index ce4d0d8..efad2ef 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -50,27 +50,6 @@ Datastores :inherited-members: -Exceptions ----------- -.. autoexception:: flask_security.exceptions.BadCredentialsError - -.. autoexception:: flask_security.exceptions.AuthenticationError - -.. autoexception:: flask_security.exceptions.UserNotFoundError - -.. autoexception:: flask_security.exceptions.RoleNotFoundError - -.. autoexception:: flask_security.exceptions.UserDatastoreError - -.. autoexception:: flask_security.exceptions.UserCreationError - -.. autoexception:: flask_security.exceptions.RoleCreationError - -.. autoexception:: flask_security.exceptions.ConfirmationError - -.. autoexception:: flask_security.exceptions.ResetPasswordError - - Signals ------- See the documentation for the signals provided by the Flask-Login and diff --git a/docs/conf.py b/docs/conf.py index 64f49a5..4432360 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -100,6 +100,8 @@ html_theme = 'flask' html_theme_options = { #'github_fork': 'mattupstate/flask-security', #'index_logo': False + 'touch_icon': 'touch-icon.png', + 'index_logo': 'logo-full.png' } # Add any paths that contain custom themes here, relative to this directory. @@ -135,7 +137,11 @@ html_static_path = ['_static'] #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +html_sidebars = { + 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], + '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', + 'sourcelink.html', 'searchbox.html'] +} # Additional templates that should be rendered to pages, maps page names to # template names. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 45ff59e..c905e06 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -4,7 +4,6 @@ Contents .. toctree:: :maxdepth: 1 - overview features configuration quickstart diff --git a/docs/index.rst b/docs/index.rst index 32784f3..97713c1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,36 @@ Flask-Security ============== -Flask-Security quickly adds security features to your Flask application. +Flask-Security allows you to quickly add common security mechanisms to your +Flask application. They include: + +1. Session based authentication +2. Role management +3. Password encryption +4. Basic HTTP authentication +5. Token based authentication +6. Token based account activation (optional) +7. Token based password recovery/resetting (optional) +8. User registration (optional) +9. Login tracking (optional) + +Many of these features are made possible by integrating various Flask extensions +and libraries. They include: + +1. `Flask-Login `_ +2. `Flask-Mail `_ +3. `Flask-Principal `_ +4. `Flask-Script `_ +5. `Flask-WTF `_ +6. `itsdangerous `_ +7. `passlib `_ + +Additionally, it assumes you'll be using a common library for your database +connections and model definitions. Flask-Security supports the following Flask +extensions out of the box for data persistance: + +1. `Flask-SQLAlchemy `_ +2. `Flask-MongoEngine `_ .. include:: contents.rst.inc \ No newline at end of file diff --git a/docs/overview.rst b/docs/overview.rst deleted file mode 100644 index 571a9af..0000000 --- a/docs/overview.rst +++ /dev/null @@ -1,33 +0,0 @@ -Overview -======== - -Flask-Security allows you to quickly add common security mechanisms to your -Flask application. They include: - -1. Session based authentication -2. Role management -3. Password encryption -4. Basic HTTP authentication -5. Token based authentication -6. Token based account activation (optional) -7. Token based password recovery/resetting (optional) -8. User registration (optional) -9. Login tracking (optional) - -Many of these features are made possible by integrating various Flask extensions -and libraries. They include: - -1. `Flask-Login `_ -2. `Flask-Mail `_ -3. `Flask-Principal `_ -4. `Flask-Script `_ -5. `Flask-WTF `_ -6. `itsdangerous `_ -7. `passlib `_ - -Additionally, it assumes you'll be using a common library for your database -connections and model definitions. Flask-Security supports the following Flask -extensions out of the box for data persistance: - -1. `Flask-SQLAlchemy `_ -2. `Flask-MongoEngine `_ \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 736bfed..4a53949 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,4 +3,4 @@ source-dir = docs/ build-dir = docs/_build [upload_sphinx] -upload-dir = docs/_build/html \ No newline at end of file +upload-dir = docs/_build \ No newline at end of file diff --git a/setup.py b/setup.py index 45fa170..6c423a4 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( install_requires=[ 'Flask>=0.9', 'Flask-Login>=0.1.3', - 'Flask-Mail>=0.6.1', + 'Flask-Mail>=0.7.0', 'Flask-Principal>=0.3', 'Flask-WTF>=0.5.4', 'itsdangerous>=0.15', @@ -47,7 +47,8 @@ setup( 'nose', 'Flask-SQLAlchemy', 'Flask-MongoEngine', - 'py-bcrypt' + 'py-bcrypt', + 'simplejson' ], classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tests/configured_tests.py b/tests/configured_tests.py new file mode 100644 index 0000000..0ac9d6f --- /dev/null +++ b/tests/configured_tests.py @@ -0,0 +1,463 @@ +from __future__ import with_statement + +import base64 +import time +import simplejson as json + +from flask.ext.security.utils import capture_registrations, \ + capture_reset_password_requests, capture_passwordless_login_requests + +from tests import SecurityTest + + +class ConfiguredPasswordHashSecurityTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_PASSWORD_HASH': 'bcrypt', + 'SECURITY_PASSWORD_SALT': 'so-salty', + 'USER_COUNT': 1 + } + + def test_authenticate(self): + r = self.authenticate(endpoint="/login") + self.assertIn('Home Page', r.data) + + +class ConfiguredSecurityTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_REGISTERABLE': True, + 'SECURITY_LOGOUT_URL': '/custom_logout', + 'SECURITY_LOGIN_URL': '/custom_login', + 'SECURITY_POST_LOGIN_VIEW': '/post_login', + 'SECURITY_POST_LOGOUT_VIEW': '/post_logout', + 'SECURITY_POST_REGISTER_VIEW': '/post_register', + 'SECURITY_UNAUTHORIZED_VIEW': '/unauthorized', + 'SECURITY_DEFAULT_HTTP_AUTH_REALM': 'Custom Realm' + } + + def test_login_view(self): + r = self._get('/custom_login') + self.assertIn("

Login

", r.data) + + def test_authenticate(self): + r = self.authenticate(endpoint="/custom_login") + self.assertIn('Post Login', r.data) + + def test_logout(self): + self.authenticate(endpoint="/custom_login") + r = self.logout(endpoint="/custom_logout") + self.assertIn('Post Logout', r.data) + + def test_register_view(self): + r = self._get('/register') + self.assertIn('

Register

', r.data) + + def test_register(self): + data = dict(email='dude@lp.com', + password='password', + password_confirm='password') + + r = self._post('/register', data=data, follow_redirects=True) + self.assertIn('Post Register', r.data) + + def test_register_json(self): + r = self._post('/register', + data='{ "email": "dude@lp.com", "password": "password" }', + content_type='application/json') + data = json.loads(r.data) + self.assertEquals(data['meta']['code'], 200) + self.assertIn('authentication_token', data['response']['user']) + + def test_register_existing_email(self): + data = dict(email='matt@lp.com', + password='password', + password_confirm='password') + r = self._post('/register', data=data, follow_redirects=True) + self.assertIn('matt@lp.com is already associated with an account', r.data) + + def test_unauthorized(self): + self.authenticate("joe@lp.com", endpoint="/custom_auth") + r = self._get("/admin", follow_redirects=True) + msg = 'You are not allowed to access the requested resouce' + self.assertIn(msg, r.data) + + def test_default_http_auth_realm(self): + r = self._get('/http', headers={ + 'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus") + }) + self.assertIn('

Unauthorized

', r.data) + self.assertIn('WWW-Authenticate', r.headers) + self.assertEquals('Basic realm="Custom Realm"', + r.headers['WWW-Authenticate']) + + +class BadConfiguredSecurityTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_PASSWORD_HASH': 'bcrypt', + 'USER_COUNT': 1 + } + + def test_bad_configuration_raises_runtimer_error(self): + self.assertRaises(RuntimeError, self.authenticate) + + +class RegisterableTests(SecurityTest): + AUTH_CONFIG = { + 'SECURITY_REGISTERABLE': True, + 'USER_COUNT': 1 + } + + def test_register_valid_user(self): + data = dict(email='dude@lp.com', + password='password', + password_confirm='password') + self.client.post('/register', data=data, follow_redirects=True) + r = self.authenticate('dude@lp.com') + self.assertIn('Hello dude@lp.com', r.data) + + +class ConfirmableTests(SecurityTest): + AUTH_CONFIG = { + 'SECURITY_CONFIRMABLE': True, + 'SECURITY_REGISTERABLE': True, + 'USER_COUNT': 1 + } + + def test_login_before_confirmation(self): + e = 'dude@lp.com' + self.register(e) + r = self.authenticate(email=e) + self.assertIn(self.get_message('CONFIRMATION_REQUIRED'), r.data) + + def test_send_confirmation_of_already_confirmed_account(self): + e = 'dude@lp.com' + + with capture_registrations() as registrations: + self.register(e) + token = registrations[0]['confirm_token'] + + self.client.get('/confirm/' + token, follow_redirects=True) + self.logout() + r = self.client.post('/confirm', data=dict(email=e)) + self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data) + + def test_register_sends_confirmation_email(self): + e = 'dude@lp.com' + with self.app.extensions['mail'].record_messages() as outbox: + self.register(e) + self.assertEqual(len(outbox), 1) + self.assertIn(e, outbox[0].html) + + def test_confirm_email(self): + e = 'dude@lp.com' + + with capture_registrations() as registrations: + self.register(e) + token = registrations[0]['confirm_token'] + + r = self.client.get('/confirm/' + token, follow_redirects=True) + + msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0] + self.assertIn(msg, r.data) + + def test_invalid_token_when_confirming_email(self): + r = self.client.get('/confirm/bogus', follow_redirects=True) + self.assertIn('Invalid confirmation token', r.data) + + def test_send_confirmation_with_invalid_email(self): + r = self._post('/confirm', data=dict(email='bogus@bogus.com')) + self.assertIn('Specified user does not exist', r.data) + + def test_resend_confirmation(self): + e = 'dude@lp.com' + self.register(e) + r = self._post('/confirm', data={'email': e}) + + msg = self.get_message('CONFIRMATION_REQUEST', email=e) + self.assertIn(msg, r.data) + + +class ExpiredConfirmationTest(SecurityTest): + AUTH_CONFIG = { + 'SECURITY_CONFIRMABLE': True, + 'SECURITY_REGISTERABLE': True, + 'SECURITY_CONFIRM_EMAIL_WITHIN': '1 milliseconds', + 'USER_COUNT': 1 + } + + def test_expired_confirmation_token_sends_email(self): + e = 'dude@lp.com' + + with capture_registrations() as registrations: + self.register(e) + token = registrations[0]['confirm_token'] + + time.sleep(1.25) + + with self.app.extensions['mail'].record_messages() as outbox: + r = self.client.get('/confirm/' + token, follow_redirects=True) + + self.assertEqual(len(outbox), 1) + self.assertNotIn(token, outbox[0].html) + + expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN'] + msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0] + msg = msg % dict(within=expire_text, email=e) + self.assertIn(msg, r.data) + + +class LoginWithoutImmediateConfirmTests(SecurityTest): + AUTH_CONFIG = { + 'SECURITY_CONFIRMABLE': True, + 'SECURITY_REGISTERABLE': True, + 'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True, + 'USER_COUNT': 1 + } + + def test_register_valid_user_automatically_signs_in(self): + 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) + self.assertIn(e, r.data) + + +class RecoverableTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_RECOVERABLE': True, + 'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/', + 'SECURITY_POST_FORGOT_VIEW': '/' + } + + def test_reset_view(self): + with capture_reset_password_requests() as requests: + r = self.client.post('/reset', + data=dict(email='joe@lp.com'), + follow_redirects=True) + t = requests[0]['token'] + r = self._get('/reset/' + t) + self.assertIn('

Reset password

', r.data) + + 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.assertEqual(len(outbox), 1) + + def test_forgot_password_invalid_email(self): + r = self.client.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', + data=dict(email='joe@lp.com'), + follow_redirects=True) + t = requests[0]['token'] + + r = self._post('/reset/' + t, data={ + 'password': 'newpassword', + 'password_confirm': 'newpassword' + }, follow_redirects=True) + + r = self.logout() + r = self.authenticate('joe@lp.com', 'newpassword') + self.assertIn('Hello joe@lp.com', r.data) + + def test_reset_password_with_invalid_token(self): + r = self._post('/reset/bogus', data={ + 'password': 'newpassword', + 'password_confirm': 'newpassword' + }, follow_redirects=True) + + self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), r.data) + + +class ExpiredResetPasswordTest(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_RECOVERABLE': True, + 'SECURITY_RESET_PASSWORD_WITHIN': '1 milliseconds' + } + + 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) + t = requests[0]['token'] + + time.sleep(1) + + r = self.client.post('/reset/' + t, data={ + 'password': 'newpassword', + 'password_confirm': 'newpassword' + }, follow_redirects=True) + + self.assertIn('You did not reset your password within', r.data) + + +class TrackableTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_TRACKABLE': True, + 'USER_COUNT': 1 + } + + def test_did_track(self): + e = 'matt@lp.com' + self.authenticate(email=e) + self.logout() + self.authenticate(email=e) + + with self.app.test_request_context('/profile'): + user = self.app.security.datastore.find_user(email=e) + self.assertIsNotNone(user.last_login_at) + self.assertIsNotNone(user.current_login_at) + self.assertEquals('untrackable', user.last_login_ip) + self.assertEquals('untrackable', user.current_login_ip) + self.assertEquals(2, user.login_count) + + +class PasswordlessTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_PASSWORDLESS': True + } + + 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) + self.assertIn(msg, r.data) + + def test_request_login_token_sends_email_and_can_login(self): + e = 'matt@lp.com' + r, user, token = None, None, None + + 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) + + self.assertEqual(len(outbox), 1) + + self.assertEquals(1, len(requests)) + self.assertIn('user', requests[0]) + self.assertIn('login_token', requests[0]) + + user = requests[0]['user'] + token = requests[0]['login_token'] + + msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0] + msg = msg % dict(email=user.email) + self.assertIn(msg, r.data) + + r = self.client.get('/login/' + token, follow_redirects=True) + msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') + self.assertIn(msg, r.data) + + r = self.client.get('/profile') + self.assertIn('Profile Page', r.data) + + def test_invalid_login_token(self): + msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0] + r = self._get('/login/bogus', follow_redirects=True) + self.assertIn(msg, r.data) + + 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) + token = requests[0]['login_token'] + + r = self.client.get('/login/' + token, follow_redirects=True) + msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') + self.assertIn(msg, r.data) + + r = self.client.get('/login/' + token, follow_redirects=True) + msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') + self.assertNotIn(msg, r.data) + + def test_send_login_with_invalid_email(self): + r = self._post('/login', data=dict(email='bogus@bogus.com')) + self.assertIn('Specified user does not exist', r.data) + + +class ExpiredLoginTokenTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_PASSWORDLESS': True, + 'SECURITY_LOGIN_WITHIN': '1 milliseconds', + 'USER_COUNT': 1 + } + + def test_expired_login_token_sends_email(self): + e = 'matt@lp.com' + + with capture_passwordless_login_requests() as requests: + self.client.post('/login', + data=dict(email=e), + follow_redirects=True) + token = requests[0]['login_token'] + + time.sleep(1.25) + + with self.app.extensions['mail'].record_messages() as outbox: + r = self.client.get('/login/' + token, follow_redirects=True) + + expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN'] + msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0] + msg = msg % dict(within=expire_text, email=e) + self.assertIn(msg, r.data) + + self.assertEqual(len(outbox), 1) + self.assertIn(e, outbox[0].html) + self.assertNotIn(token, outbox[0].html) + + +class AsyncMailTaskTests(SecurityTest): + + AUTH_CONFIG = { + 'SECURITY_RECOVERABLE': True, + 'USER_COUNT': 1 + } + + def setUp(self): + super(AsyncMailTaskTests, self).setUp() + self.mail_sent = False + + def test_send_email_task_is_called(self): + @self.app.security.send_mail_task + def send_email(msg): + self.mail_sent = True + + self.client.post('/reset', data=dict(email='matt@lp.com')) + self.assertTrue(self.mail_sent) + + +class NoBlueprintTests(SecurityTest): + + AUTH_CONFIG = { + 'USER_COUNT': 1 + } + + def _create_app(self, auth_config): + return super(NoBlueprintTests, self)._create_app(auth_config, False) + + def test_login_endpoint_is_404(self): + r = self._get('/login') + self.assertEqual(404, r.status_code) + + def test_http_auth_without_blueprint(self): + auth = 'Basic ' + base64.b64encode("matt@lp.com:password") + r = self._get('/http', headers={'Authorization': auth}) + self.assertIn('HTTP Authentication', r.data) diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 8f2a0ba..fa3bb05 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -3,17 +3,9 @@ from __future__ import with_statement import base64 -import time - +import simplejson as json from cookielib import Cookie -try: - import simplejson as json -except ImportError: - import json - -from flask.ext.security.utils import capture_registrations, \ - capture_reset_password_requests, capture_passwordless_login_requests from werkzeug.utils import parse_cookie from tests import SecurityTest @@ -197,401 +189,6 @@ class DefaultSecurityTests(SecurityTest): self.assertNotIn('BadSignature', r.data) -class ConfiguredPasswordHashSecurityTests(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_PASSWORD_HASH': 'bcrypt', - 'SECURITY_PASSWORD_SALT': 'so-salty', - 'USER_COUNT': 1 - } - - def test_authenticate(self): - r = self.authenticate(endpoint="/login") - self.assertIn('Home Page', r.data) - - -class ConfiguredSecurityTests(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_REGISTERABLE': True, - 'SECURITY_LOGOUT_URL': '/custom_logout', - 'SECURITY_LOGIN_URL': '/custom_login', - 'SECURITY_POST_LOGIN_VIEW': '/post_login', - 'SECURITY_POST_LOGOUT_VIEW': '/post_logout', - 'SECURITY_POST_REGISTER_VIEW': '/post_register', - 'SECURITY_UNAUTHORIZED_VIEW': '/unauthorized', - 'SECURITY_DEFAULT_HTTP_AUTH_REALM': 'Custom Realm' - } - - def test_login_view(self): - r = self._get('/custom_login') - self.assertIn("

Login

", r.data) - - def test_authenticate(self): - r = self.authenticate(endpoint="/custom_login") - self.assertIn('Post Login', r.data) - - def test_logout(self): - self.authenticate(endpoint="/custom_login") - r = self.logout(endpoint="/custom_logout") - self.assertIn('Post Logout', r.data) - - def test_register_view(self): - r = self._get('/register') - self.assertIn('

Register

', r.data) - - def test_register(self): - data = dict(email='dude@lp.com', - password='password', - password_confirm='password') - - r = self._post('/register', data=data, follow_redirects=True) - self.assertIn('Post Register', r.data) - - def test_register_json(self): - r = self._post('/register', - data='{ "email": "dude@lp.com", "password": "password" }', - content_type='application/json') - data = json.loads(r.data) - self.assertEquals(data['meta']['code'], 200) - self.assertIn('authentication_token', data['response']['user']) - - def test_register_existing_email(self): - data = dict(email='matt@lp.com', - password='password', - password_confirm='password') - r = self._post('/register', data=data, follow_redirects=True) - self.assertIn('matt@lp.com is already associated with an account', r.data) - - def test_unauthorized(self): - self.authenticate("joe@lp.com", endpoint="/custom_auth") - r = self._get("/admin", follow_redirects=True) - msg = 'You are not allowed to access the requested resouce' - self.assertIn(msg, r.data) - - def test_default_http_auth_realm(self): - r = self._get('/http', headers={ - 'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus") - }) - self.assertIn('

Unauthorized

', r.data) - self.assertIn('WWW-Authenticate', r.headers) - self.assertEquals('Basic realm="Custom Realm"', r.headers['WWW-Authenticate']) - - -class BadConfiguredSecurityTests(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_PASSWORD_HASH': 'bcrypt', - 'USER_COUNT': 1 - } - - def test_bad_configuration_raises_runtimer_error(self): - self.assertRaises(RuntimeError, self.authenticate) - - -class RegisterableTests(SecurityTest): - AUTH_CONFIG = { - 'SECURITY_REGISTERABLE': True, - 'USER_COUNT': 1 - } - - def test_register_valid_user(self): - data = dict(email='dude@lp.com', password='password', password_confirm='password') - self.client.post('/register', data=data, follow_redirects=True) - r = self.authenticate('dude@lp.com') - self.assertIn('Hello dude@lp.com', r.data) - - -class ConfirmableTests(SecurityTest): - AUTH_CONFIG = { - 'SECURITY_CONFIRMABLE': True, - 'SECURITY_REGISTERABLE': True, - 'USER_COUNT': 1 - } - - def test_login_before_confirmation(self): - e = 'dude@lp.com' - self.register(e) - r = self.authenticate(email=e) - self.assertIn(self.get_message('CONFIRMATION_REQUIRED'), r.data) - - def test_send_confirmation_of_already_confirmed_account(self): - e = 'dude@lp.com' - - with capture_registrations() as registrations: - self.register(e) - token = registrations[0]['confirm_token'] - - self.client.get('/confirm/' + token, follow_redirects=True) - self.logout() - r = self.client.post('/confirm', data=dict(email=e)) - self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data) - - def test_register_sends_confirmation_email(self): - e = 'dude@lp.com' - with self.app.extensions['mail'].record_messages() as outbox: - self.register(e) - self.assertEqual(len(outbox), 1) - self.assertIn(e, outbox[0].html) - - def test_confirm_email(self): - e = 'dude@lp.com' - - with capture_registrations() as registrations: - self.register(e) - token = registrations[0]['confirm_token'] - - r = self.client.get('/confirm/' + token, follow_redirects=True) - - msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0] - self.assertIn(msg, r.data) - - def test_invalid_token_when_confirming_email(self): - r = self.client.get('/confirm/bogus', follow_redirects=True) - self.assertIn('Invalid confirmation token', r.data) - - def test_send_confirmation_with_invalid_email(self): - r = self._post('/confirm', data=dict(email='bogus@bogus.com')) - self.assertIn('Specified user does not exist', r.data) - - def test_resend_confirmation(self): - e = 'dude@lp.com' - self.register(e) - r = self._post('/confirm', data={'email': e}) - self.assertIn(self.get_message('CONFIRMATION_REQUEST', email=e), r.data) - - -class ExpiredConfirmationTest(SecurityTest): - AUTH_CONFIG = { - 'SECURITY_CONFIRMABLE': True, - 'SECURITY_REGISTERABLE': True, - 'SECURITY_CONFIRM_EMAIL_WITHIN': '1 milliseconds', - 'USER_COUNT': 1 - } - - def test_expired_confirmation_token_sends_email(self): - e = 'dude@lp.com' - - with capture_registrations() as registrations: - self.register(e) - token = registrations[0]['confirm_token'] - - time.sleep(1.25) - - with self.app.extensions['mail'].record_messages() as outbox: - r = self.client.get('/confirm/' + token, follow_redirects=True) - - self.assertEqual(len(outbox), 1) - self.assertNotIn(token, outbox[0].html) - - expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN'] - msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0] % dict(within=expire_text, email=e) - self.assertIn(msg, r.data) - - -class LoginWithoutImmediateConfirmTests(SecurityTest): - AUTH_CONFIG = { - 'SECURITY_CONFIRMABLE': True, - 'SECURITY_REGISTERABLE': True, - 'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True, - 'USER_COUNT': 1 - } - - def test_register_valid_user_automatically_signs_in(self): - 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) - self.assertIn(e, r.data) - - -class RecoverableTests(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_RECOVERABLE': True, - 'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/', - 'SECURITY_POST_FORGOT_VIEW': '/' - } - - def test_reset_view(self): - with capture_reset_password_requests() as requests: - r = self.client.post('/reset', - data=dict(email='joe@lp.com'), - follow_redirects=True) - t = requests[0]['token'] - r = self._get('/reset/' + t) - self.assertIn('

Reset password

', r.data) - - 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.assertEqual(len(outbox), 1) - - def test_forgot_password_invalid_email(self): - r = self.client.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', - data=dict(email='joe@lp.com'), - follow_redirects=True) - t = requests[0]['token'] - - r = self._post('/reset/' + t, data={ - 'password': 'newpassword', - 'password_confirm': 'newpassword' - }, follow_redirects=True) - - r = self.logout() - r = self.authenticate('joe@lp.com', 'newpassword') - self.assertIn('Hello joe@lp.com', r.data) - - def test_reset_password_with_invalid_token(self): - r = self._post('/reset/bogus', data={ - 'password': 'newpassword', - 'password_confirm': 'newpassword' - }, follow_redirects=True) - - self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), r.data) - - -class ExpiredResetPasswordTest(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_RECOVERABLE': True, - 'SECURITY_RESET_PASSWORD_WITHIN': '1 milliseconds' - } - - 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) - t = requests[0]['token'] - - time.sleep(1) - - r = self.client.post('/reset/' + t, data={ - 'password': 'newpassword', - 'password_confirm': 'newpassword' - }, follow_redirects=True) - - self.assertIn('You did not reset your password within', r.data) - - -class TrackableTests(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_TRACKABLE': True, - 'USER_COUNT': 1 - } - - def test_did_track(self): - e = 'matt@lp.com' - self.authenticate(email=e) - self.logout() - self.authenticate(email=e) - - with self.app.test_request_context('/profile'): - user = self.app.security.datastore.find_user(email=e) - self.assertIsNotNone(user.last_login_at) - self.assertIsNotNone(user.current_login_at) - self.assertEquals('untrackable', user.last_login_ip) - self.assertEquals('untrackable', user.current_login_ip) - self.assertEquals(2, user.login_count) - - -class PasswordlessTests(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_PASSWORDLESS': True - } - - 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) - self.assertIn(msg, r.data) - - def test_request_login_token_sends_email_and_can_login(self): - e = 'matt@lp.com' - r, user, token = None, None, None - - 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) - - self.assertEqual(len(outbox), 1) - - self.assertEquals(1, len(requests)) - self.assertIn('user', requests[0]) - self.assertIn('login_token', requests[0]) - - user = requests[0]['user'] - token = requests[0]['login_token'] - - msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0] % dict(email=user.email) - self.assertIn(msg, r.data) - - r = self.client.get('/login/' + token, follow_redirects=True) - self.assertIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data) - - r = self.client.get('/profile') - self.assertIn('Profile Page', r.data) - - def test_invalid_login_token(self): - msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0] - r = self._get('/login/bogus', follow_redirects=True) - self.assertIn(msg, r.data) - - def test_token_login_forwards_to_post_login_view_when_already_authenticated(self): - with capture_passwordless_login_requests() as requests: - self.client.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) - self.assertIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data) - - r = self.client.get('/login/' + token, follow_redirects=True) - self.assertNotIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data) - - def test_send_login_with_invalid_email(self): - r = self._post('/login', data=dict(email='bogus@bogus.com')) - self.assertIn('Specified user does not exist', r.data) - - -class ExpiredLoginTokenTests(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_PASSWORDLESS': True, - 'SECURITY_LOGIN_WITHIN': '1 milliseconds', - 'USER_COUNT': 1 - } - - def test_expired_login_token_sends_email(self): - e = 'matt@lp.com' - - with capture_passwordless_login_requests() as requests: - self.client.post('/login', data=dict(email=e), follow_redirects=True) - token = requests[0]['login_token'] - - time.sleep(1.25) - - with self.app.extensions['mail'].record_messages() as outbox: - r = self.client.get('/login/' + token, follow_redirects=True) - - expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN'] - msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0] % dict(within=expire_text, email=e) - self.assertIn(msg, r.data) - - self.assertEqual(len(outbox), 1) - self.assertIn(e, outbox[0].html) - self.assertNotIn(token, outbox[0].html) - - class MongoEngineSecurityTests(DefaultSecurityTests): def _create_app(self, auth_config): @@ -627,43 +224,3 @@ class MongoEngineDatastoreTests(DefaultDatastoreTests): def _create_app(self, auth_config): from tests.test_app.mongoengine import create_app return create_app(auth_config) - - -class AsyncMailTaskTests(SecurityTest): - - AUTH_CONFIG = { - 'SECURITY_RECOVERABLE': True, - 'USER_COUNT': 1 - } - - def setUp(self): - super(AsyncMailTaskTests, self).setUp() - self.mail_sent = False - - def test_send_email_task_is_called(self): - @self.app.security.send_mail_task - def send_email(msg): - self.mail_sent = True - - self.client.post('/reset', data=dict(email='matt@lp.com')) - self.assertTrue(self.mail_sent) - - -class NoBlueprintTests(SecurityTest): - - AUTH_CONFIG = { - 'USER_COUNT': 1 - } - - def _create_app(self, auth_config): - return super(NoBlueprintTests, self)._create_app(auth_config, False) - - def test_login_endpoint_is_404(self): - r = self._get('/login') - self.assertEqual(404, r.status_code) - - def test_http_auth_without_blueprint(self): - r = self._get('/http', headers={ - 'Authorization': 'Basic ' + base64.b64encode("matt@lp.com:password") - }) - self.assertIn('HTTP Authentication', r.data)