From 48ba921c234abb218b14d7f1ae4d5b017ab8d388 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 17 May 2013 20:59:50 +0000 Subject: Added flask_security locally --- wqflask/flask_security/__init__.py | 23 ++ wqflask/flask_security/changeable.py | 45 +++ wqflask/flask_security/confirmable.py | 83 +++++ wqflask/flask_security/core.py | 381 +++++++++++++++++++++ wqflask/flask_security/datastore.py | 260 ++++++++++++++ wqflask/flask_security/decorators.py | 207 +++++++++++ wqflask/flask_security/forms.py | 277 +++++++++++++++ wqflask/flask_security/passwordless.py | 59 ++++ wqflask/flask_security/recoverable.py | 80 +++++ wqflask/flask_security/registerable.py | 43 +++ wqflask/flask_security/script.py | 130 +++++++ wqflask/flask_security/signals.py | 29 ++ wqflask/flask_security/templates/.DS_Store | Bin 0 -> 6148 bytes .../flask_security/templates/security/.DS_Store | Bin 0 -> 6148 bytes .../flask_security/templates/security/_macros.html | 16 + .../flask_security/templates/security/_menu.html | 15 + .../templates/security/_messages.html | 9 + .../templates/security/change_password.html | 11 + .../templates/security/email/change_notice.html | 4 + .../templates/security/email/change_notice.txt | 5 + .../security/email/confirmation_instructions.html | 3 + .../security/email/confirmation_instructions.txt | 3 + .../security/email/login_instructions.html | 5 + .../security/email/login_instructions.txt | 5 + .../security/email/reset_instructions.html | 1 + .../security/email/reset_instructions.txt | 3 + .../templates/security/email/reset_notice.html | 1 + .../templates/security/email/reset_notice.txt | 1 + .../templates/security/email/welcome.html | 7 + .../templates/security/email/welcome.txt | 7 + .../templates/security/forgot_password.html | 9 + .../templates/security/login_user.html | 12 + .../templates/security/register_user.html | 13 + .../templates/security/reset_password.html | 10 + .../templates/security/send_confirmation.html | 9 + .../templates/security/send_login.html | 9 + wqflask/flask_security/utils.py | 379 ++++++++++++++++++++ wqflask/flask_security/views.py | 359 +++++++++++++++++++ wqflask/wqflask/templates/security/_macros.html | 48 ++- wqflask/wqflask/templates/security/login_user.html | 7 +- wqflask/wqflask/views.py | 3 + 41 files changed, 2550 insertions(+), 21 deletions(-) create mode 100644 wqflask/flask_security/__init__.py create mode 100644 wqflask/flask_security/changeable.py create mode 100644 wqflask/flask_security/confirmable.py create mode 100644 wqflask/flask_security/core.py create mode 100644 wqflask/flask_security/datastore.py create mode 100644 wqflask/flask_security/decorators.py create mode 100644 wqflask/flask_security/forms.py create mode 100644 wqflask/flask_security/passwordless.py create mode 100644 wqflask/flask_security/recoverable.py create mode 100644 wqflask/flask_security/registerable.py create mode 100644 wqflask/flask_security/script.py create mode 100644 wqflask/flask_security/signals.py create mode 100644 wqflask/flask_security/templates/.DS_Store create mode 100644 wqflask/flask_security/templates/security/.DS_Store create mode 100644 wqflask/flask_security/templates/security/_macros.html create mode 100644 wqflask/flask_security/templates/security/_menu.html create mode 100644 wqflask/flask_security/templates/security/_messages.html create mode 100644 wqflask/flask_security/templates/security/change_password.html create mode 100644 wqflask/flask_security/templates/security/email/change_notice.html create mode 100644 wqflask/flask_security/templates/security/email/change_notice.txt create mode 100644 wqflask/flask_security/templates/security/email/confirmation_instructions.html create mode 100644 wqflask/flask_security/templates/security/email/confirmation_instructions.txt create mode 100644 wqflask/flask_security/templates/security/email/login_instructions.html create mode 100644 wqflask/flask_security/templates/security/email/login_instructions.txt create mode 100644 wqflask/flask_security/templates/security/email/reset_instructions.html create mode 100644 wqflask/flask_security/templates/security/email/reset_instructions.txt create mode 100644 wqflask/flask_security/templates/security/email/reset_notice.html create mode 100644 wqflask/flask_security/templates/security/email/reset_notice.txt create mode 100644 wqflask/flask_security/templates/security/email/welcome.html create mode 100644 wqflask/flask_security/templates/security/email/welcome.txt create mode 100644 wqflask/flask_security/templates/security/forgot_password.html create mode 100644 wqflask/flask_security/templates/security/login_user.html create mode 100644 wqflask/flask_security/templates/security/register_user.html create mode 100644 wqflask/flask_security/templates/security/reset_password.html create mode 100644 wqflask/flask_security/templates/security/send_confirmation.html create mode 100644 wqflask/flask_security/templates/security/send_login.html create mode 100644 wqflask/flask_security/utils.py create mode 100644 wqflask/flask_security/views.py (limited to 'wqflask') 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 = """ +
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.
+ """ + + +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 Binary files /dev/null and b/wqflask/flask_security/templates/.DS_Store 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 Binary files /dev/null and b/wqflask/flask_security/templates/security/.DS_Store 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) %} ++ {{ field.label }} {{ field(**kwargs)|safe }} + {% if field.errors %} +
{{ field(**kwargs)|safe }}
+{% 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 %} +Your password has been changed.
+{% if security.recoverable %} +If you did not change your password, click here to reset it.
+{% 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 @@ +Please confirm your email through the link below:
+ + \ 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 @@ +Welcome {{ user.email }}!
+ +You can log into your through the link below:
+ + \ 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 @@ +Click here to reset your password
\ 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 @@ +Your password has been reset
\ 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 @@ +Welcome {{ user.email }}!
+ +{% if security.confirmable %} +You can confirm your email through the link below:
+ + +{% 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" %} +- {{ field.label }} {{ field(**kwargs)|safe }} - {% if field.errors %} -
+ {{ field.label }} {{ field(**kwargs)|safe }} + {% if field.errors %} +
+
{% if field.errors %} -
You must confirm your email address before signing in.
+Check your email for confirmation instructions.
+Can't find the email? Check your spam folder.
+Still can't find it? + Click here to resend.
+{{ field(**kwargs)|safe }}
+{{ field(**kwargs)|safe }}
{% endmacro %} diff --git a/wqflask/wqflask/templates/security/login_user.html b/wqflask/wqflask/templates/security/login_user.html index d6f6fb63..f982dc08 100644 --- a/wqflask/wqflask/templates/security/login_user.html +++ b/wqflask/wqflask/templates/security/login_user.html @@ -2,6 +2,8 @@ {% include "security/_messages.html" %}