aboutsummaryrefslogtreecommitdiff
path: root/wqflask/flask_security
diff options
context:
space:
mode:
authorSam2013-05-17 20:59:50 +0000
committerSam2013-05-17 20:59:50 +0000
commit48ba921c234abb218b14d7f1ae4d5b017ab8d388 (patch)
treec8614aff3e6013a61eec42b4a52a3db1f9c9272f /wqflask/flask_security
parent457712ce46beed04126535024daae603dd6136fc (diff)
downloadgenenetwork2-48ba921c234abb218b14d7f1ae4d5b017ab8d388.tar.gz
Added flask_security locally
Diffstat (limited to 'wqflask/flask_security')
-rw-r--r--wqflask/flask_security/__init__.py23
-rw-r--r--wqflask/flask_security/changeable.py45
-rw-r--r--wqflask/flask_security/confirmable.py83
-rw-r--r--wqflask/flask_security/core.py381
-rw-r--r--wqflask/flask_security/datastore.py260
-rw-r--r--wqflask/flask_security/decorators.py207
-rw-r--r--wqflask/flask_security/forms.py277
-rw-r--r--wqflask/flask_security/passwordless.py59
-rw-r--r--wqflask/flask_security/recoverable.py80
-rw-r--r--wqflask/flask_security/registerable.py43
-rw-r--r--wqflask/flask_security/script.py130
-rw-r--r--wqflask/flask_security/signals.py29
-rw-r--r--wqflask/flask_security/templates/.DS_Storebin0 -> 6148 bytes
-rw-r--r--wqflask/flask_security/templates/security/.DS_Storebin0 -> 6148 bytes
-rw-r--r--wqflask/flask_security/templates/security/_macros.html16
-rw-r--r--wqflask/flask_security/templates/security/_menu.html15
-rw-r--r--wqflask/flask_security/templates/security/_messages.html9
-rw-r--r--wqflask/flask_security/templates/security/change_password.html11
-rw-r--r--wqflask/flask_security/templates/security/email/change_notice.html4
-rw-r--r--wqflask/flask_security/templates/security/email/change_notice.txt5
-rw-r--r--wqflask/flask_security/templates/security/email/confirmation_instructions.html3
-rw-r--r--wqflask/flask_security/templates/security/email/confirmation_instructions.txt3
-rw-r--r--wqflask/flask_security/templates/security/email/login_instructions.html5
-rw-r--r--wqflask/flask_security/templates/security/email/login_instructions.txt5
-rw-r--r--wqflask/flask_security/templates/security/email/reset_instructions.html1
-rw-r--r--wqflask/flask_security/templates/security/email/reset_instructions.txt3
-rw-r--r--wqflask/flask_security/templates/security/email/reset_notice.html1
-rw-r--r--wqflask/flask_security/templates/security/email/reset_notice.txt1
-rw-r--r--wqflask/flask_security/templates/security/email/welcome.html7
-rw-r--r--wqflask/flask_security/templates/security/email/welcome.txt7
-rw-r--r--wqflask/flask_security/templates/security/forgot_password.html9
-rw-r--r--wqflask/flask_security/templates/security/login_user.html12
-rw-r--r--wqflask/flask_security/templates/security/register_user.html13
-rw-r--r--wqflask/flask_security/templates/security/reset_password.html10
-rw-r--r--wqflask/flask_security/templates/security/send_confirmation.html9
-rw-r--r--wqflask/flask_security/templates/security/send_login.html9
-rw-r--r--wqflask/flask_security/utils.py379
-rw-r--r--wqflask/flask_security/views.py359
38 files changed, 2513 insertions, 0 deletions
diff --git a/wqflask/flask_security/__init__.py b/wqflask/flask_security/__init__.py
new file mode 100644
index 00000000..68267cff
--- /dev/null
+++ b/wqflask/flask_security/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security
+ ~~~~~~~~~~~~~~~~~~
+
+ Flask-Security is a Flask extension that aims to add quick and simple
+ security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+__version__ = '1.6.0'
+
+from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
+from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
+from .decorators import auth_token_required, http_auth_required, \
+ login_required, roles_accepted, roles_required
+from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
+ ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
+from .signals import confirm_instructions_sent, password_reset, \
+ reset_password_instructions_sent, user_confirmed, user_registered
+from .utils import login_user, logout_user, url_for_security
diff --git a/wqflask/flask_security/changeable.py b/wqflask/flask_security/changeable.py
new file mode 100644
index 00000000..4447b655
--- /dev/null
+++ b/wqflask/flask_security/changeable.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.changeable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security recoverable module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :author: Eskil Heyn Olsen
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app as app, request
+from werkzeug.local import LocalProxy
+
+from .signals import password_changed
+from .utils import send_mail, encrypt_password, url_for_security, \
+ config_value
+
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions['security'])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def send_password_changed_notice(user):
+ """Sends the password changed notice email for the specified user.
+
+ :param user: The user to send the notice to
+ """
+ send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email,
+ 'change_notice', user=user)
+
+
+def change_user_password(user, password):
+ """Change the specified user's password
+
+ :param user: The user to change_password
+ :param password: The unencrypted new password
+ """
+ user.password = encrypt_password(password)
+ _datastore.put(user)
+ send_password_changed_notice(user)
+ password_changed.send(user, app=app._get_current_object())
diff --git a/wqflask/flask_security/confirmable.py b/wqflask/flask_security/confirmable.py
new file mode 100644
index 00000000..a7caf6cd
--- /dev/null
+++ b/wqflask/flask_security/confirmable.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.confirmable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security confirmable module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from datetime import datetime
+
+from flask import current_app as app, request
+from werkzeug.local import LocalProxy
+
+from .utils import send_mail, md5, url_for_security, get_token_status,\
+ config_value
+from .signals import user_confirmed, confirm_instructions_sent
+
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions['security'])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def generate_confirmation_link(user):
+ token = generate_confirmation_token(user)
+ url = url_for_security('confirm_email', token=token)
+ return request.url_root[:-1] + url, token
+
+
+def send_confirmation_instructions(user):
+ """Sends the confirmation instructions email for the specified user.
+
+ :param user: The user to send the instructions to
+ :param token: The confirmation token
+ """
+
+ confirmation_link, token = generate_confirmation_link(user)
+
+ send_mail(config_value('EMAIL_SUBJECT_CONFIRM'), user.email,
+ 'confirmation_instructions', user=user,
+ confirmation_link=confirmation_link)
+
+ confirm_instructions_sent.send(user, app=app._get_current_object())
+ return token
+
+
+def generate_confirmation_token(user):
+ """Generates a unique confirmation token for the specified user.
+
+ :param user: The user to work with
+ """
+ data = [str(user.id), md5(user.email)]
+ return _security.confirm_serializer.dumps(data)
+
+
+def requires_confirmation(user):
+ """Returns `True` if the user requires confirmation."""
+ return _security.confirmable and user.confirmed_at == None
+
+
+def confirm_email_token_status(token):
+ """Returns the expired status, invalid status, and user of a confirmation
+ token. For example::
+
+ expired, invalid, user = confirm_email_token_status('...')
+
+ :param token: The confirmation token
+ """
+ return get_token_status(token, 'confirm', 'CONFIRM_EMAIL')
+
+
+def confirm_user(user):
+ """Confirms the specified user
+
+ :param user: The user to confirm
+ """
+ user.confirmed_at = datetime.utcnow()
+ _datastore.put(user)
+ user_confirmed.send(user, app=app._get_current_object())
diff --git a/wqflask/flask_security/core.py b/wqflask/flask_security/core.py
new file mode 100644
index 00000000..d794fad5
--- /dev/null
+++ b/wqflask/flask_security/core.py
@@ -0,0 +1,381 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.core
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security core module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app
+from flask.ext.login import AnonymousUser as AnonymousUserBase, \
+ UserMixin as BaseUserMixin, LoginManager, current_user
+from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \
+ identity_loaded
+from itsdangerous import URLSafeTimedSerializer
+from passlib.context import CryptContext
+from werkzeug.datastructures import ImmutableList
+from werkzeug.local import LocalProxy
+
+from .utils import config_value as cv, get_config, md5, url_for_security
+from .views import create_blueprint
+from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
+ ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \
+ SendConfirmationForm, PasswordlessLoginForm
+
+# Convenient references
+_security = LocalProxy(lambda: current_app.extensions['security'])
+
+
+#: Default Flask-Security configuration
+_default_config = {
+ 'BLUEPRINT_NAME': 'security',
+ 'URL_PREFIX': None,
+ 'SUBDOMAIN': None,
+ 'FLASH_MESSAGES': True,
+ 'PASSWORD_HASH': 'plaintext',
+ 'PASSWORD_SALT': None,
+ 'LOGIN_URL': '/login',
+ 'LOGOUT_URL': '/logout',
+ 'REGISTER_URL': '/register',
+ 'RESET_URL': '/reset',
+ 'CHANGE_URL': '/change',
+ 'CONFIRM_URL': '/confirm',
+ 'POST_LOGIN_VIEW': '/',
+ 'POST_LOGOUT_VIEW': '/',
+ 'CONFIRM_ERROR_VIEW': None,
+ 'POST_REGISTER_VIEW': None,
+ 'POST_CONFIRM_VIEW': None,
+ 'POST_RESET_VIEW': None,
+ 'POST_CHANGE_VIEW': None,
+ 'UNAUTHORIZED_VIEW': None,
+ 'FORGOT_PASSWORD_TEMPLATE': 'security/forgot_password.html',
+ 'LOGIN_USER_TEMPLATE': 'security/login_user.html',
+ 'REGISTER_USER_TEMPLATE': 'security/register_user.html',
+ 'RESET_PASSWORD_TEMPLATE': 'security/reset_password.html',
+ 'SEND_CONFIRMATION_TEMPLATE': 'security/send_confirmation.html',
+ 'SEND_LOGIN_TEMPLATE': 'security/send_login.html',
+ 'CONFIRMABLE': False,
+ 'REGISTERABLE': False,
+ 'RECOVERABLE': False,
+ 'TRACKABLE': False,
+ 'PASSWORDLESS': False,
+ 'CHANGEABLE': False,
+ 'SEND_REGISTER_EMAIL': True,
+ 'LOGIN_WITHIN': '1 days',
+ 'CONFIRM_EMAIL_WITHIN': '5 days',
+ 'RESET_PASSWORD_WITHIN': '5 days',
+ 'LOGIN_WITHOUT_CONFIRMATION': False,
+ 'EMAIL_SENDER': 'no-reply@localhost',
+ 'TOKEN_AUTHENTICATION_KEY': 'auth_token',
+ 'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
+ 'CONFIRM_SALT': 'confirm-salt',
+ 'RESET_SALT': 'reset-salt',
+ 'LOGIN_SALT': 'login-salt',
+ 'CHANGE_SALT': 'change-salt',
+ 'REMEMBER_SALT': 'remember-salt',
+ 'DEFAULT_HTTP_AUTH_REALM': 'Login Required',
+ 'EMAIL_SUBJECT_REGISTER': 'Welcome',
+ 'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email',
+ 'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions',
+ 'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset',
+ 'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
+ 'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions'
+}
+
+#: Default Flask-Security messages
+_default_messages = {
+ 'UNAUTHORIZED': ('You do not have permission to view this resource.', 'error'),
+ 'CONFIRM_REGISTRATION': ('Thank you. Confirmation instructions have been sent to %(email)s.', 'success'),
+ 'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'),
+ 'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'),
+ 'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'),
+ 'EMAIL_ALREADY_ASSOCIATED': ('%(email)s is already associated with an account.', 'error'),
+ 'PASSWORD_MISMATCH': ('Password does not match', 'error'),
+ 'RETYPE_PASSWORD_MISMATCH': ('Passwords do not match', 'error'),
+ 'INVALID_REDIRECT': ('Redirections outside the domain are forbidden', 'error'),
+ 'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'),
+ 'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'),
+ 'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'),
+ 'CONFIRMATION_REQUIRED': ('Email requires confirmation.', 'error'),
+ 'CONFIRMATION_REQUEST': ('Confirmation instructions have been sent to %(email)s.', 'info'),
+ 'CONFIRMATION_EXPIRED': ('You did not confirm your email within %(within)s. New instructions to confirm your email have been sent to %(email)s.', 'error'),
+ 'LOGIN_EXPIRED': ('You did not login within %(within)s. New instructions to login have been sent to %(email)s.', 'error'),
+ 'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'),
+ 'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'),
+ 'DISABLED_ACCOUNT': ('Account is disabled.', 'error'),
+ 'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'),
+ 'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'),
+ 'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'),
+ 'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'),
+ 'INVALID_PASSWORD': ('Invalid password', 'error'),
+ 'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'),
+ 'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'),
+ 'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'),
+ 'LOGIN': ('Please log in to access this page.', 'info'),
+ 'REFRESH': ('Please reauthenticate to access this page.', 'info'),
+}
+
+_allowed_password_hash_schemes = [
+ 'bcrypt',
+ 'des_crypt',
+ 'pbkdf2_sha256',
+ 'pbkdf2_sha512',
+ 'sha256_crypt',
+ 'sha512_crypt',
+ # And always last one...
+ 'plaintext'
+]
+
+_default_forms = {
+ 'login_form': LoginForm,
+ 'confirm_register_form': ConfirmRegisterForm,
+ 'register_form': RegisterForm,
+ 'forgot_password_form': ForgotPasswordForm,
+ 'reset_password_form': ResetPasswordForm,
+ 'change_password_form': ChangePasswordForm,
+ 'send_confirmation_form': SendConfirmationForm,
+ 'passwordless_login_form': PasswordlessLoginForm,
+}
+
+
+def _user_loader(user_id):
+ return _security.datastore.find_user(id=user_id)
+
+
+def _token_loader(token):
+ try:
+ data = _security.remember_token_serializer.loads(token)
+ user = _security.datastore.find_user(id=data[0])
+ if user and md5(user.password) == data[1]:
+ return user
+ except:
+ pass
+
+ return None
+
+
+def _identity_loader():
+ if not isinstance(current_user._get_current_object(), AnonymousUser):
+ identity = Identity(current_user.id)
+ return identity
+
+
+def _on_identity_loaded(sender, identity):
+ if hasattr(current_user, 'id'):
+ identity.provides.add(UserNeed(current_user.id))
+
+ for role in current_user.roles:
+ identity.provides.add(RoleNeed(role.name))
+
+ identity.user = current_user
+
+
+def _get_login_manager(app):
+ lm = LoginManager()
+ lm.anonymous_user = AnonymousUser
+ lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
+ lm.user_loader(_user_loader)
+ lm.token_loader(_token_loader)
+ lm.login_message, lm.login_message_category = cv('MSG_LOGIN', app=app)
+ lm.needs_refresh_message, lm.needs_refresh_message_category = cv('MSG_REFRESH', app=app)
+ lm.init_app(app)
+ return lm
+
+
+def _get_principal(app):
+ p = Principal(app, use_sessions=False)
+ p.identity_loader(_identity_loader)
+ return p
+
+
+def _get_pwd_context(app):
+ pw_hash = cv('PASSWORD_HASH', app=app)
+ if pw_hash not in _allowed_password_hash_schemes:
+ allowed = ', '.join(_allowed_password_hash_schemes[:-1]) + ' and ' + _allowed_password_hash_schemes[-1]
+ raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed))
+ return CryptContext(schemes=_allowed_password_hash_schemes, default=pw_hash)
+
+
+def _get_serializer(app, name):
+ secret_key = app.config.get('SECRET_KEY')
+ salt = app.config.get('SECURITY_%s_SALT' % name.upper())
+ return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
+
+
+def _get_state(app, datastore, **kwargs):
+ for key, value in get_config(app).items():
+ kwargs[key.lower()] = value
+
+ kwargs.update(dict(
+ app=app,
+ datastore=datastore,
+ login_manager=_get_login_manager(app),
+ principal=_get_principal(app),
+ pwd_context=_get_pwd_context(app),
+ remember_token_serializer=_get_serializer(app, 'remember'),
+ login_serializer=_get_serializer(app, 'login'),
+ reset_serializer=_get_serializer(app, 'reset'),
+ confirm_serializer=_get_serializer(app, 'confirm'),
+ _context_processors={},
+ _send_mail_task=None
+ ))
+
+ for key, value in _default_forms.items():
+ if key not in kwargs or not kwargs[key]:
+ kwargs[key] = value
+
+ return _SecurityState(**kwargs)
+
+
+def _context_processor():
+ return dict(url_for_security=url_for_security, security=_security)
+
+
+class RoleMixin(object):
+ """Mixin for `Role` model definitions"""
+ def __eq__(self, other):
+ return (self.name == other or \
+ self.name == getattr(other, 'name', None))
+
+ def __ne__(self, other):
+ return (self.name != other and
+ self.name != getattr(other, 'name', None))
+
+
+class UserMixin(BaseUserMixin):
+ """Mixin for `User` model definitions"""
+
+ def is_active(self):
+ """Returns `True` if the user is active."""
+ return self.active
+
+ def get_auth_token(self):
+ """Returns the user's authentication token."""
+ data = [str(self.id), md5(self.password)]
+ return _security.remember_token_serializer.dumps(data)
+
+ def has_role(self, role):
+ """Returns `True` if the user identifies with the specified role.
+
+ :param role: A role name or `Role` instance"""
+ return role in self.roles
+
+
+class AnonymousUser(AnonymousUserBase):
+ """AnonymousUser definition"""
+
+ def __init__(self):
+ super(AnonymousUser, self).__init__()
+ self.roles = ImmutableList()
+
+ def has_role(self, *args):
+ """Returns `False`"""
+ return False
+
+
+class _SecurityState(object):
+
+ def __init__(self, **kwargs):
+ for key, value in kwargs.items():
+ setattr(self, key.lower(), value)
+
+ def _add_ctx_processor(self, endpoint, fn):
+ group = self._context_processors.setdefault(endpoint, [])
+ fn not in group and group.append(fn)
+
+ def _run_ctx_processor(self, endpoint):
+ rv, fns = {}, []
+ for g in [None, endpoint]:
+ for fn in self._context_processors.setdefault(g, []):
+ rv.update(fn())
+ return rv
+
+ def context_processor(self, fn):
+ self._add_ctx_processor(None, fn)
+
+ def forgot_password_context_processor(self, fn):
+ self._add_ctx_processor('forgot_password', fn)
+
+ def login_context_processor(self, fn):
+ self._add_ctx_processor('login', fn)
+
+ def register_context_processor(self, fn):
+ self._add_ctx_processor('register', fn)
+
+ def reset_password_context_processor(self, fn):
+ self._add_ctx_processor('reset_password', fn)
+
+ def change_password_context_processor(self, fn):
+ self._add_ctx_processor('change_password', fn)
+
+ def send_confirmation_context_processor(self, fn):
+ self._add_ctx_processor('send_confirmation', fn)
+
+ def send_login_context_processor(self, fn):
+ self._add_ctx_processor('send_login', fn)
+
+ def mail_context_processor(self, fn):
+ self._add_ctx_processor('mail', fn)
+
+ def send_mail_task(self, fn):
+ self._send_mail_task = fn
+
+
+class Security(object):
+ """The :class:`Security` class initializes the Flask-Security extension.
+
+ :param app: The application.
+ :param datastore: An instance of a user datastore.
+ """
+ def __init__(self, app=None, datastore=None, **kwargs):
+ self.app = app
+ self.datastore = datastore
+
+ if app is not None and datastore is not None:
+ self._state = self.init_app(app, datastore, **kwargs)
+
+ def init_app(self, app, datastore=None, register_blueprint=True,
+ login_form=None, confirm_register_form=None,
+ register_form=None, forgot_password_form=None,
+ reset_password_form=None, change_password_form=None,
+ send_confirmation_form=None, passwordless_login_form=None):
+ """Initializes the Flask-Security extension for the specified
+ application and datastore implentation.
+
+ :param app: The application.
+ :param datastore: An instance of a user datastore.
+ :param register_blueprint: to register the Security blueprint or not.
+ """
+ datastore = datastore or self.datastore
+
+ for key, value in _default_config.items():
+ app.config.setdefault('SECURITY_' + key, value)
+
+ for key, value in _default_messages.items():
+ app.config.setdefault('SECURITY_MSG_' + key, value)
+
+ identity_loaded.connect_via(app)(_on_identity_loaded)
+
+ state = _get_state(app, datastore,
+ login_form=login_form,
+ confirm_register_form=confirm_register_form,
+ register_form=register_form,
+ forgot_password_form=forgot_password_form,
+ reset_password_form=reset_password_form,
+ change_password_form=change_password_form,
+ send_confirmation_form=send_confirmation_form,
+ passwordless_login_form=passwordless_login_form)
+
+ if register_blueprint:
+ app.register_blueprint(create_blueprint(state, __name__))
+ app.context_processor(_context_processor)
+
+ app.extensions['security'] = state
+
+ return state
+
+ def __getattr__(self, name):
+ return getattr(self._state, name, None)
diff --git a/wqflask/flask_security/datastore.py b/wqflask/flask_security/datastore.py
new file mode 100644
index 00000000..f8c7218d
--- /dev/null
+++ b/wqflask/flask_security/datastore.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.datastore
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module contains an user datastore classes.
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+class Datastore(object):
+ def __init__(self, db):
+ self.db = db
+
+ def commit(self):
+ pass
+
+ def put(self, model):
+ raise NotImplementedError
+
+ def delete(self, model):
+ raise NotImplementedError
+
+
+class SQLAlchemyDatastore(Datastore):
+ def commit(self):
+ self.db.session.commit()
+
+ def put(self, model):
+ self.db.session.add(model)
+ return model
+
+ def delete(self, model):
+ self.db.session.delete(model)
+
+
+class MongoEngineDatastore(Datastore):
+ def put(self, model):
+ model.save()
+ return model
+
+ def delete(self, model):
+ model.delete()
+
+
+class PeeweeDatastore(Datastore):
+ def put(self, model):
+ model.save()
+ return model
+
+ def delete(self, model):
+ model.delete_instance()
+
+
+class UserDatastore(object):
+ """Abstracted user datastore.
+
+ :param user_model: A user model class definition
+ :param role_model: A role model class definition
+ """
+
+ def __init__(self, user_model, role_model):
+ self.user_model = user_model
+ self.role_model = role_model
+
+ def _prepare_role_modify_args(self, user, role):
+ if isinstance(user, basestring):
+ user = self.find_user(email=user)
+ if isinstance(role, basestring):
+ role = self.find_role(role)
+ return user, role
+
+ def _prepare_create_user_args(self, **kwargs):
+ kwargs.setdefault('active', True)
+ roles = kwargs.get('roles', [])
+ for i, role in enumerate(roles):
+ rn = role.name if isinstance(role, self.role_model) else role
+ # see if the role exists
+ roles[i] = self.find_role(rn)
+ kwargs['roles'] = roles
+ return kwargs
+
+ def find_user(self, *args, **kwargs):
+ """Returns a user matching the provided parameters."""
+ raise NotImplementedError
+
+ def find_role(self, *args, **kwargs):
+ """Returns a role matching the provided name."""
+ raise NotImplementedError
+
+ def add_role_to_user(self, user, role):
+ """Adds a role tp a user
+
+ :param user: The user to manipulate
+ :param role: The role to add to the user
+ """
+ rv = False
+ user, role = self._prepare_role_modify_args(user, role)
+ if role not in user.roles:
+ rv = True
+ user.roles.append(role)
+ return rv
+
+ def remove_role_from_user(self, user, role):
+ """Removes a role from a user
+
+ :param user: The user to manipulate
+ :param role: The role to remove from the user
+ """
+ rv = False
+ user, role = self._prepare_role_modify_args(user, role)
+ if role in user.roles:
+ rv = True
+ user.roles.remove(role)
+ return rv
+
+ def toggle_active(self, user):
+ """Toggles a user's active status. Always returns True."""
+ user.active = not user.active
+ return True
+
+ def deactivate_user(self, user):
+ """Deactivates a specified user. Returns `True` if a change was made.
+
+ :param user: The user to deactivate
+ """
+ if user.active:
+ user.active = False
+ return True
+ return False
+
+ def activate_user(self, user):
+ """Activates a specified user. Returns `True` if a change was made.
+
+ :param user: The user to activate
+ """
+ if not user.active:
+ user.active = True
+ return True
+ return False
+
+ def create_role(self, **kwargs):
+ """Creates and returns a new role from the given parameters."""
+
+ role = self.role_model(**kwargs)
+ return self.put(role)
+
+ def find_or_create_role(self, name, **kwargs):
+ """Returns a role matching the given name or creates it with any
+ additionally provided parameters
+ """
+ kwargs["name"] = name
+ return self.find_role(name) or self.create_role(**kwargs)
+
+ def create_user(self, **kwargs):
+ """Creates and returns a new user from the given parameters."""
+
+ user = self.user_model(**self._prepare_create_user_args(**kwargs))
+ return self.put(user)
+
+ def delete_user(self, user):
+ """Delete the specified user
+
+ :param user: The user to delete
+ """
+ self.delete(user)
+
+
+class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
+ """A SQLAlchemy datastore implementation for Flask-Security that assumes the
+ use of the Flask-SQLAlchemy extension.
+ """
+ def __init__(self, db, user_model, role_model):
+ SQLAlchemyDatastore.__init__(self, db)
+ UserDatastore.__init__(self, user_model, role_model)
+
+ def find_user(self, **kwargs):
+ return self.user_model.query.filter_by(**kwargs).first()
+
+ def find_role(self, role):
+ return self.role_model.query.filter_by(name=role).first()
+
+
+class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
+ """A MongoEngine datastore implementation for Flask-Security that assumes
+ the use of the Flask-MongoEngine extension.
+ """
+ def __init__(self, db, user_model, role_model):
+ MongoEngineDatastore.__init__(self, db)
+ UserDatastore.__init__(self, user_model, role_model)
+
+ def find_user(self, **kwargs):
+ return self.user_model.objects(**kwargs).first()
+
+ def find_role(self, role):
+ return self.role_model.objects(name=role).first()
+
+
+class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
+ """A PeeweeD datastore implementation for Flask-Security that assumes
+ the use of the Flask-Peewee extension.
+
+ :param user_model: A user model class definition
+ :param role_model: A role model class definition
+ :param role_link: A model implementing the many-to-many user-role relation
+ """
+ def __init__(self, db, user_model, role_model, role_link):
+ PeeweeDatastore.__init__(self, db)
+ UserDatastore.__init__(self, user_model, role_model)
+ self.UserRole = role_link
+
+ def find_user(self, **kwargs):
+ try:
+ return self.user_model.filter(**kwargs).get()
+ except self.user_model.DoesNotExist:
+ return None
+
+ def find_role(self, role):
+ try:
+ return self.role_model.filter(name=role).get()
+ except self.role_model.DoesNotExist:
+ return None
+
+ def create_user(self, **kwargs):
+ """Creates and returns a new user from the given parameters."""
+ roles = kwargs.pop('roles', [])
+ user = self.user_model(**self._prepare_create_user_args(**kwargs))
+ user = self.put(user)
+ for role in roles:
+ self.add_role_to_user(user, role)
+ return user
+
+
+ def add_role_to_user(self, user, role):
+ """Adds a role tp a user
+
+ :param user: The user to manipulate
+ :param role: The role to add to the user
+ """
+ user, role = self._prepare_role_modify_args(user, role)
+ if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count():
+ return False
+ else:
+ self.UserRole.create(user=user, role=role)
+ return True
+
+ def remove_role_from_user(self, user, role):
+ """Removes a role from a user
+
+ :param user: The user to manipulate
+ :param role: The role to remove from the user
+ """
+ user, role = self._prepare_role_modify_args(user, role)
+ if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count():
+ self.UserRole.delete().where(self.UserRole.user==user, self.UserRole.role==role)
+ return True
+ else:
+ return False
+
diff --git a/wqflask/flask_security/decorators.py b/wqflask/flask_security/decorators.py
new file mode 100644
index 00000000..0ea1105c
--- /dev/null
+++ b/wqflask/flask_security/decorators.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.decorators
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security decorators module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from functools import wraps
+
+from flask import current_app, Response, request, redirect, _request_ctx_stack
+from flask.ext.login import current_user, login_required
+from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed
+from werkzeug.local import LocalProxy
+
+from . import utils
+
+
+# Convenient references
+_security = LocalProxy(lambda: current_app.extensions['security'])
+
+
+_default_unauthorized_html = """
+ <h1>Unauthorized</h1>
+ <p>The server could not verify that you are authorized to access the URL
+ requested. You either supplied the wrong credentials (e.g. a bad password),
+ or your browser doesn't understand how to supply the credentials required.</p>
+ """
+
+
+def _get_unauthorized_response(text=None, headers=None):
+ text = text or _default_unauthorized_html
+ headers = headers or {}
+ return Response(text, 401, headers)
+
+
+def _get_unauthorized_view():
+ cv = utils.get_url(utils.config_value('UNAUTHORIZED_VIEW'))
+ utils.do_flash(*utils.get_message('UNAUTHORIZED'))
+ return redirect(cv or request.referrer or '/')
+
+
+def _check_token():
+ header_key = _security.token_authentication_header
+ args_key = _security.token_authentication_key
+ header_token = request.headers.get(header_key, None)
+ token = request.args.get(args_key, header_token)
+ if request.json:
+ token = request.json.get(args_key, token)
+ serializer = _security.remember_token_serializer
+
+ try:
+ data = serializer.loads(token)
+ except:
+ return False
+
+ user = _security.datastore.find_user(id=data[0])
+
+ if utils.md5(user.password) == data[1]:
+ app = current_app._get_current_object()
+ _request_ctx_stack.top.user = user
+ identity_changed.send(app, identity=Identity(user.id))
+ return True
+
+
+def _check_http_auth():
+ auth = request.authorization or dict(username=None, password=None)
+ user = _security.datastore.find_user(email=auth.username)
+
+ if user and utils.verify_and_update_password(auth.password, user):
+ _security.datastore.commit()
+ app = current_app._get_current_object()
+ _request_ctx_stack.top.user = user
+ identity_changed.send(app, identity=Identity(user.id))
+ return True
+
+ return False
+
+
+def http_auth_required(realm):
+ """Decorator that protects endpoints using Basic HTTP authentication.
+ The username should be set to the user's email address.
+
+ :param realm: optional realm name"""
+
+ def decorator(fn):
+ @wraps(fn)
+ def wrapper(*args, **kwargs):
+ if _check_http_auth():
+ return fn(*args, **kwargs)
+ r = _security.default_http_auth_realm if callable(realm) else realm
+ h = {'WWW-Authenticate': 'Basic realm="%s"' % r}
+ return _get_unauthorized_response(headers=h)
+ return wrapper
+
+ if callable(realm):
+ return decorator(realm)
+ return decorator
+
+
+def auth_token_required(fn):
+ """Decorator that protects endpoints using token authentication. The token
+ should be added to the request by the client by using a query string
+ variable with a name equal to the configuration value of
+ `SECURITY_TOKEN_AUTHENTICATION_KEY` or in a request header named that of
+ the configuration value of `SECURITY_TOKEN_AUTHENTICATION_HEADER`
+ """
+
+ @wraps(fn)
+ def decorated(*args, **kwargs):
+ if _check_token():
+ return fn(*args, **kwargs)
+ return _get_unauthorized_response()
+ return decorated
+
+
+def auth_required(*auth_methods):
+ """
+ Decorator that protects enpoints through multiple mechanisms
+ Example::
+
+ @app.route('/dashboard')
+ @auth_required('token', 'session')
+ def dashboard():
+ return 'Dashboard'
+
+ :param auth_methods: Specified mechanisms.
+ """
+ login_mechanisms = {
+ 'token': lambda: _check_token(),
+ 'basic': lambda: _check_http_auth(),
+ 'session': lambda: current_user.is_authenticated()
+ }
+
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated_view(*args, **kwargs):
+ mechanisms = [login_mechanisms.get(method) for method in auth_methods]
+ for mechanism in mechanisms:
+ if mechanism and mechanism():
+ return fn(*args, **kwargs)
+ return _get_unauthorized_response()
+ return decorated_view
+ return wrapper
+
+
+def roles_required(*roles):
+ """Decorator which specifies that a user must have all the specified roles.
+ Example::
+
+ @app.route('/dashboard')
+ @roles_required('admin', 'editor')
+ def dashboard():
+ return 'Dashboard'
+
+ The current user must have both the `admin` role and `editor` role in order
+ to view the page.
+
+ :param args: The required roles.
+ """
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated_view(*args, **kwargs):
+ perms = [Permission(RoleNeed(role)) for role in roles]
+ for perm in perms:
+ if not perm.can():
+ return _get_unauthorized_view()
+ return fn(*args, **kwargs)
+ return decorated_view
+ return wrapper
+
+
+def roles_accepted(*roles):
+ """Decorator which specifies that a user must have at least one of the
+ specified roles. Example::
+
+ @app.route('/create_post')
+ @roles_accepted('editor', 'author')
+ def create_post():
+ return 'Create Post'
+
+ The current user must have either the `editor` role or `author` role in
+ order to view the page.
+
+ :param args: The possible roles.
+ """
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated_view(*args, **kwargs):
+ perm = Permission(*[RoleNeed(role) for role in roles])
+ if perm.can():
+ return fn(*args, **kwargs)
+ return _get_unauthorized_view()
+ return decorated_view
+ return wrapper
+
+
+def anonymous_user_required(f):
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ if current_user.is_authenticated():
+ return redirect(utils.get_url(_security.post_login_view))
+ return f(*args, **kwargs)
+ return wrapper
diff --git a/wqflask/flask_security/forms.py b/wqflask/flask_security/forms.py
new file mode 100644
index 00000000..e64e1502
--- /dev/null
+++ b/wqflask/flask_security/forms.py
@@ -0,0 +1,277 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.forms
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security forms module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+import inspect
+import urlparse
+
+import flask_wtf as wtf
+
+from flask import request, current_app
+from flask_wtf import Form as BaseForm, TextField, PasswordField, \
+ SubmitField, HiddenField, BooleanField, ValidationError, Field
+from flask_login import current_user
+from werkzeug.local import LocalProxy
+
+from .confirmable import requires_confirmation
+from .utils import verify_and_update_password, get_message
+
+# Convenient reference
+_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
+
+_default_field_labels = {
+ 'email': 'Email Address',
+ 'password': 'Password',
+ 'remember_me': 'Remember Me',
+ 'login': 'Login',
+ 'retype_password': 'Retype Password',
+ 'register': 'Register',
+ 'send_confirmation': 'Resend Confirmation Instructions',
+ 'recover_password': 'Recover Password',
+ 'reset_password': 'Reset Password',
+ 'retype_password': 'Retype Password',
+ 'new_password': 'New Password',
+ 'change_password': 'Change Password',
+ 'send_login_link': 'Send Login Link'
+}
+
+
+class ValidatorMixin(object):
+ def __call__(self, form, field):
+ if self.message and self.message.isupper():
+ self.message = get_message(self.message)[0]
+ return super(ValidatorMixin, self).__call__(form, field)
+
+
+class EqualTo(ValidatorMixin, wtf.EqualTo):
+ pass
+
+
+class Required(ValidatorMixin, wtf.Required):
+ pass
+
+
+class Email(ValidatorMixin, wtf.Email):
+ pass
+
+
+class Length(ValidatorMixin, wtf.Length):
+ pass
+
+
+email_required = Required(message='EMAIL_NOT_PROVIDED')
+email_validator = Email(message='INVALID_EMAIL_ADDRESS')
+password_required = Required(message='PASSWORD_NOT_PROVIDED')
+
+
+def get_form_field_label(key):
+ return _default_field_labels.get(key, '')
+
+
+def unique_user_email(form, field):
+ if _datastore.find_user(email=field.data) is not None:
+ msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
+ raise ValidationError(msg)
+
+
+def valid_user_email(form, field):
+ form.user = _datastore.find_user(email=field.data)
+ if form.user is None:
+ raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
+
+
+class Form(BaseForm):
+ def __init__(self, *args, **kwargs):
+ if current_app.testing:
+ self.TIME_LIMIT = None
+ super(Form, self).__init__(*args, **kwargs)
+
+
+class EmailFormMixin():
+ email = TextField(get_form_field_label('email'),
+ validators=[email_required,
+ email_validator])
+
+
+class UserEmailFormMixin():
+ user = None
+ email = TextField(get_form_field_label('email'),
+ validators=[email_required,
+ email_validator,
+ valid_user_email])
+
+
+class UniqueEmailFormMixin():
+ email = TextField(get_form_field_label('email'),
+ validators=[email_required,
+ email_validator,
+ unique_user_email])
+
+
+class PasswordFormMixin():
+ password = PasswordField(get_form_field_label('password'),
+ validators=[password_required])
+
+
+class NewPasswordFormMixin():
+ password = PasswordField(get_form_field_label('password'),
+ validators=[password_required,
+ Length(min=6, max=128)])
+
+
+class PasswordConfirmFormMixin():
+ password_confirm = PasswordField(
+ get_form_field_label('retype_password'),
+ validators=[EqualTo('password', message='RETYPE_PASSWORD_MISMATCH')])
+
+
+class NextFormMixin():
+ next = HiddenField()
+
+ def validate_next(self, field):
+ url_next = urlparse.urlsplit(field.data)
+ url_base = urlparse.urlsplit(request.host_url)
+ if url_next.netloc and url_next.netloc != url_base.netloc:
+ field.data = ''
+ raise ValidationError(get_message('INVALID_REDIRECT')[0])
+
+
+class RegisterFormMixin():
+ submit = SubmitField(get_form_field_label('register'))
+
+ def to_dict(form):
+ def is_field_and_user_attr(member):
+ return isinstance(member, Field) and \
+ hasattr(_datastore.user_model, member.name)
+
+ fields = inspect.getmembers(form, is_field_and_user_attr)
+ return dict((key, value.data) for key, value in fields)
+
+
+class SendConfirmationForm(Form, UserEmailFormMixin):
+ """The default forgot password form"""
+
+ submit = SubmitField(get_form_field_label('send_confirmation'))
+
+ def __init__(self, *args, **kwargs):
+ super(SendConfirmationForm, self).__init__(*args, **kwargs)
+ if request.method == 'GET':
+ self.email.data = request.args.get('email', None)
+
+ def validate(self):
+ if not super(SendConfirmationForm, self).validate():
+ return False
+ if self.user.confirmed_at is not None:
+ self.email.errors.append(get_message('ALREADY_CONFIRMED')[0])
+ return False
+ return True
+
+
+class ForgotPasswordForm(Form, UserEmailFormMixin):
+ """The default forgot password form"""
+
+ submit = SubmitField(get_form_field_label('recover_password'))
+
+
+class PasswordlessLoginForm(Form, UserEmailFormMixin):
+ """The passwordless login form"""
+
+ submit = SubmitField(get_form_field_label('send_login_link'))
+
+ def __init__(self, *args, **kwargs):
+ super(PasswordlessLoginForm, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ if not super(PasswordlessLoginForm, self).validate():
+ return False
+ if not self.user.is_active():
+ self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
+ return False
+ return True
+
+
+class LoginForm(Form, NextFormMixin):
+ """The default login form"""
+
+ email = TextField(get_form_field_label('email'))
+ password = PasswordField(get_form_field_label('password'))
+ remember = BooleanField(get_form_field_label('remember_me'))
+ submit = SubmitField(get_form_field_label('login'))
+
+ def __init__(self, *args, **kwargs):
+ super(LoginForm, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ if not super(LoginForm, self).validate():
+ return False
+
+ if self.email.data.strip() == '':
+ self.email.errors.append(get_message('EMAIL_NOT_PROVIDED')[0])
+ return False
+
+ if self.password.data.strip() == '':
+ self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
+ return False
+
+ self.user = _datastore.find_user(email=self.email.data)
+
+ if self.user is None:
+ self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
+ return False
+ if not verify_and_update_password(self.password.data, self.user):
+ self.password.errors.append(get_message('INVALID_PASSWORD')[0])
+ return False
+ if requires_confirmation(self.user):
+ self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
+ return False
+ if not self.user.is_active():
+ self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
+ return False
+ return True
+
+
+class ConfirmRegisterForm(Form, RegisterFormMixin,
+ UniqueEmailFormMixin, NewPasswordFormMixin):
+ pass
+
+
+class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
+ pass
+
+
+class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
+ """The default reset password form"""
+
+ submit = SubmitField(get_form_field_label('reset_password'))
+
+
+class ChangePasswordForm(Form, PasswordFormMixin):
+ """The default change password form"""
+
+ new_password = PasswordField(get_form_field_label('new_password'),
+ validators=[password_required,
+ Length(min=6, max=128)])
+
+ new_password_confirm = PasswordField(get_form_field_label('retype_password'),
+ validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')])
+
+ submit = SubmitField(get_form_field_label('change_password'))
+
+ def validate(self):
+ if not super(ChangePasswordForm, self).validate():
+ return False
+
+ if self.password.data.strip() == '':
+ self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
+ return False
+ if not verify_and_update_password(self.password.data, current_user):
+ self.password.errors.append(get_message('INVALID_PASSWORD')[0])
+ return False
+ return True
diff --git a/wqflask/flask_security/passwordless.py b/wqflask/flask_security/passwordless.py
new file mode 100644
index 00000000..b0accb2c
--- /dev/null
+++ b/wqflask/flask_security/passwordless.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.passwordless
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security passwordless module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import request, current_app as app
+from werkzeug.local import LocalProxy
+
+from .signals import login_instructions_sent
+from .utils import send_mail, url_for_security, get_token_status, \
+ config_value
+
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions['security'])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def send_login_instructions(user):
+ """Sends the login instructions email for the specified user.
+
+ :param user: The user to send the instructions to
+ :param token: The login token
+ """
+ token = generate_login_token(user)
+ url = url_for_security('token_login', token=token)
+ login_link = request.url_root[:-1] + url
+
+ send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
+ 'login_instructions', user=user, login_link=login_link)
+
+ login_instructions_sent.send(dict(user=user, login_token=token),
+ app=app._get_current_object())
+
+
+def generate_login_token(user):
+ """Generates a unique login token for the specified user.
+
+ :param user: The user the token belongs to
+ """
+ return _security.login_serializer.dumps([str(user.id)])
+
+
+def login_token_status(token):
+ """Returns the expired status, invalid status, and user of a login token.
+ For example::
+
+ expired, invalid, user = login_token_status('...')
+
+ :param token: The login token
+ """
+ return get_token_status(token, 'login', 'LOGIN')
diff --git a/wqflask/flask_security/recoverable.py b/wqflask/flask_security/recoverable.py
new file mode 100644
index 00000000..6aafc111
--- /dev/null
+++ b/wqflask/flask_security/recoverable.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.recoverable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security recoverable module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app as app, request
+from werkzeug.local import LocalProxy
+
+from .signals import password_reset, reset_password_instructions_sent
+from .utils import send_mail, md5, encrypt_password, url_for_security, \
+ get_token_status, config_value
+
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions['security'])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def send_reset_password_instructions(user):
+ """Sends the reset password instructions email for the specified user.
+
+ :param user: The user to send the instructions to
+ """
+ token = generate_reset_password_token(user)
+ url = url_for_security('reset_password', token=token)
+ reset_link = request.url_root[:-1] + url
+
+ send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email,
+ 'reset_instructions',
+ user=user, reset_link=reset_link)
+
+ reset_password_instructions_sent.send(dict(user=user, token=token),
+ app=app._get_current_object())
+
+
+def send_password_reset_notice(user):
+ """Sends the password reset notice email for the specified user.
+
+ :param user: The user to send the notice to
+ """
+ send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email,
+ 'reset_notice', user=user)
+
+
+def generate_reset_password_token(user):
+ """Generates a unique reset password token for the specified user.
+
+ :param user: The user to work with
+ """
+ data = [str(user.id), md5(user.password)]
+ return _security.reset_serializer.dumps(data)
+
+
+def reset_password_token_status(token):
+ """Returns the expired status, invalid status, and user of a password reset
+ token. For example::
+
+ expired, invalid, user = reset_password_token_status('...')
+
+ :param token: The password reset token
+ """
+ return get_token_status(token, 'reset', 'RESET_PASSWORD')
+
+def update_password(user, password):
+ """Update the specified user's password
+
+ :param user: The user to update_password
+ :param password: The unencrypted new password
+ """
+ user.password = encrypt_password(password)
+ _datastore.put(user)
+ send_password_reset_notice(user)
+ password_reset.send(user, app=app._get_current_object())
diff --git a/wqflask/flask_security/registerable.py b/wqflask/flask_security/registerable.py
new file mode 100644
index 00000000..4e9f357d
--- /dev/null
+++ b/wqflask/flask_security/registerable.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.registerable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security registerable module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app as app
+from werkzeug.local import LocalProxy
+
+from .confirmable import generate_confirmation_link
+from .signals import user_registered
+from .utils import do_flash, get_message, send_mail, encrypt_password, \
+ config_value
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions['security'])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def register_user(**kwargs):
+ confirmation_link, token = None, None
+ kwargs['password'] = encrypt_password(kwargs['password'])
+ user = _datastore.create_user(**kwargs)
+ _datastore.commit()
+
+ if _security.confirmable:
+ confirmation_link, token = generate_confirmation_link(user)
+ do_flash(*get_message('CONFIRM_REGISTRATION', email=user.email))
+
+ user_registered.send(dict(user=user, confirm_token=token),
+ app=app._get_current_object())
+
+ if config_value('SEND_REGISTER_EMAIL'):
+ send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome',
+ user=user, confirmation_link=confirmation_link)
+
+ return user
diff --git a/wqflask/flask_security/script.py b/wqflask/flask_security/script.py
new file mode 100644
index 00000000..9c9a2469
--- /dev/null
+++ b/wqflask/flask_security/script.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.script
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security script module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+import re
+
+from flask import current_app
+from flask.ext.script import Command, Option
+from werkzeug.local import LocalProxy
+
+from .utils import encrypt_password
+
+
+_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
+
+
+def pprint(obj):
+ print json.dumps(obj, sort_keys=True, indent=4)
+
+
+def commit(fn):
+ def wrapper(*args, **kwargs):
+ fn(*args, **kwargs)
+ _datastore.commit()
+ return wrapper
+
+
+class CreateUserCommand(Command):
+ """Create a user"""
+
+ option_list = (
+ Option('-e', '--email', dest='email', default=None),
+ Option('-p', '--password', dest='password', default=None),
+ Option('-a', '--active', dest='active', default=''),
+ )
+
+ @commit
+ def run(self, **kwargs):
+ # sanitize active input
+ ai = re.sub(r'\s', '', str(kwargs['active']))
+ kwargs['active'] = ai.lower() in ['', 'y', 'yes', '1', 'active']
+
+ from flask_security.forms import ConfirmRegisterForm
+ from werkzeug.datastructures import MultiDict
+
+ form = ConfirmRegisterForm(MultiDict(kwargs), csrf_enabled=False)
+
+ if form.validate():
+ kwargs['password'] = encrypt_password(kwargs['password'])
+ _datastore.create_user(**kwargs)
+ print 'User created successfully.'
+ kwargs['password'] = '****'
+ pprint(kwargs)
+ else:
+ print 'Error creating user'
+ pprint(form.errors)
+
+
+class CreateRoleCommand(Command):
+ """Create a role"""
+
+ option_list = (
+ Option('-n', '--name', dest='name', default=None),
+ Option('-d', '--desc', dest='description', default=None),
+ )
+
+ @commit
+ def run(self, **kwargs):
+ _datastore.create_role(**kwargs)
+ print 'Role "%(name)s" created successfully.' % kwargs
+
+
+class _RoleCommand(Command):
+ option_list = (
+ Option('-u', '--user', dest='user_identifier'),
+ Option('-r', '--role', dest='role_name'),
+ )
+
+
+class AddRoleCommand(_RoleCommand):
+ """Add a role to a user"""
+
+ @commit
+ def run(self, user_identifier, role_name):
+ _datastore.add_role_to_user(user_identifier, role_name)
+ print "Role '%s' added to user '%s' successfully" % (role_name, user_identifier)
+
+
+class RemoveRoleCommand(_RoleCommand):
+ """Add a role to a user"""
+
+ @commit
+ def run(self, user_identifier, role_name):
+ _datastore.remove_role_from_user(user_identifier, role_name)
+ print "Role '%s' removed from user '%s' successfully" % (role_name, user_identifier)
+
+
+class _ToggleActiveCommand(Command):
+ option_list = (
+ Option('-u', '--user', dest='user_identifier'),
+ )
+
+
+class DeactivateUserCommand(_ToggleActiveCommand):
+ """Deactive a user"""
+
+ @commit
+ def run(self, user_identifier):
+ _datastore.deactivate_user(user_identifier)
+ print "User '%s' has been deactivated" % user_identifier
+
+
+class ActivateUserCommand(_ToggleActiveCommand):
+ """Deactive a user"""
+
+ @commit
+ def run(self, user_identifier):
+ _datastore.activate_user(user_identifier)
+ print "User '%s' has been activated" % user_identifier
diff --git a/wqflask/flask_security/signals.py b/wqflask/flask_security/signals.py
new file mode 100644
index 00000000..e1c29548
--- /dev/null
+++ b/wqflask/flask_security/signals.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.signals
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security signals module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+import blinker
+
+
+signals = blinker.Namespace()
+
+user_registered = signals.signal("user-registered")
+
+user_confirmed = signals.signal("user-confirmed")
+
+confirm_instructions_sent = signals.signal("confirm-instructions-sent")
+
+login_instructions_sent = signals.signal("login-instructions-sent")
+
+password_reset = signals.signal("password-reset")
+
+password_changed = signals.signal("password-changed")
+
+reset_password_instructions_sent = signals.signal("password-reset-instructions-sent")
diff --git a/wqflask/flask_security/templates/.DS_Store b/wqflask/flask_security/templates/.DS_Store
new file mode 100644
index 00000000..b72f1d98
--- /dev/null
+++ b/wqflask/flask_security/templates/.DS_Store
Binary files differ
diff --git a/wqflask/flask_security/templates/security/.DS_Store b/wqflask/flask_security/templates/security/.DS_Store
new file mode 100644
index 00000000..5008ddfc
--- /dev/null
+++ b/wqflask/flask_security/templates/security/.DS_Store
Binary files differ
diff --git a/wqflask/flask_security/templates/security/_macros.html b/wqflask/flask_security/templates/security/_macros.html
new file mode 100644
index 00000000..8575f3db
--- /dev/null
+++ b/wqflask/flask_security/templates/security/_macros.html
@@ -0,0 +1,16 @@
+{% macro render_field_with_errors(field) %}
+ <p>
+ {{ field.label }} {{ field(**kwargs)|safe }}
+ {% if field.errors %}
+ <ul>
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ </p>
+{% endmacro %}
+
+{% macro render_field(field) %}
+ <p>{{ field(**kwargs)|safe }}</p>
+{% endmacro %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/_menu.html b/wqflask/flask_security/templates/security/_menu.html
new file mode 100644
index 00000000..5291f809
--- /dev/null
+++ b/wqflask/flask_security/templates/security/_menu.html
@@ -0,0 +1,15 @@
+{% if security.registerable or security.recoverable or security.confirmabled %}
+<h2>Menu</h2>
+<ul>
+ <li><a href="{{ url_for_security('login') }}">Login</a></li>
+ {% if security.registerable %}
+ <li><a href="{{ url_for_security('register') }}">Register</a><br/></li>
+ {% endif %}
+ {% if security.recoverable %}
+ <li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
+ {% endif %}
+ {% if security.confirmable %}
+ <li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li>
+ {% endif %}
+</ul>
+{% endif %}
diff --git a/wqflask/flask_security/templates/security/_messages.html b/wqflask/flask_security/templates/security/_messages.html
new file mode 100644
index 00000000..179d0636
--- /dev/null
+++ b/wqflask/flask_security/templates/security/_messages.html
@@ -0,0 +1,9 @@
+{%- with messages = get_flashed_messages(with_categories=true) -%}
+ {% if messages %}
+ <ul class="flashes">
+ {% for category, message in messages %}
+ <li class="{{ category }}">{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+{%- endwith %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/change_password.html b/wqflask/flask_security/templates/security/change_password.html
new file mode 100644
index 00000000..8ee3eb73
--- /dev/null
+++ b/wqflask/flask_security/templates/security/change_password.html
@@ -0,0 +1,11 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+<h1>Change password</h1>
+<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
+ {{ change_password_form.hidden_tag() }}
+ {{ render_field_with_errors(change_password_form.password) }}
+ {{ render_field_with_errors(change_password_form.new_password) }}
+ {{ render_field_with_errors(change_password_form.new_password_confirm) }}
+ {{ render_field(change_password_form.submit) }}
+</form>
+
diff --git a/wqflask/flask_security/templates/security/email/change_notice.html b/wqflask/flask_security/templates/security/email/change_notice.html
new file mode 100644
index 00000000..d1224cf5
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/change_notice.html
@@ -0,0 +1,4 @@
+<p>Your password has been changed.</p>
+{% if security.recoverable %}
+<p>If you did not change your password, <a href="{{ url_for_security('forgot_password', _external=True) }}">click here to reset it</a>.</p>
+{% endif %}
diff --git a/wqflask/flask_security/templates/security/email/change_notice.txt b/wqflask/flask_security/templates/security/email/change_notice.txt
new file mode 100644
index 00000000..e74bd80d
--- /dev/null
+++ b/wqflask/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/wqflask/flask_security/templates/security/email/confirmation_instructions.html b/wqflask/flask_security/templates/security/email/confirmation_instructions.html
new file mode 100644
index 00000000..5082a9a8
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/confirmation_instructions.html
@@ -0,0 +1,3 @@
+<p>Please confirm your email through the link below:</p>
+
+<p><a href="{{ confirmation_link }}">Confirm my account</a></p> \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/confirmation_instructions.txt b/wqflask/flask_security/templates/security/email/confirmation_instructions.txt
new file mode 100644
index 00000000..fb435b55
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/confirmation_instructions.txt
@@ -0,0 +1,3 @@
+Please confirm your email through the link below:
+
+{{ confirmation_link }} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/login_instructions.html b/wqflask/flask_security/templates/security/email/login_instructions.html
new file mode 100644
index 00000000..45a7cb57
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/login_instructions.html
@@ -0,0 +1,5 @@
+<p>Welcome {{ user.email }}!</p>
+
+<p>You can log into your through the link below:</p>
+
+<p><a href="{{ login_link }}">Login now</a></p> \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/login_instructions.txt b/wqflask/flask_security/templates/security/email/login_instructions.txt
new file mode 100644
index 00000000..1364ed65
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/login_instructions.txt
@@ -0,0 +1,5 @@
+Welcome {{ user.email }}!
+
+You can log into your through the link below:
+
+{{ login_link }} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/reset_instructions.html b/wqflask/flask_security/templates/security/email/reset_instructions.html
new file mode 100644
index 00000000..fd0b48d8
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/reset_instructions.html
@@ -0,0 +1 @@
+<p><a href="{{ reset_link }}">Click here to reset your password</a></p> \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/reset_instructions.txt b/wqflask/flask_security/templates/security/email/reset_instructions.txt
new file mode 100644
index 00000000..91ac288e
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/reset_instructions.txt
@@ -0,0 +1,3 @@
+Click the link below to reset your password:
+
+{{ reset_link }} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/reset_notice.html b/wqflask/flask_security/templates/security/email/reset_notice.html
new file mode 100644
index 00000000..536e2961
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/reset_notice.html
@@ -0,0 +1 @@
+<p>Your password has been reset</p> \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/reset_notice.txt b/wqflask/flask_security/templates/security/email/reset_notice.txt
new file mode 100644
index 00000000..a3fa0b4b
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/reset_notice.txt
@@ -0,0 +1 @@
+Your password has been reset \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/welcome.html b/wqflask/flask_security/templates/security/email/welcome.html
new file mode 100644
index 00000000..55eaed61
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/welcome.html
@@ -0,0 +1,7 @@
+<p>Welcome {{ user.email }}!</p>
+
+{% if security.confirmable %}
+<p>You can confirm your email through the link below:</p>
+
+<p><a href="{{ confirmation_link }}">Confirm my account</a></p>
+{% endif %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/email/welcome.txt b/wqflask/flask_security/templates/security/email/welcome.txt
new file mode 100644
index 00000000..fb6ee5b5
--- /dev/null
+++ b/wqflask/flask_security/templates/security/email/welcome.txt
@@ -0,0 +1,7 @@
+Welcome {{ user.email }}!
+
+{% if security.confirmable %}
+You can confirm your email through the link below:
+
+{{ confirmation_link }}
+{% endif %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/forgot_password.html b/wqflask/flask_security/templates/security/forgot_password.html
new file mode 100644
index 00000000..90fcaf66
--- /dev/null
+++ b/wqflask/flask_security/templates/security/forgot_password.html
@@ -0,0 +1,9 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+<h1>Send password reset instructions</h1>
+<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
+ {{ forgot_password_form.hidden_tag() }}
+ {{ render_field_with_errors(forgot_password_form.email) }}
+ {{ render_field(forgot_password_form.submit) }}
+</form>
+{% include "security/_menu.html" %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/login_user.html b/wqflask/flask_security/templates/security/login_user.html
new file mode 100644
index 00000000..d781ce08
--- /dev/null
+++ b/wqflask/flask_security/templates/security/login_user.html
@@ -0,0 +1,12 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+<h1>Login</h1>
+<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
+ {{ login_user_form.hidden_tag() }}
+ {{ render_field_with_errors(login_user_form.email) }}
+ {{ render_field_with_errors(login_user_form.password) }}
+ {{ render_field_with_errors(login_user_form.remember) }}
+ {{ render_field(login_user_form.next) }}
+ {{ render_field(login_user_form.submit) }}
+</form>
+{% include "security/_menu.html" %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/register_user.html b/wqflask/flask_security/templates/security/register_user.html
new file mode 100644
index 00000000..87cf9b1d
--- /dev/null
+++ b/wqflask/flask_security/templates/security/register_user.html
@@ -0,0 +1,13 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+<h1>Register</h1>
+<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
+ {{ register_user_form.hidden_tag() }}
+ {{ render_field_with_errors(register_user_form.email) }}
+ {{ render_field_with_errors(register_user_form.password) }}
+ {% if register_user_form.password_confirm %}
+ {{ render_field_with_errors(register_user_form.password_confirm) }}
+ {% endif %}
+ {{ render_field(register_user_form.submit) }}
+</form>
+{% include "security/_menu.html" %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/reset_password.html b/wqflask/flask_security/templates/security/reset_password.html
new file mode 100644
index 00000000..e6fc3f58
--- /dev/null
+++ b/wqflask/flask_security/templates/security/reset_password.html
@@ -0,0 +1,10 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+<h1>Reset password</h1>
+<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form">
+ {{ reset_password_form.hidden_tag() }}
+ {{ render_field_with_errors(reset_password_form.password) }}
+ {{ render_field_with_errors(reset_password_form.password_confirm) }}
+ {{ render_field(reset_password_form.submit) }}
+</form>
+{% include "security/_menu.html" %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/send_confirmation.html b/wqflask/flask_security/templates/security/send_confirmation.html
new file mode 100644
index 00000000..3e828407
--- /dev/null
+++ b/wqflask/flask_security/templates/security/send_confirmation.html
@@ -0,0 +1,9 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+<h1>Resend confirmation instructions</h1>
+<form action="{{ url_for_security('send_confirmation') }}" method="POST" name="send_confirmation_form">
+ {{ send_confirmation_form.hidden_tag() }}
+ {{ render_field_with_errors(send_confirmation_form.email) }}
+ {{ render_field(send_confirmation_form.submit) }}
+</form>
+{% include "security/_menu.html" %} \ No newline at end of file
diff --git a/wqflask/flask_security/templates/security/send_login.html b/wqflask/flask_security/templates/security/send_login.html
new file mode 100644
index 00000000..15611c57
--- /dev/null
+++ b/wqflask/flask_security/templates/security/send_login.html
@@ -0,0 +1,9 @@
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+{% include "security/_messages.html" %}
+<h1>Login</h1>
+<form action="{{ url_for_security('login') }}" method="POST" name="send_login_form">
+ {{ send_login_form.hidden_tag() }}
+ {{ render_field_with_errors(send_login_form.email) }}
+ {{ render_field(send_login_form.submit) }}
+</form>
+{% include "security/_menu.html" %} \ No newline at end of file
diff --git a/wqflask/flask_security/utils.py b/wqflask/flask_security/utils.py
new file mode 100644
index 00000000..7397ab4f
--- /dev/null
+++ b/wqflask/flask_security/utils.py
@@ -0,0 +1,379 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.utils
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security utils module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+import base64
+import blinker
+import functools
+import hashlib
+import hmac
+from contextlib import contextmanager
+from datetime import datetime, timedelta
+
+from flask import url_for, flash, current_app, request, session, render_template
+from flask.ext.login import login_user as _login_user, \
+ logout_user as _logout_user
+from flask.ext.mail import Message
+from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
+from itsdangerous import BadSignature, SignatureExpired
+from werkzeug.local import LocalProxy
+
+from .signals import user_registered, user_confirmed, \
+ confirm_instructions_sent, login_instructions_sent, \
+ password_reset, password_changed, reset_password_instructions_sent
+
+# Convenient references
+_security = LocalProxy(lambda: current_app.extensions['security'])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+_pwd_context = LocalProxy(lambda: _security.pwd_context)
+
+
+def login_user(user, remember=True):
+ """Performs the login and sends the appropriate signal."""
+
+ if not _login_user(user, remember):
+ return False
+
+ if _security.trackable:
+ old_current_login, new_current_login = user.current_login_at, datetime.utcnow()
+ remote_addr = request.remote_addr or 'untrackable'
+ old_current_ip, new_current_ip = user.current_login_ip, remote_addr
+
+ user.last_login_at = old_current_login or new_current_login
+ user.current_login_at = new_current_login
+ user.last_login_ip = old_current_ip or new_current_ip
+ user.current_login_ip = new_current_ip
+ user.login_count = user.login_count + 1 if user.login_count else 1
+
+ _datastore.put(user)
+
+ identity_changed.send(current_app._get_current_object(),
+ identity=Identity(user.id))
+ return True
+
+
+def logout_user():
+ for key in ('identity.name', 'identity.auth_type'):
+ session.pop(key, None)
+ identity_changed.send(current_app._get_current_object(),
+ identity=AnonymousIdentity())
+ _logout_user()
+
+
+def get_hmac(password):
+ if _security.password_hash == 'plaintext':
+ return password
+
+ if _security.password_salt is None:
+ raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` '
+ 'must not be None when the value of `SECURITY_PASSWORD_HASH` is '
+ 'set to "%s"' % _security.password_hash)
+
+ h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512)
+ return base64.b64encode(h.digest())
+
+
+def verify_password(password, password_hash):
+ return _pwd_context.verify(get_hmac(password), password_hash)
+
+
+def verify_and_update_password(password, user):
+ verified, new_password = _pwd_context.verify_and_update(get_hmac(password), user.password)
+ if verified and new_password:
+ user.password = new_password
+ _datastore.put(user)
+ return verified
+
+
+def encrypt_password(password):
+ return _pwd_context.encrypt(get_hmac(password))
+
+
+def md5(data):
+ return hashlib.md5(data).hexdigest()
+
+
+def do_flash(message, category=None):
+ """Flash a message depending on if the `FLASH_MESSAGES` configuration
+ value is set.
+
+ :param message: The flash message
+ :param category: The flash message category
+ """
+ if config_value('FLASH_MESSAGES'):
+ flash(message, category)
+
+
+def get_url(endpoint_or_url):
+ """Returns a URL if a valid endpoint is found. Otherwise, returns the
+ provided value.
+
+ :param endpoint_or_url: The endpoint name or URL to default to
+ """
+ try:
+ return url_for(endpoint_or_url)
+ except:
+ return endpoint_or_url
+
+
+def get_security_endpoint_name(endpoint):
+ return '%s.%s' % (_security.blueprint_name, endpoint)
+
+
+def url_for_security(endpoint, **values):
+ """Return a URL for the security blueprint
+
+ :param endpoint: the endpoint of the URL (name of the function)
+ :param values: the variable arguments of the URL rule
+ :param _external: if set to `True`, an absolute URL is generated. Server
+ address can be changed via `SERVER_NAME` configuration variable which
+ defaults to `localhost`.
+ :param _anchor: if provided this is added as anchor to the URL.
+ :param _method: if provided this explicitly specifies an HTTP method.
+ """
+ endpoint = get_security_endpoint_name(endpoint)
+ return url_for(endpoint, **values)
+
+
+def get_post_login_redirect():
+ """Returns the URL to redirect to after a user logs in successfully."""
+ return (get_url(request.args.get('next')) or
+ get_url(request.form.get('next')) or
+ find_redirect('SECURITY_POST_LOGIN_VIEW'))
+
+
+def find_redirect(key):
+ """Returns the URL to redirect to after a user logs in successfully.
+
+ :param key: The session or application configuration key to search for
+ """
+ rv = (get_url(session.pop(key.lower(), None)) or
+ get_url(current_app.config[key.upper()] or None) or '/')
+ return rv
+
+
+def get_config(app):
+ """Conveniently get the security configuration for the specified
+ application without the annoying 'SECURITY_' prefix.
+
+ :param app: The application to inspect
+ """
+ items = app.config.items()
+ prefix = 'SECURITY_'
+
+ def strip_prefix(tup):
+ return (tup[0].replace('SECURITY_', ''), tup[1])
+
+ return dict([strip_prefix(i) for i in items if i[0].startswith(prefix)])
+
+
+def get_message(key, **kwargs):
+ rv = config_value('MSG_' + key)
+ return rv[0] % kwargs, rv[1]
+
+
+def config_value(key, app=None, default=None):
+ """Get a Flask-Security configuration value.
+
+ :param key: The configuration key without the prefix `SECURITY_`
+ :param app: An optional specific application to inspect. Defaults to Flask's
+ `current_app`
+ :param default: An optional default value if the value is not set
+ """
+ app = app or current_app
+ return get_config(app).get(key.upper(), default)
+
+
+def get_max_age(key, app=None):
+ now = datetime.utcnow()
+ expires = now + get_within_delta(key + '_WITHIN', app)
+ td = (expires - now)
+ return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6
+
+
+def get_within_delta(key, app=None):
+ """Get a timedelta object from the application configuration following
+ the internal convention of::
+
+ <Amount of Units> <Type of Units>
+
+ Examples of valid config values::
+
+ 5 days
+ 10 minutes
+
+ :param key: The config value key without the 'SECURITY_' prefix
+ :param app: Optional application to inspect. Defaults to Flask's
+ `current_app`
+ """
+ txt = config_value(key, app=app)
+ values = txt.split()
+ return timedelta(**{values[1]: int(values[0])})
+
+
+def send_mail(subject, recipient, template, **context):
+ """Send an email via the Flask-Mail extension.
+
+ :param subject: Email subject
+ :param recipient: Email recipient
+ :param template: The name of the email template
+ :param context: The context to render the template with
+ """
+
+ context.setdefault('security', _security)
+ context.update(_security._run_ctx_processor('mail'))
+
+ msg = Message(subject,
+ sender=_security.email_sender,
+ recipients=[recipient])
+
+ ctx = ('security/email', template)
+ msg.body = render_template('%s/%s.txt' % ctx, **context)
+ msg.html = render_template('%s/%s.html' % ctx, **context)
+
+ if _security._send_mail_task:
+ _security._send_mail_task(msg)
+ return
+
+ mail = current_app.extensions.get('mail')
+ mail.send(msg)
+
+
+def get_token_status(token, serializer, max_age=None):
+ serializer = getattr(_security, serializer + '_serializer')
+ max_age = get_max_age(max_age)
+ user, data = None, None
+ expired, invalid = False, False
+
+ try:
+ data = serializer.loads(token, max_age=max_age)
+ except SignatureExpired:
+ d, data = serializer.loads_unsafe(token)
+ expired = True
+ except BadSignature:
+ invalid = True
+
+ if data:
+ user = _datastore.find_user(id=data[0])
+
+ expired = expired and (user is not None)
+ return expired, invalid, user
+
+
+@contextmanager
+def capture_passwordless_login_requests():
+ login_requests = []
+
+ def _on(data, app):
+ login_requests.append(data)
+
+ login_instructions_sent.connect(_on)
+
+ try:
+ yield login_requests
+ finally:
+ login_instructions_sent.disconnect(_on)
+
+
+@contextmanager
+def capture_registrations():
+ """Testing utility for capturing registrations.
+
+ :param confirmation_sent_at: An optional datetime object to set the
+ user's `confirmation_sent_at` to
+ """
+ registrations = []
+
+ def _on(data, app):
+ registrations.append(data)
+
+ user_registered.connect(_on)
+
+ try:
+ yield registrations
+ finally:
+ user_registered.disconnect(_on)
+
+
+@contextmanager
+def capture_reset_password_requests(reset_password_sent_at=None):
+ """Testing utility for capturing password reset requests.
+
+ :param reset_password_sent_at: An optional datetime object to set the
+ user's `reset_password_sent_at` to
+ """
+ reset_requests = []
+
+ def _on(request, app):
+ reset_requests.append(request)
+
+ reset_password_instructions_sent.connect(_on)
+
+ try:
+ yield reset_requests
+ finally:
+ reset_password_instructions_sent.disconnect(_on)
+
+
+class CaptureSignals(object):
+ """Testing utility for capturing blinker signals.
+
+ Context manager which mocks out selected signals and registers which are `sent` on and what
+ arguments were sent. Instantiate with a list of blinker `NamedSignals` to patch. Each signal
+ has it's `send` mocked out.
+ """
+ def __init__(self, signals):
+ """Patch all given signals and make them available as attributes.
+
+ :param signals: list of signals
+ """
+ self._records = {}
+ self._receivers = {}
+ for signal in signals:
+ self._records[signal] = []
+ self._receivers[signal] = functools.partial(self._record, signal)
+
+ def __getitem__(self, signal):
+ """All captured signals are available via `ctxt[signal]`.
+ """
+ if isinstance(signal, blinker.base.NamedSignal):
+ return self._records[signal]
+ else:
+ super(CaptureSignals, self).__setitem__(signal)
+
+ def _record(self, signal, *args, **kwargs):
+ self._records[signal].append((args, kwargs))
+
+ def __enter__(self):
+ for signal, receiver in self._receivers.iteritems():
+ signal.connect(receiver)
+ return self
+
+ def __exit__(self, type, value, traceback):
+ for signal, receiver in self._receivers.iteritems():
+ signal.disconnect(receiver)
+
+ def signals_sent(self):
+ """Return a set of the signals sent.
+ :rtype: list of blinker `NamedSignals`.
+ """
+ return set([signal for signal, _ in self._records.iteritems() if self._records[signal]])
+
+
+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, password_changed,
+ reset_password_instructions_sent])
+
+
diff --git a/wqflask/flask_security/views.py b/wqflask/flask_security/views.py
new file mode 100644
index 00000000..1b8488d8
--- /dev/null
+++ b/wqflask/flask_security/views.py
@@ -0,0 +1,359 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.security.views
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security views module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+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
+
+from .confirmable import send_confirmation_instructions, \
+ confirm_user, confirm_email_token_status
+from .decorators import login_required, anonymous_user_required
+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, \
+ config_value
+
+
+# Convenient references
+_security = LocalProxy(lambda: current_app.extensions['security'])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def _render_json(form, include_auth_token=False):
+ has_errors = len(form.errors) > 0
+
+ if has_errors:
+ code = 400
+ response = dict(errors=form.errors)
+ else:
+ code = 200
+ response = dict(user=dict(id=str(form.user.id)))
+ if include_auth_token:
+ token = form.user.get_auth_token()
+ response['user']['authentication_token'] = token
+
+ return jsonify(dict(meta=dict(code=code), response=response))
+
+
+def _commit(response=None):
+ _datastore.commit()
+ return response
+
+
+def _ctx(endpoint):
+ return _security._run_ctx_processor(endpoint)
+
+
+@anonymous_user_required
+def login():
+ """View function for login view"""
+
+ form_class = _security.login_form
+
+ if request.json:
+ form = form_class(MultiDict(request.json))
+ else:
+ form = form_class()
+
+ if form.validate_on_submit():
+ login_user(form.user, remember=form.remember.data)
+ after_this_request(_commit)
+
+ if not request.json:
+ return redirect(get_post_login_redirect())
+
+ form.next.data = get_url(request.args.get('next')) \
+ or get_url(request.form.get('next')) or ''
+
+ if request.json:
+ return _render_json(form, True)
+
+ return render_template(config_value('LOGIN_USER_TEMPLATE'),
+ login_user_form=form,
+ **_ctx('login'))
+
+
+@login_required
+def logout():
+ """View function which handles a logout request."""
+
+ logout_user()
+
+ return redirect(request.args.get('next', None) or
+ get_url(_security.post_logout_view))
+
+
+def register():
+ """View function which handles a registration request."""
+
+ if _security.confirmable or request.json:
+ form_class = _security.confirm_register_form
+ else:
+ form_class = _security.register_form
+
+ if request.json:
+ form_data = MultiDict(request.json)
+ else:
+ form_data = request.form
+
+ form = form_class(form_data)
+
+ if form.validate_on_submit():
+ user = register_user(**form.to_dict())
+ form.user = user
+
+ if not _security.confirmable or _security.login_without_confirmation:
+ after_this_request(_commit)
+ login_user(user)
+
+ if not request.json:
+ post_register_url = get_url(_security.post_register_view)
+ post_login_url = get_url(_security.post_login_view)
+ return redirect(post_register_url or post_login_url)
+
+ if request.json:
+ return _render_json(form)
+
+ return render_template(config_value('REGISTER_USER_TEMPLATE'),
+ register_user_form=form,
+ **_ctx('register'))
+
+
+def send_login():
+ """View function that sends login instructions for passwordless login"""
+
+ form_class = _security.passwordless_login_form
+
+ if request.json:
+ form = form_class(MultiDict(request.json))
+ else:
+ form = form_class()
+
+ if form.validate_on_submit():
+ send_login_instructions(form.user)
+ if request.json is None:
+ do_flash(*get_message('LOGIN_EMAIL_SENT', email=form.user.email))
+
+ if request.json:
+ return _render_json(form)
+
+ return render_template(config_value('SEND_LOGIN_TEMPLATE'),
+ send_login_form=form,
+ **_ctx('send_login'))
+
+
+@anonymous_user_required
+def token_login(token):
+ """View function that handles passwordless login via a token"""
+
+ expired, invalid, user = login_token_status(token)
+
+ if invalid:
+ do_flash(*get_message('INVALID_LOGIN_TOKEN'))
+ if expired:
+ send_login_instructions(user)
+ do_flash(*get_message('LOGIN_EXPIRED', email=user.email,
+ within=_security.login_within))
+ if invalid or expired:
+ return redirect(url_for('login'))
+
+ login_user(user, True)
+ after_this_request(_commit)
+ do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'))
+
+ return redirect(get_post_login_redirect())
+
+
+def send_confirmation():
+ """View function which sends confirmation instructions."""
+
+ form_class = _security.send_confirmation_form
+
+ if request.json:
+ form = form_class(MultiDict(request.json))
+ else:
+ form = form_class()
+
+ if form.validate_on_submit():
+ send_confirmation_instructions(form.user)
+ if request.json is None:
+ do_flash(*get_message('CONFIRMATION_REQUEST', email=form.user.email))
+
+ if request.json:
+ return _render_json(form)
+
+ return render_template(config_value('SEND_CONFIRMATION_TEMPLATE'),
+ send_confirmation_form=form,
+ **_ctx('send_confirmation'))
+
+
+@anonymous_user_required
+def confirm_email(token):
+ """View function which handles a email confirmation request."""
+
+ expired, invalid, user = confirm_email_token_status(token)
+
+ if not user or invalid:
+ invalid = True
+ do_flash(*get_message('INVALID_CONFIRMATION_TOKEN'))
+ if expired:
+ send_confirmation_instructions(user)
+ do_flash(*get_message('CONFIRMATION_EXPIRED', email=user.email,
+ within=_security.confirm_email_within))
+ if invalid or expired:
+ return redirect(get_url(_security.confirm_error_view) or
+ url_for('send_confirmation'))
+
+ confirm_user(user)
+ login_user(user, True)
+ after_this_request(_commit)
+ do_flash(*get_message('EMAIL_CONFIRMED'))
+
+ return redirect(get_url(_security.post_confirm_view) or
+ get_url(_security.post_login_view))
+
+
+def forgot_password():
+ """View function that handles a forgotten password request."""
+
+ form_class = _security.forgot_password_form
+
+ if request.json:
+ form = form_class(MultiDict(request.json))
+ else:
+ form = form_class()
+
+ if form.validate_on_submit():
+ send_reset_password_instructions(form.user)
+ if request.json is None:
+ do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
+
+ if request.json:
+ return _render_json(form)
+
+ return render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
+ forgot_password_form=form,
+ **_ctx('forgot_password'))
+
+
+@anonymous_user_required
+def reset_password(token):
+ """View function that handles a reset password request."""
+
+ expired, invalid, user = reset_password_token_status(token)
+
+ if invalid:
+ do_flash(*get_message('INVALID_RESET_PASSWORD_TOKEN'))
+ if expired:
+ do_flash(*get_message('PASSWORD_RESET_EXPIRED', email=user.email,
+ within=_security.reset_password_within))
+ if invalid or expired:
+ return redirect(url_for('forgot_password'))
+
+ form = _security.reset_password_form()
+
+ if form.validate_on_submit():
+ after_this_request(_commit)
+ update_password(user, form.password.data)
+ do_flash(*get_message('PASSWORD_RESET'))
+ login_user(user, True)
+ return redirect(get_url(_security.post_reset_view) or
+ get_url(_security.post_login_view))
+
+ return render_template(config_value('RESET_PASSWORD_TEMPLATE'),
+ reset_password_form=form,
+ reset_password_token=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"""
+
+ bp = Blueprint(state.blueprint_name, import_name,
+ url_prefix=state.url_prefix,
+ subdomain=state.subdomain,
+ template_folder='templates')
+
+ bp.route(state.logout_url, endpoint='logout')(logout)
+
+ if state.passwordless:
+ bp.route(state.login_url,
+ methods=['GET', 'POST'],
+ endpoint='login')(send_login)
+ bp.route(state.login_url + '/<token>',
+ endpoint='token_login')(token_login)
+ else:
+ bp.route(state.login_url,
+ methods=['GET', 'POST'],
+ endpoint='login')(login)
+
+ if state.registerable:
+ bp.route(state.register_url,
+ methods=['GET', 'POST'],
+ endpoint='register')(register)
+
+ if state.recoverable:
+ bp.route(state.reset_url,
+ methods=['GET', 'POST'],
+ endpoint='forgot_password')(forgot_password)
+ bp.route(state.reset_url + '/<token>',
+ 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'],
+ endpoint='send_confirmation')(send_confirmation)
+ bp.route(state.confirm_url + '/<token>',
+ methods=['GET', 'POST'],
+ endpoint='confirm_email')(confirm_email)
+
+ return bp