Flask-Security-1.7.5/ 0000755 0000765 0000024 00000000000 12627667302 014474 5 ustar matt staff 0000000 0000000 Flask-Security-1.7.5/flask_security/ 0000755 0000765 0000024 00000000000 12627667302 017523 5 ustar matt staff 0000000 0000000 Flask-Security-1.7.5/flask_security/__init__.py 0000644 0000765 0000024 00000001705 12627667276 021651 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_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. """ 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, auth_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 __version__ = '1.7.5' Flask-Security-1.7.5/flask_security/changeable.py 0000644 0000765 0000024 00000002475 12627667222 022157 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_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 from werkzeug.local import LocalProxy from .signals import password_changed from .utils import send_mail, encrypt_password, 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 """ if config_value('SEND_PASSWORD_CHANGE_EMAIL'): subject = config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE') send_mail(subject, 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(app._get_current_object(), user=user._get_current_object()) Flask-Security-1.7.5/flask_security/confirmable.py 0000644 0000765 0000024 00000004630 12627667222 022362 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_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 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) return url_for_security('confirm_email', token=token, _external=True), 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(app._get_current_object(), user=user) 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 not _security.login_without_confirmation and user.confirmed_at is 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 """ if user.confirmed_at is not None: return False user.confirmed_at = datetime.utcnow() _datastore.put(user) user_confirmed.send(app._get_current_object(), user=user) return True Flask-Security-1.7.5/flask_security/core.py 0000644 0000765 0000024 00000036646 12627667222 021045 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_security.core ~~~~~~~~~~~~~~~~~~~ Flask-Security core module :copyright: (c) 2012 by Matt Wright. :license: MIT, see LICENSE for more details. """ from flask import current_app, render_template from flask_login import AnonymousUserMixin, UserMixin as BaseUserMixin, \ LoginManager, current_user from flask_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 werkzeug.security import safe_str_cmp from .utils import config_value as cv, get_config, md5, url_for_security, string_types 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', 'CHANGE_PASSWORD_TEMPLATE': 'security/change_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, 'SEND_PASSWORD_CHANGE_EMAIL': True, 'SEND_PASSWORD_RESET_NOTICE_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', 'TOKEN_MAX_AGE': None, 'CONFIRM_SALT': 'confirm-salt', 'RESET_SALT': 'reset-salt', 'LOGIN_SALT': 'login-salt', 'CHANGE_SALT': 'change-salt', 'REMEMBER_SALT': 'remember-salt', 'DEFAULT_REMEMBER_ME': False, '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', 'USER_IDENTITY_ATTRIBUTES': ['email'], 'PASSWORD_SCHEMES': [ 'bcrypt', 'des_crypt', 'pbkdf2_sha256', 'pbkdf2_sha512', 'sha256_crypt', 'sha512_crypt', # And always last one... 'plaintext' ], 'DEPRECATED_PASSWORD_SCHEMES': ['auto'] } #: 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'), 'PASSWORD_NOT_SET': ( 'No password is set for this user', 'error'), 'PASSWORD_INVALID_LENGTH': ( 'Password must be at least 6 characters', '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_IS_THE_SAME': ( 'Your new password must be different than your previous password.', 'error'), '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'), } _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, max_age=_security.token_max_age) user = _security.datastore.find_user(id=data[0]) if user and safe_str_cmp(md5(user.password), data[1]): return user except: pass return _security.login_manager.anonymous_user() def _identity_loader(): if not isinstance(current_user._get_current_object(), AnonymousUserMixin): 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, anonymous_user): lm = LoginManager() lm.anonymous_user = anonymous_user or AnonymousUser lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app) lm.user_loader(_user_loader) lm.token_loader(_token_loader) if cv('FLASH_MESSAGES', app=app): 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) else: lm.login_message = None lm.needs_refresh_message = None 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) schemes = cv('PASSWORD_SCHEMES', app=app) deprecated = cv('DEPRECATED_PASSWORD_SCHEMES', app=app) if pw_hash not in schemes: allowed = (', '.join(schemes[:-1]) + ' and ' + schemes[-1]) raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed)) return CryptContext(schemes=schemes, default=pw_hash, deprecated=deprecated) 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, anonymous_user=None, **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, anonymous_user), 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, _unauthorized_callback=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 not self.__eq__(other) def __hash__(self): return hash(self.name) class UserMixin(BaseUserMixin): """Mixin for `User` model definitions""" @property 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""" if isinstance(role, string_types): return role in (role.name for role in self.roles) else: return role in self.roles class AnonymousUser(AnonymousUserMixin): """AnonymousUser definition""" def __init__(self): 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 = {} 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 def unauthorized_handler(self, fn): self._unauthorized_callback = 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, anonymous_user=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, anonymous_user=anonymous_user) if register_blueprint: app.register_blueprint(create_blueprint(state, __name__)) app.context_processor(_context_processor) state.render_template = self.render_template app.extensions['security'] = state return state def render_template(self, *args, **kwargs): return render_template(*args, **kwargs) def __getattr__(self, name): return getattr(self._state, name, None) Flask-Security-1.7.5/flask_security/datastore.py 0000644 0000765 0000024 00000024366 12627667222 022077 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_security.datastore ~~~~~~~~~~~~~~~~~~~~~~~~ This module contains an user datastore classes. :copyright: (c) 2012 by Matt Wright. :license: MIT, see LICENSE for more details. """ from .utils import get_identity_attributes, string_types 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, string_types): user = self.find_user(email=user) if isinstance(role, string_types): 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 get_user(self, id_or_email): """Returns a user matching the specified ID or email address.""" raise NotImplementedError 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 to 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 role not in user.roles: user.roles.append(role) self.put(user) return True return False 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) self.put(user) 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.""" kwargs = self._prepare_create_user_args(**kwargs) user = self.user_model(**kwargs) return self.put(user) def delete_user(self, user): """Deletes 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 get_user(self, identifier): if self._is_numeric(identifier): return self.user_model.query.get(identifier) for attr in get_identity_attributes(): query = getattr(self.user_model, attr).ilike(identifier) rv = self.user_model.query.filter(query).first() if rv is not None: return rv def _is_numeric(self, value): try: int(value) except (TypeError, ValueError): return False return True 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 get_user(self, identifier): from mongoengine import ValidationError try: return self.user_model.objects(id=identifier).first() except ValidationError: pass for attr in get_identity_attributes(): query_key = '%s__iexact' % attr query = {query_key: identifier} rv = self.user_model.objects(**query).first() if rv is not None: return rv def find_user(self, **kwargs): try: from mongoengine.queryset import Q, QCombination except ImportError: from mongoengine.queryset.visitor import Q, QCombination from mongoengine.errors import ValidationError queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items()) query = QCombination(QCombination.AND, queries) try: return self.user_model.objects(query).first() except ValidationError: # pragma: no cover return None def find_role(self, role): return self.role_model.objects(name=role).first() # TODO: Not sure why this was added but tests pass without it # def add_role_to_user(self, user, role): # rv = super(MongoEngineUserDatastore, self).add_role_to_user(user, role) # if rv: # self.put(user) # return rv 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 get_user(self, identifier): try: return self.user_model.get(self.user_model.id == identifier) except ValueError: pass for attr in get_identity_attributes(): column = getattr(self.user_model, attr) try: return self.user_model.get(column ** identifier) except self.user_model.DoesNotExist: pass 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) self.put(user) return user def add_role_to_user(self, user, role): """Adds a role to 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) result = self.UserRole.select() \ .where(self.UserRole.user == user.id, self.UserRole.role == role.id) if result.count(): return False else: self.put(self.UserRole.create(user=user.id, role=role.id)) 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) result = self.UserRole.select() \ .where(self.UserRole.user == user, self.UserRole.role == role) if result.count(): query = self.UserRole.delete().where( self.UserRole.user == user, self.UserRole.role == role) query.execute() return True else: return False Flask-Security-1.7.5/flask_security/decorators.py 0000644 0000765 0000024 00000016212 12627667222 022245 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_security.decorators ~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security decorators module :copyright: (c) 2012 by Matt Wright. :license: MIT, see LICENSE for more details. """ from collections import namedtuple from functools import wraps from flask import current_app, Response, request, redirect, _request_ctx_stack from flask_login import current_user, login_required # pragma: no flakes from flask_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.
""" BasicAuth = namedtuple('BasicAuth', 'username, password') 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.get_json(silent=True): if not isinstance(request.json, list): token = request.json.get(args_key, token) user = _security.login_manager.token_callback(token) if user and user.is_authenticated: 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 _check_http_auth(): auth = request.authorization or BasicAuth(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) if _security._unauthorized_callback: return _security._unauthorized_callback() else: 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) if _security._unauthorized_callback: return _security._unauthorized_callback() else: 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): h = {} mechanisms = [(method, login_mechanisms.get(method)) for method in auth_methods] for method, mechanism in mechanisms: if mechanism and mechanism(): return fn(*args, **kwargs) elif method == 'basic': r = _security.default_http_auth_realm h['WWW-Authenticate'] = 'Basic realm="%s"' % r if _security._unauthorized_callback: return _security._unauthorized_callback() else: return _get_unauthorized_response(headers=h) 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(): if _security._unauthorized_callback: return _security._unauthorized_callback() else: 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) if _security._unauthorized_callback: return _security._unauthorized_callback() else: 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 Flask-Security-1.7.5/flask_security/forms.py 0000644 0000765 0000024 00000022156 12627667222 021232 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_security.forms ~~~~~~~~~~~~~~~~~~~~ Flask-Security forms module :copyright: (c) 2012 by Matt Wright. :license: MIT, see LICENSE for more details. """ import inspect from flask import request, current_app, flash from flask_wtf import Form as BaseForm from wtforms import StringField, PasswordField, validators, \ 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, config_value, validate_redirect_url # 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, validators.EqualTo): pass class Required(ValidatorMixin, validators.Required): pass class Email(ValidatorMixin, validators.Email): pass class Length(ValidatorMixin, validators.Length): pass email_required = Required(message='EMAIL_NOT_PROVIDED') email_validator = Email(message='INVALID_EMAIL_ADDRESS') password_required = Required(message='PASSWORD_NOT_PROVIDED') password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH') def get_form_field_label(key): return _default_field_labels.get(key, '') def unique_user_email(form, field): if _datastore.get_user(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.get_user(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 = StringField( get_form_field_label('email'), validators=[email_required, email_validator]) class UserEmailFormMixin(): user = None email = StringField( get_form_field_label('email'), validators=[email_required, email_validator, valid_user_email]) class UniqueEmailFormMixin(): email = StringField( 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, password_length]) 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): if field.data and not validate_redirect_url(field.data): field.data = '' flash(*get_message('INVALID_REDIRECT')) 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')) def validate(self): if not super(ForgotPasswordForm, self).validate(): return False if requires_confirmation(self.user): self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) return False return True 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 = StringField(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) if not self.next.data: self.next.data = request.args.get('next', '') self.remember.default = config_value('DEFAULT_REMEMBER_ME') 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.get_user(self.email.data) if self.user is None: self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) return False if not self.user.password: self.password.errors.append(get_message('PASSWORD_NOT_SET')[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, NextFormMixin): def __init__(self, *args, **kwargs): super(RegisterForm, self).__init__(*args, **kwargs) if not self.next.data: self.next.data = request.args.get('next', '') 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, password_length]) 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 not verify_and_update_password(self.password.data, current_user): self.password.errors.append(get_message('INVALID_PASSWORD')[0]) return False if self.password.data.strip() == self.new_password.data.strip(): self.password.errors.append(get_message('PASSWORD_IS_THE_SAME')[0]) return False return True Flask-Security-1.7.5/flask_security/passwordless.py 0000644 0000765 0000024 00000003123 12627667222 022626 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_security.passwordless ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security passwordless 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 .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) login_link = url_for_security('token_login', token=token, _external=True) send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email, 'login_instructions', user=user, login_link=login_link) login_instructions_sent.send(app._get_current_object(), user=user, login_token=token) 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') Flask-Security-1.7.5/flask_security/recoverable.py 0000644 0000765 0000024 00000005515 12627667222 022375 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_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 from werkzeug.local import LocalProxy from werkzeug.security import safe_str_cmp 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) reset_link = url_for_security('reset_password', token=token, _external=True) send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email, 'reset_instructions', user=user, reset_link=reset_link) reset_password_instructions_sent.send(app._get_current_object(), user=user, token=token) 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 """ if config_value('SEND_PASSWORD_RESET_NOTICE_EMAIL'): 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 """ password_hash = md5(user.password) if user.password else None data = [str(user.id), password_hash] 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, data = reset_password_token_status('...') :param token: The password reset token """ expired, invalid, user, data = get_token_status(token, 'reset', 'RESET_PASSWORD', return_data=True) if not invalid: if user.password: password_hash = md5(user.password) if not safe_str_cmp(password_hash, data[1]): invalid = True return expired, invalid, user 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(app._get_current_object(), user=user) Flask-Security-1.7.5/flask_security/registerable.py 0000644 0000765 0000024 00000002443 12627667222 022551 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_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(app._get_current_object(), user=user, confirm_token=token) if config_value('SEND_REGISTER_EMAIL'): send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome', user=user, confirmation_link=confirmation_link) return user Flask-Security-1.7.5/flask_security/script.py 0000644 0000765 0000024 00000006634 12627667222 021413 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_security.script ~~~~~~~~~~~~~~~~~~~~~ Flask-Security script module :copyright: (c) 2012 by Matt Wright. :license: MIT, see LICENSE for more details. """ from __future__ import print_function try: import simplejson as json except ImportError: import json import re from flask import current_app from flask_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): """Remove a role from 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): """Deactivate a user""" @commit def run(self, user_identifier): _datastore.deactivate_user(user_identifier) print("User '%s' has been deactivated" % user_identifier) class ActivateUserCommand(_ToggleActiveCommand): """Activate a user""" @commit def run(self, user_identifier): _datastore.activate_user(user_identifier) print("User '%s' has been activated" % user_identifier) Flask-Security-1.7.5/flask_security/signals.py 0000644 0000765 0000024 00000001272 12627667222 021540 0 ustar matt staff 0000000 0000000 # -*- coding: utf-8 -*- """ flask_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") Flask-Security-1.7.5/flask_security/templates/ 0000755 0000765 0000024 00000000000 12627667302 021521 5 ustar matt staff 0000000 0000000 Flask-Security-1.7.5/flask_security/templates/.DS_Store 0000644 0000765 0000024 00000014004 12052511622 023164 0 ustar matt staff 0000000 0000000 Bud1 r i t ybwsp s e c u r i t ybwspblob bplist00 \WindowBounds[ShowSidebar]ShowStatusBar[ShowPathbar[ShowToolbar\SidebarWidth_{{316, 89}, {839, 1024}} ".