diff --git a/example/app.py b/example/app.py index 3f86072..c269b72 100644 --- a/example/app.py +++ b/example/app.py @@ -1,128 +1,156 @@ # a little trick so you can run: -# $ python example/app.py +# $ python example/app.py # from the root of the security project -import sys, os +import os +import sys sys.path.pop(0) sys.path.insert(0, os.getcwd()) -from flask import Flask, render_template - +from flask import Flask, render_template, current_app from flask.ext.mongoengine import MongoEngine from flask.ext.sqlalchemy import SQLAlchemy +from flask.ext.security import Security, LoginForm, login_required, \ + roles_required, roles_accepted, UserMixin, RoleMixin +from flask.ext.security.datastore import SQLAlchemyUserDatastore, \ + MongoEngineUserDatastore -from flask.ext.security import (Security, LoginForm, user_datastore, - login_required, roles_required, roles_accepted) - -from flask.ext.security.datastore.sqlalchemy import SQLAlchemyUserDatastore -from flask.ext.security.datastore.mongoengine import MongoEngineUserDatastore def create_roles(): for role in ('admin', 'editor', 'author'): - user_datastore.create_role(name=role) - + current_app.security.datastore.create_role(name=role) + + def create_users(): - for u in (('matt','matt@lp.com','password',['admin'],True), - ('joe','joe@lp.com','password',['editor'],True), - ('jill','jill@lp.com','password',['author'],True), - ('tiya','tiya@lp.com','password',[],False)): - user_datastore.create_user(username=u[0], email=u[1], password=u[2], - roles=u[3], active=u[4]) + for u in (('matt', 'matt@lp.com', 'password', ['admin'], True), + ('joe', 'joe@lp.com', 'password', ['editor'], True), + ('jill', 'jill@lp.com', 'password', ['author'], True), + ('tiya', 'tiya@lp.com', 'password', [], False)): + current_app.security.datastore.create_user(username=u[0], email=u[1], + password=u[2], roles=u[3], active=u[4]) + def populate_data(): create_roles() create_users() - + + def create_app(auth_config): app = Flask(__name__) app.debug = True app.config['SECRET_KEY'] = 'secret' - + if auth_config: for key, value in auth_config.items(): app.config[key] = value - + @app.route('/') def index(): return render_template('index.html', content='Home Page') - + @app.route('/login') def login(): return render_template('login.html', content='Login Page', form=LoginForm()) - + @app.route('/custom_login') def custom_login(): return render_template('login.html', content='Custom Login Page', form=LoginForm()) - + @app.route('/profile') @login_required def profile(): return render_template('index.html', content='Profile Page') - + @app.route('/post_login') @login_required def post_login(): return render_template('index.html', content='Post Login') - + @app.route('/post_logout') def post_logout(): return render_template('index.html', content='Post Logout') - + @app.route('/admin') @roles_required('admin') def admin(): return render_template('index.html', content='Admin Page') - + @app.route('/admin_or_editor') @roles_accepted('admin', 'editor') def admin_or_editor(): return render_template('index.html', content='Admin or Editor Page') - + return app + def create_sqlalchemy_app(auth_config=None): app = create_app(auth_config) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_security_example.sqlite' - + db = SQLAlchemy(app) - class UserAccountMixin(): + roles_users = db.Table('roles_users', + db.Column('user_id', db.Integer(), db.ForeignKey('role.id')), + db.Column('role_id', db.Integer(), db.ForeignKey('user.id'))) + + class Role(db.Model, RoleMixin): + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(80), unique=True) + description = db.Column(db.String(255)) + + class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(255), unique=True) + email = db.Column(db.String(255), unique=True) + password = db.Column(db.String(120)) first_name = db.Column(db.String(120)) last_name = db.Column(db.String(120)) + active = db.Column(db.Boolean()) + created_at = db.Column(db.DateTime()) + modified_at = db.Column(db.DateTime()) + roles = db.relationship('Role', secondary=roles_users, + backref=db.backref('users', lazy='dynamic')) + + Security(app, SQLAlchemyUserDatastore(db, User, Role)) - Security(app, SQLAlchemyUserDatastore(db, UserAccountMixin)) - @app.before_first_request def before_first_request(): db.drop_all() db.create_all() populate_data() - + return app + def create_mongoengine_app(auth_config=None): app = create_app(auth_config) app.config['MONGODB_DB'] = 'flask_security_example' app.config['MONGODB_HOST'] = 'localhost' app.config['MONGODB_PORT'] = 27017 - + db = MongoEngine(app) - class UserAccountMixin(): - first_name = db.StringField(max_length=120) - last_name = db.StringField(max_length=120) + class Role(db.Document, RoleMixin): + name = db.StringField(required=True, unique=True, max_length=80) + description = db.StringField(max_length=255) + + class User(db.Document, UserMixin): + username = db.StringField(unique=True, max_length=255) + email = db.StringField(unique=True, max_length=255) + password = db.StringField(required=True, max_length=120) + active = db.BooleanField(default=True) + roles = db.ListField(db.ReferenceField(Role), default=[]) + + Security(app, MongoEngineUserDatastore(db, User, Role)) - Security(app, MongoEngineUserDatastore(db, UserAccountMixin)) - @app.before_first_request def before_first_request(): - from flask.ext.security import User, Role User.drop_collection() Role.drop_collection() populate_data() - + return app if __name__ == '__main__': app = create_sqlalchemy_app() #app = create_mongoengine_app() - app.run() \ No newline at end of file + app.run() diff --git a/flask_security/__init__.py b/flask_security/__init__.py index 4cb1a62..a28d4ef 100644 --- a/flask_security/__init__.py +++ b/flask_security/__init__.py @@ -3,74 +3,42 @@ flask.ext.security ~~~~~~~~~~~~~~~~~~ - Flask-Security is a Flask extension that aims to add quick and simple + 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. """ -import sys - -from datetime import datetime -from types import StringType - -from flask import (current_app, Blueprint, flash, redirect, request, - session, _request_ctx_stack, url_for, abort, g) - -from flask.ext.login import (AnonymousUser as AnonymousUserBase, - UserMixin as BaseUserMixin, LoginManager, login_required, login_user, - logout_user, current_user, user_logged_in, user_logged_out, - login_url) - -from flask.ext.principal import (Identity, Principal, RoleNeed, UserNeed, - Permission, AnonymousIdentity, identity_changed, identity_loaded) - -from flask.ext.wtf import (Form, TextField, PasswordField, SubmitField, - HiddenField, Required, ValidationError, BooleanField, Email) - from functools import wraps +from importlib import import_module + +from flask import current_app, Blueprint, flash, redirect, request, \ + session, url_for +from flask.ext.login import AnonymousUser as AnonymousUserBase, \ + UserMixin as BaseUserMixin, LoginManager, login_required, login_user, \ + logout_user, current_user, login_url +from flask.ext.principal import Identity, Principal, RoleNeed, UserNeed, \ + Permission, AnonymousIdentity, identity_changed, identity_loaded +from flask.ext.wtf import Form, TextField, PasswordField, SubmitField, \ + HiddenField, Required, BooleanField from passlib.context import CryptContext -from werkzeug.utils import import_string -from werkzeug.local import LocalProxy +from werkzeug.datastructures import ImmutableList -class User(object): - """User model""" - -class Role(object): - """Role model""" - -URL_PREFIX_KEY = 'SECURITY_URL_PREFIX' -AUTH_PROVIDER_KEY = 'SECURITY_AUTH_PROVIDER' -PASSWORD_HASH_KEY = 'SECURITY_PASSWORD_HASH' -USER_DATASTORE_KEY = 'SECURITY_USER_DATASTORE' -LOGIN_FORM_KEY = 'SECURITY_LOGIN_FORM' -AUTH_URL_KEY = 'SECURITY_AUTH_URL' -LOGOUT_URL_KEY = 'SECURITY_LOGOUT_URL' -LOGIN_VIEW_KEY = 'SECURITY_LOGIN_VIEW' -POST_LOGIN_KEY = 'SECURITY_POST_LOGIN' -POST_LOGOUT_KEY = 'SECURITY_POST_LOGOUT' -FLASH_MESSAGES_KEY = 'SECURITY_FLASH_MESSAGES' - -DEBUG_LOGIN = 'User %s logged in. Redirecting to: %s' -ERROR_LOGIN = 'Unsuccessful authentication attempt: %s. Redirecting to: %s' -DEBUG_LOGOUT = 'User logged out, redirecting to: %s' -FLASH_INACTIVE = 'Inactive user' -FLASH_PERMISSIONS = 'You do not have permission to view this resource.' #: Default Flask-Security configuration -default_config = { - URL_PREFIX_KEY: None, - FLASH_MESSAGES_KEY: True, - PASSWORD_HASH_KEY: 'plaintext', - USER_DATASTORE_KEY: 'user_datastore', - AUTH_PROVIDER_KEY: 'flask.ext.security.AuthenticationProvider', - LOGIN_FORM_KEY: 'flask.ext.security.LoginForm', - AUTH_URL_KEY: '/auth', - LOGOUT_URL_KEY: '/logout', - LOGIN_VIEW_KEY: '/login', - POST_LOGIN_KEY: '/', - POST_LOGOUT_KEY: '/', +_default_config = { + 'SECURITY_URL_PREFIX': None, + 'SECURITY_FLASH_MESSAGES': True, + 'SECURITY_PASSWORD_HASH': 'plaintext', + 'SECURITY_USER_DATASTORE': 'user_datastore', + 'SECURITY_AUTH_PROVIDER': 'flask.ext.security::AuthenticationProvider', + 'SECURITY_LOGIN_FORM': 'flask.ext.security::LoginForm', + 'SECURITY_AUTH_URL': '/auth', + 'SECURITY_LOGOUT_URL': '/logout', + 'SECURITY_LOGIN_VIEW': '/login', + 'SECURITY_POST_LOGIN_VIEW': '/', + 'SECURITY_POST_LOGOUT_VIEW': '/', } @@ -78,122 +46,114 @@ class BadCredentialsError(Exception): """Raised when an authentication attempt fails due to an error with the provided credentials. """ - + + class AuthenticationError(Exception): """Raised when an authentication attempt fails due to invalid configuration or an unknown reason. - """ - + """ + + class UserNotFoundError(Exception): - """Raised by a user datastore when there is an attempt to find a user by + """Raised by a user datastore when there is an attempt to find a user by their identifier, often username or email, and the user is not found. """ - + + class RoleNotFoundError(Exception): """Raised by a user datastore when there is an attempt to find a role and the role cannot be found. """ - + + class UserIdNotFoundError(Exception): - """Raised by a user datastore when there is an attempt to find a user by + """Raised by a user datastore when there is an attempt to find a user by ID and the user is not found. """ - + + class UserDatastoreError(Exception): """Raised when a user datastore experiences an unexpected error """ - + + class UserCreationError(Exception): """Raised when an error occurs when creating a user """ - + + class RoleCreationError(Exception): """Raised when an error occurs when creating a role """ - - -#: App logger for convenience -logger = LocalProxy(lambda: current_app.logger) -#: Authentication provider -auth_provider = LocalProxy(lambda: current_app.auth_provider) - -#: Login manager -login_manager = LocalProxy(lambda: current_app.login_manager) - -#: Password encyption context -pwd_context = LocalProxy(lambda: current_app.pwd_context) - -#: User datastore -user_datastore = LocalProxy(lambda: getattr(current_app, - current_app.config[USER_DATASTORE_KEY])) def roles_required(*args): """View 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. + + :param args: The required roles. """ roles = args perm = Permission(*[RoleNeed(role) for role in roles]) + def wrapper(fn): @wraps(fn) def decorated_view(*args, **kwargs): if not current_user.is_authenticated(): - return redirect( - login_url(current_app.config[LOGIN_VIEW_KEY], request.url)) - + login_view = current_app.security.login_manager.login_view + return redirect(login_url(login_view, request.url)) + if perm.can(): return fn(*args, **kwargs) - - logger.debug('Identity does not provide all of the ' - 'following roles: %s' % [r for r in roles]) - - do_flash(FLASH_PERMISSIONS, 'error') + + current_app.logger.debug('Identity does not provide the ' + 'roles: %s' % [r for r in roles]) return redirect(request.referrer or '/') return decorated_view return wrapper def roles_accepted(*args): - """View decorator which specifies that a user must have at least one of the + """View 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 + + The current user must have either the `editor` role or `author` role in order to view the page. - - :param args: The possible roles. + + :param args: The possible roles. """ roles = args perms = [Permission(RoleNeed(role)) for role in roles] + def wrapper(fn): @wraps(fn) def decorated_view(*args, **kwargs): if not current_user.is_authenticated(): - return redirect( - login_url(current_app.config[LOGIN_VIEW_KEY], request.url)) - + login_view = current_app.security.login_manager.login_view + return redirect(login_url(login_view, request.url)) + for perm in perms: if perm.can(): return fn(*args, **kwargs) - - logger.debug('Identity does not provide at least one of ' - 'the following roles: %s' % [r for r in roles]) - - do_flash(FLASH_PERMISSIONS, 'error') + + current_app.logger.debug('Identity does not provide at least one ' + 'role: %s' % [r for r in roles]) + + _do_flash('You do not have permission to view this resource', + 'error') return redirect(request.referrer or '/') return decorated_view return wrapper @@ -202,30 +162,32 @@ def roles_accepted(*args): class RoleMixin(object): """Mixin for `Role` model definitions""" def __eq__(self, other): + if isinstance(other, basestring): + return self.name == other return self.name == other.name - + def __ne__(self, other): + if isinstance(other, basestring): + return self.name != other return self.name != other.name - + def __str__(self): return '' % (self.name, self.description) class UserMixin(BaseUserMixin): """Mixin for `User` model definitions""" - + def is_active(self): - """Returns `True` if the user is active.""" + """Returns `True` if the user is active.""" return self.active - + def has_role(self, role): """Returns `True` if the user identifies with the specified role. - + :param role: A role name or `Role` instance""" - if not isinstance(role, Role): - role = Role(name=role) return role in self.roles - + def __str__(self): ctx = (str(self.id), self.username, self.email) return '' % ctx @@ -234,8 +196,8 @@ class UserMixin(BaseUserMixin): class AnonymousUser(AnonymousUserBase): def __init__(self): super(AnonymousUser, self).__init__() - self.roles = [] # TODO: Make this immutable? - + self.roles = ImmutableList() + def has_role(self, *args): """Returns `False`""" return False @@ -243,145 +205,141 @@ class AnonymousUser(AnonymousUserBase): 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): self.init_app(app, datastore) - + def init_app(self, app, datastore): - """Initializes the Flask-Security extension for the specified + """Initializes the Flask-Security extension for the specified application and datastore implentation. - + :param app: The application. :param datastore: An instance of a user datastore. """ - if app is None or datastore is None: return - - # TODO: change blueprint name - blueprint = Blueprint('auth', __name__) - - configured = {} - - for key, value in default_config.items(): - configured[key] = app.config.get(key, value) - - app.config.update(configured) - config = app.config - - # setup the login manager extension + if app is None or datastore is None: + return + + for key, value in _default_config.items(): + app.config.setdefault(key, value) + login_manager = LoginManager() login_manager.anonymous_user = AnonymousUser - login_manager.login_view = config[LOGIN_VIEW_KEY] + login_manager.login_view = _config_value(app, 'LOGIN_VIEW') login_manager.setup_app(app) - app.login_manager = login_manager - - Provider = get_class_from_config(AUTH_PROVIDER_KEY, config) - Form = get_class_from_config(LOGIN_FORM_KEY, config) - pw_hash = config[PASSWORD_HASH_KEY] - - app.pwd_context = CryptContext(schemes=[pw_hash], default=pw_hash) - app.auth_provider = Provider(Form) - app.principal = Principal(app) - - from flask.ext import security as s - s.User, s.Role = datastore.get_models() - - setattr(app, config[USER_DATASTORE_KEY], datastore) - + + Provider = _get_class_from_string(app, 'AUTH_PROVIDER') + Form = _get_class_from_string(app, 'LOGIN_FORM') + pw_hash = _config_value(app, 'PASSWORD_HASH') + + self.login_manager = login_manager + self.pwd_context = CryptContext(schemes=[pw_hash], default=pw_hash) + self.auth_provider = Provider(Form) + self.principal = Principal(app) + self.datastore = datastore + self.auth_url = _config_value(app, 'AUTH_URL') + self.logout_url = _config_value(app, 'LOGOUT_URL') + self.post_login_view = _config_value(app, 'POST_LOGIN_VIEW') + self.post_logout_view = _config_value(app, 'POST_LOGOUT_VIEW') + @identity_loaded.connect_via(app) 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 - + @login_manager.user_loader def load_user(user_id): - try: - return datastore.with_id(user_id) + try: + return app.security.datastore.with_id(user_id) except Exception, e: - logger.error('Error getting user: %s' % e) + app.logger.error('Error getting user: %s' % e) return None - - auth_url = config[AUTH_URL_KEY] - @blueprint.route(auth_url, methods=['POST'], endpoint='authenticate') + + bp = Blueprint('auth', __name__) + + @bp.route(self.auth_url, methods=['POST'], endpoint='authenticate') def authenticate(): try: form = Form() - user = auth_provider.authenticate(form) - + user = current_app.security.auth_provider.authenticate(form) + if login_user(user, remember=form.remember.data): - redirect_url = get_post_login_redirect() + redirect_url = _get_post_login_redirect() identity_changed.send(app, identity=Identity(user.id)) - logger.debug(DEBUG_LOGIN % (user, redirect_url)) + app.logger.debug('User %s logged in. Redirecting to: ' + '%s' % (user, redirect_url)) return redirect(redirect_url) - raise BadCredentialsError(FLASH_INACTIVE) - + raise BadCredentialsError('Inactive user') + except BadCredentialsError, e: message = '%s' % e - do_flash(message, 'error') + _do_flash(message, 'error') redirect_url = request.referrer or login_manager.login_view - logger.error(ERROR_LOGIN % (message, redirect_url)) + app.logger.error('Unsuccessful authentication attempt: %s. ' + 'Redirect to: %s' % (message, redirect_url)) return redirect(redirect_url) - - @blueprint.route(config[LOGOUT_URL_KEY], endpoint='logout') + + @bp.route(self.logout_url, endpoint='logout') @login_required def logout(): for value in ('identity.name', 'identity.auth_type'): session.pop(value, None) - + identity_changed.send(app, identity=AnonymousIdentity()) logout_user() - - redirect_url = find_redirect(POST_LOGOUT_KEY) - logger.debug(DEBUG_LOGOUT % redirect_url) + + redirect_url = _find_redirect('SECURITY_POST_LOGOUT_VIEW') + app.logger.debug('User logged out. Redirect to: %s' % redirect_url) return redirect(redirect_url) - - app.register_blueprint(blueprint, url_prefix=config[URL_PREFIX_KEY]) - - + + app.register_blueprint(bp, url_prefix=_config_value(app, 'URL_PREFIX')) + app.security = self + + class LoginForm(Form): """The default login form""" - - username = TextField("Username or Email", + + username = TextField("Username or Email", validators=[Required(message="Username not provided")]) - password = PasswordField("Password", + password = PasswordField("Password", validators=[Required(message="Password not provided")]) remember = BooleanField("Remember Me") next = HiddenField() submit = SubmitField("Login") - + def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) self.next.data = request.args.get('next', None) - + class AuthenticationProvider(object): """The default authentication provider implementation. - + :param login_form_class: The login form class to use when authenticating a user """ - + def __init__(self, login_form_class=None): self.login_form_class = login_form_class or LoginForm - + def login_form(self, formdata=None): """Returns an instance of the login form with the provided form. - + :param formdata: The incoming form data""" return self.login_form_class(formdata) - + def authenticate(self, form): """Processes an authentication request and returns a user instance if authentication is successful. - + :param form: An instance of a populated login form """ if not form.validate(): @@ -389,19 +347,19 @@ class AuthenticationProvider(object): raise BadCredentialsError(form.username.errors[0]) if form.password.errors: raise BadCredentialsError(form.password.errors[0]) - + return self.do_authenticate(form.username.data, form.password.data) - + def do_authenticate(self, user_identifier, password): """Returns the authenticated user if authentication is successfull. If authentication fails an appropriate error is raised - + :param user_identifier: The user's identifier, either an email address or username :param password: The user's unencrypted password """ try: - user = user_datastore.find_user(user_identifier) + user = current_app.security.datastore.find_user(user_identifier) except AttributeError, e: self.auth_error("Could not find user service: %s" % e) except UserNotFoundError, e: @@ -410,65 +368,61 @@ class AuthenticationProvider(object): self.auth_error('Invalid user service: %s' % e) except Exception, e: self.auth_error('Unexpected authentication error: %s' % e) - + # compare passwords - if pwd_context.verify(password, user.password): + if current_app.security.pwd_context.verify(password, user.password): return user # bad match raise BadCredentialsError("Password does not match") - + def auth_error(self, msg): """Sends an error log message and raises an authentication error. - + :param msg: An authentication error message""" - logger.error(msg) + current_app.logger.error(msg) raise AuthenticationError(msg) -def do_flash(message, category): - if current_app.config[FLASH_MESSAGES_KEY]: + +def _do_flash(message, category): + if _config_value(current_app, 'FLASH_MESSAGES'): flash(message, category) -def get_class_by_name(clazz): - """Get a reference to a class by its string representation.""" - parts = clazz.split('.') - module = ".".join(parts[:-1]) - m = __import__( module ) - for comp in parts[1:]: - m = getattr(m, comp) - return m - -def get_class_from_config(key, config): +def _get_class_from_string(app, key): """Get a reference to a class by its configuration key name.""" - try: - return get_class_by_name(config[key]) - except Exception, e: - raise AttributeError( - "Could not get class '%s' for Auth setting '%s' >> %s" % - (config[key], key, e)) + cv = _config_value(app, key).split('::') + cm = import_module(cv[0]) + return getattr(cm, cv[1]) + def get_url(endpoint_or_url): - """Returns a URL if a valid endpoint is found. Otherwise, returns the + """Returns a URL if a valid endpoint is found. Otherwise, returns the provided value.""" - try: + try: return url_for(endpoint_or_url) - except: + except: return endpoint_or_url -def get_post_login_redirect(): - """Returns the URL to redirect to after a user logs in successfully""" - return (get_url(request.args.get('next')) or - get_url(request.form.get('next')) or - find_redirect(POST_LOGIN_KEY)) -def find_redirect(key): +def _get_post_login_redirect(): """Returns the URL to redirect to after a user logs in successfully""" - result = (get_url(session.pop(key.lower(), None)) or + return (get_url(request.args.get('next')) or + get_url(request.form.get('next')) or + _find_redirect('SECURITY_POST_LOGIN_VIEW')) + + +def _find_redirect(key): + """Returns the URL to redirect to after a user logs in successfully""" + result = (get_url(session.pop(key.lower(), None)) or get_url(current_app.config[key.upper()] or None) or '/') - - try: + + try: del session[key.lower()] - except: + except: pass return result + + +def _config_value(app, key): + return app.config['SECURITY_' + key.upper()] diff --git a/flask_security/datastore/__init__.py b/flask_security/datastore.py similarity index 50% rename from flask_security/datastore/__init__.py rename to flask_security/datastore.py index 9210448..4b1a17b 100644 --- a/flask_security/datastore/__init__.py +++ b/flask_security/datastore.py @@ -3,64 +3,59 @@ flask.ext.security.datastore ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - This module contains an abstracted user datastore. + This module contains an user datastore classes. :copyright: (c) 2012 by Matt Wright. :license: MIT, see LICENSE for more details. """ -from datetime import datetime +from flask import current_app from flask.ext import security -from flask.ext.security import UserCreationError, RoleCreationError, pwd_context + class UserDatastore(object): - """Abstracted user datastore. Always extend this class and implement the - :attr:`get_models`, :attr:`_save_model`, :attr:`_do_with_id`, + """Abstracted user datastore. Always extend this class and implement the + :attr:`get_models`, :attr:`_save_model`, :attr:`_do_with_id`, :attr:`_do_find_user`, and :attr:`_do_find_role` methods. - - :param db: An instance of a configured databse manager from a Flask + + :param db: An instance of a configured databse manager from a Flask extension such as Flask-SQLAlchemy or Flask-MongoEngine - :param user_account_mixin: An optional mixin class that specifies additional - fields to be added to the user model + :param user_model: A user model class + :param role_model: A role model class """ - def __init__(self, db, user_account_mixin=None): + def __init__(self, db, user_model, role_model): self.db = db - self.user_account_mixin = user_account_mixin or object - - def get_models(self): - """Returns configured `User` and `Role` models for the datastore - implementation""" - raise NotImplementedError( - "User datastore does not implement get_models method") - + self.user_model = user_model + self.role_model = role_model + def _save_model(self, model, **kwargs): raise NotImplementedError( "User datastore does not implement _save_model method") - + def _do_with_id(self, id): raise NotImplementedError( "User datastore does not implement _do_with_id method") - + def _do_find_user(self): raise NotImplementedError( "User datastore does not implement _do_find_user method") - + def _do_find_role(self): raise NotImplementedError( "User datastore does not implement _do_find_role method") - + def _do_add_role(self, user, role): user, role = self._prepare_role_modify_args(user, role) if role not in user.roles: user.roles.append(role) return user - + def _do_remove_role(self, user, role): user, role = self._prepare_role_modify_args(user, role) if role in user.roles: user.roles.remove(role) return user - + def _do_toggle_active(self, user, active=None): user = self.find_user(user) if active is None: @@ -68,134 +63,239 @@ class UserDatastore(object): elif active != user.active: user.active = active return user - + def _do_deactive_user(self, user): return self._do_toggle_active(user, False) - + def _do_active_user(self, user): return self._do_toggle_active(user, True) - + def _prepare_role_modify_args(self, user, role): - if isinstance(user, security.User): + if isinstance(user, self.user_model): user = user.username or user.email - - if isinstance(role, security.Role): + + if isinstance(role, self.role_model): role = role.name - + return self.find_user(user), self.find_role(role) - + def _prepare_create_role_args(self, kwargs): for key in ('name', 'description'): kwargs[key] = kwargs.get(key, None) - + if kwargs['name'] is None: - raise RoleCreationError("Missing name argument") - + raise security.RoleCreationError("Missing name argument") + return kwargs - + def _prepare_create_user_args(self, kwargs): username = kwargs.get('username', None) email = kwargs.get('email', None) password = kwargs.get('password', None) - + kwargs.setdefault('active', True) + if username is None and email is None: - raise UserCreationError('Missing username and/or email arguments') - + raise security.UserCreationError( + 'Missing username and/or email arguments') + if password is None: - raise UserCreationError('Missing password argument') - + raise security.UserCreationError('Missing password argument') + roles = kwargs.get('roles', []) - + for i, role in enumerate(roles): - rn = role.name if isinstance(role, security.Role) else role + 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 - - now = datetime.utcnow() - kwargs['created_at'], kwargs['modified_at'] = now, now - + + pwd_context = current_app.security.pwd_context pw = kwargs['password'] if not pwd_context.identify(pw): kwargs['password'] = pwd_context.encrypt(pw) - + return kwargs - + def with_id(self, id): """Returns a user with the specified ID. - + :param id: User ID""" user = self._do_with_id(id) - if user: return user + if user: + return user raise security.UserIdNotFoundError() - + def find_user(self, user): - """Returns a user based on the specified identifier. - + """Returns a user based on the specified identifier. + :param user: User identifier, usually a username or email address """ user = self._do_find_user(user) - if user: return user + if user: + return user raise security.UserNotFoundError() - + def find_role(self, role): """Returns a role based on its name. - + :param role: Role name """ role = self._do_find_role(role) - if role: return role + if role: + return role raise security.RoleNotFoundError() - + def create_role(self, **kwargs): """Creates and returns a new role. - + :param name: Role name :param description: Role description """ - role = security.Role(**self._prepare_create_role_args(kwargs)) + role = self.role_model(**self._prepare_create_role_args(kwargs)) return self._save_model(role) - + def create_user(self, **kwargs): """Creates and returns a new user. - + :param username: Username :param email: Email address :param password: Unencrypted password :param active: The optional active state """ - user = security.User(**self._prepare_create_user_args(kwargs)) + user = self.user_model(**self._prepare_create_user_args(kwargs)) return self._save_model(user) - + def add_role_to_user(self, user, role): - """Adds a role to a user if the user does not have it already. Returns + """Adds a role to a user if the user does not have it already. Returns the modified user. - + :param user: A User instance or a user identifier :param role: A Role instance or a role name """ return self._save_model(self._do_add_role(user, role)) - + def remove_role_from_user(self, user, role, commit=True): - """Removes a role from a user if the user has the role. Returns the + """Removes a role from a user if the user has the role. Returns the modified user. - + :param user: A User instance or a user identifier :param role: A Role instance or a role name """ return self._save_model(self._do_remove_role(user, role)) - + def deactivate_user(self, user): """Deactivates a user and returns the modified user. - + :param user: A User instance or a user identifier """ return self._save_model(self._do_deactive_user(user)) - + def activate_user(self, user, commit=True): """Activates a user and returns the modified user. - + :param user: A User instance or a user identifier """ - return self._save_model(self._do_active_user(user)) \ No newline at end of file + return self._save_model(self._do_active_user(user)) + + +class SQLAlchemyUserDatastore(UserDatastore): + """A SQLAlchemy datastore implementation for Flask-Security. + Example usage:: + + from flask import Flask + from flask.ext.security import Security, SQLAlchemyUserDatastore + from flask.ext.sqlalchemy import SQLAlchemy + + app = Flask(__name__) + app.config['SECRET_KEY'] = 'secret' + app.config['SQLALCHEMY_DATABASE_URI'] = \ + 'sqlite:////tmp/flask_security_example.sqlite' + + db = SQLAlchemy(app) + + roles_users = db.Table('roles_users', + db.Column('user_id', db.Integer(), db.ForeignKey('role.id')), + db.Column('role_id', db.Integer(), db.ForeignKey('user.id'))) + + class Role(db.Model, RoleMixin): + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(80), unique=True) + + class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(255), unique=True) + email = db.Column(db.String(255), unique=True) + password = db.Column(db.String(120)) + first_name = db.Column(db.String(120)) + last_name = db.Column(db.String(120)) + active = db.Column(db.Boolean()) + created_at = db.Column(db.DateTime()) + modified_at = db.Column(db.DateTime()) + roles = db.relationship('Role', secondary=roles_users, + backref=db.backref('users', lazy='dynamic')) + + Security(app, SQLAlchemyUserDatastore(db, User, Role)) + """ + + def _save_model(self, model): + self.db.session.add(model) + self.db.session.commit() + return model + + def _do_with_id(self, id): + return self.user_model.query.get(id) + + def _do_find_user(self, user): + return self.user_model.query.filter_by(username=user).first() or \ + self.user_model.query.filter_by(email=user).first() + + def _do_find_role(self, role): + return self.role_model.query.filter_by(name=role).first() + + +class MongoEngineUserDatastore(UserDatastore): + """A MongoEngine datastore implementation for Flask-Security. + Example usage:: + + from flask import Flask + from flask.ext.mongoengine import MongoEngine + from flask.ext.security import Security, MongoEngineUserDatastore + + app = Flask(__name__) + app.config['SECRET_KEY'] = 'secret' + app.config['MONGODB_DB'] = 'flask_security_example' + app.config['MONGODB_HOST'] = 'localhost' + app.config['MONGODB_PORT'] = 27017 + + db = MongoEngine(app) + + class Role(db.Document, RoleMixin): + name = db.StringField(required=True, unique=True, max_length=80) + + class User(db.Document, UserMixin): + username = db.StringField(unique=True, max_length=255) + email = db.StringField(unique=True, max_length=255) + password = db.StringField(required=True, max_length=120) + active = db.BooleanField(default=True) + roles = db.ListField(db.ReferenceField(Role), default=[]) + + Security(app, MongoEngineUserDatastore(db, User, Role)) + """ + + def _save_model(self, model): + model.save() + return model + + def _do_with_id(self, id): + try: + return self.user_model.objects.get(id=id) + except: + return None + + def _do_find_user(self, user): + return self.user_model.objects(username=user).first() or \ + self.user_model.objects(email=user).first() + + def _do_find_role(self, role): + return self.role_model.objects(name=role).first() diff --git a/flask_security/datastore/mongoengine.py b/flask_security/datastore/mongoengine.py deleted file mode 100644 index 7e28c39..0000000 --- a/flask_security/datastore/mongoengine.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.datastore.mongoengine - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - This module contains a Flask-Security MongoEngine datastore implementation - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from flask.ext import security -from flask.ext.security import UserMixin, RoleMixin -from flask.ext.security.datastore import UserDatastore - -class MongoEngineUserDatastore(UserDatastore): - """A MongoEngine datastore implementation for Flask-Security. - Example usage:: - - from flask import Flask - from flask.ext.mongoengine import MongoEngine - from flask.ext.security import Security - from flask.ext.security.datastore.mongoengine import MongoEngineUserDatastore - - app = Flask(__name__) - app.config['SECRET_KEY'] = 'secret' - app.config['MONGODB_DB'] = 'flask_security_example' - app.config['MONGODB_HOST'] = 'localhost' - app.config['MONGODB_PORT'] = 27017 - - db = MongoEngine(app) - Security(app, MongoEngineUserDatastore(db)) - """ - - def get_models(self): - db = self.db - - class Role(db.Document, RoleMixin): - """MongoEngine Role model""" - - name = db.StringField(required=True, unique=True, max_length=80) - description = db.StringField(max_length=255) - - class User(db.Document, UserMixin, self.user_account_mixin): - """MongoEngine User model""" - - username = db.StringField(unique=True, max_length=255) - email = db.StringField(unique=True, max_length=255) - password = db.StringField(required=True, max_length=120) - active = db.BooleanField(default=True) - roles= db.ListField(db.ReferenceField(Role), default=[]) - created_at = db.DateTimeField() - modified_at = db.DateTimeField() - - return User, Role - - def _save_model(self, model): - model.save() - return model - - def _do_with_id(self, id): - try: return security.User.objects.get(id=id) - except: return None - - def _do_find_user(self, user): - return security.User.objects(username=user).first() or \ - security.User.objects(email=user).first() - - def _do_find_role(self, role): - return security.Role.objects(name=role).first() - \ No newline at end of file diff --git a/flask_security/datastore/sqlalchemy.py b/flask_security/datastore/sqlalchemy.py deleted file mode 100644 index e239b51..0000000 --- a/flask_security/datastore/sqlalchemy.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.datastore.sqlalchemy - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - This module contains a Flask-Security SQLAlchemy datastore implementation - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from flask.ext import security -from flask.ext.security import UserMixin, RoleMixin -from flask.ext.security.datastore import UserDatastore - -class SQLAlchemyUserDatastore(UserDatastore): - """A SQLAlchemy datastore implementation for Flask-Security. - Example usage:: - - from flask import Flask - from flask.ext.security import Security - from flask.ext.security.datastore.sqlalchemy import SQLAlchemyUserDatastore - from flask.ext.sqlalchemy import SQLAlchemy - - app = Flask(__name__) - app.config['SECRET_KEY'] = 'secret' - app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/flask_security_example.sqlite' - - db = SQLAlchemy(app) - Security(app, SQLAlchemyUserDatastore(db)) - """ - - def get_models(self): - db = self.db - - roles_users = db.Table('roles_users', - db.Column('user_id', db.Integer(), db.ForeignKey('role.id')), - db.Column('role_id', db.Integer(), db.ForeignKey('user.id'))) - - class Role(db.Model, RoleMixin): - """SQLAlchemy Role model""" - - id = db.Column(db.Integer(), primary_key=True) - name = db.Column(db.String(80), unique=True) - description = db.Column(db.String(255)) - - def __init__(self, name=None, description=None): - self.name = name - self.description = description - - class User(db.Model, UserMixin, self.user_account_mixin): - """SQLAlchemy User model""" - - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(255), unique=True) - email = db.Column(db.String(255), unique=True) - password = db.Column(db.String(120)) - active = db.Column(db.Boolean()) - created_at = db.Column(db.DateTime()) - modified_at = db.Column(db.DateTime()) - - roles= db.relationship('Role', secondary=roles_users, - backref=db.backref('users', lazy='dynamic')) - - def __init__(self, username=None, email=None, password=None, - active=True, roles=None, - created_at=None, modified_at=None): - self.username = username - self.email = email - self.password = password - self.active = active - self.roles = roles or [] - self.created_at = created_at - self.modified_at = modified_at - - return User, Role - - def _save_model(self, model): - self.db.session.add(model) - self.db.session.commit() - return model - - def _do_with_id(self, id): - return security.User.query.get(id) - - def _do_find_user(self, user): - return security.User.query.filter_by(username=user).first() or \ - security.User.query.filter_by(email=user).first() - - def _do_find_role(self, role): - return security.Role.query.filter_by(name=role).first() - \ No newline at end of file diff --git a/setup.py b/setup.py index 3738b25..93952f8 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ from setuptools import setup setup( name='Flask-Security', - version='1.2.3-dev', + version='1.3.0-dev', url='https://github.com/mattupstate/flask-security', license='MIT', author='Matthew Wright', @@ -25,8 +25,7 @@ setup( description='Simple security for Flask apps', long_description=__doc__, packages=[ - 'flask_security', - 'flask_security.datastore' + 'flask_security' ], zip_safe=False, include_package_data=True, diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 0451e7d..4ba161d 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -114,8 +114,8 @@ class ConfiguredSecurityTests(SecurityTest): 'SECURITY_AUTH_URL': '/custom_auth', 'SECURITY_LOGOUT_URL': '/custom_logout', 'SECURITY_LOGIN_VIEW': '/custom_login', - 'SECURITY_POST_LOGIN': '/post_login', - 'SECURITY_POST_LOGOUT': '/post_logout' + 'SECURITY_POST_LOGIN_VIEW': '/post_login', + 'SECURITY_POST_LOGOUT_VIEW': '/post_logout' } def test_login_view(self):