Added a bunch of code documentation

This commit is contained in:
Matt Wright
2012-03-12 17:07:21 -04:00
parent e8d41ad4b5
commit 65eac687b7
8 changed files with 244 additions and 41 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/mitsuhiko/flask-sphinx-themes.git
Submodule
+1
Submodule docs/_themes added at 0269f3d188
+98 -30
View File
@@ -1,15 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security flask.ext.security
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
Flask-Security is a Flask extension module that aims to add quick and Flask-Security is a Flask extension that aims to add quick and simple
simple security via Flask-Login and Flask-Principal. security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
:copyright: (c) 2012 by Matt Wright. :copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from __future__ import absolute_import
import sys import sys
@@ -34,8 +33,11 @@ from passlib.context import CryptContext
from werkzeug.utils import import_string from werkzeug.utils import import_string
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
#: User model
User = None
User, Role = None, None #: Role model
Role = None
URL_PREFIX_KEY = 'SECURITY_URL_PREFIX' URL_PREFIX_KEY = 'SECURITY_URL_PREFIX'
AUTH_PROVIDER_KEY = 'SECURITY_AUTH_PROVIDER' AUTH_PROVIDER_KEY = 'SECURITY_AUTH_PROVIDER'
@@ -54,6 +56,7 @@ DEBUG_LOGOUT = 'User logged out, redirecting to: %s'
FLASH_INACTIVE = 'Inactive user' FLASH_INACTIVE = 'Inactive user'
FLASH_PERMISSIONS = 'You do not have permission to view this resource.' FLASH_PERMISSIONS = 'You do not have permission to view this resource.'
#: Default Flask-Security configuration
default_config = { default_config = {
URL_PREFIX_KEY: None, URL_PREFIX_KEY: None,
PASSWORD_HASH_KEY: 'plaintext', PASSWORD_HASH_KEY: 'plaintext',
@@ -94,15 +97,15 @@ class UserIdNotFoundError(Exception):
""" """
class UserDatastoreError(Exception): class UserDatastoreError(Exception):
"""Raise when a user datastore experiences an unexpected error """Raised when a user datastore experiences an unexpected error
""" """
class UserCreationError(Exception): class UserCreationError(Exception):
"""Raise when an error occurs when creating a user """Raised when an error occurs when creating a user
""" """
class RoleCreationError(Exception): class RoleCreationError(Exception):
"""Raise when an error occurs when creating a role """Raised when an error occurs when creating a role
""" """
@@ -118,11 +121,24 @@ login_manager = LocalProxy(lambda: current_app.login_manager)
#: Password encyption context #: Password encyption context
pwd_context = LocalProxy(lambda: current_app.pwd_context) pwd_context = LocalProxy(lambda: current_app.pwd_context)
# User service #: User datastore
user_datastore = LocalProxy(lambda: getattr(current_app, user_datastore = LocalProxy(lambda: getattr(current_app,
current_app.config[USER_DATASTORE_KEY])) current_app.config[USER_DATASTORE_KEY]))
def roles_required(*args): 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.
"""
roles = args roles = args
perm = Permission(*[RoleNeed(role) for role in roles]) perm = Permission(*[RoleNeed(role) for role in roles])
def wrapper(fn): def wrapper(fn):
@@ -144,6 +160,19 @@ def roles_required(*args):
def roles_accepted(*args): def roles_accepted(*args):
"""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
order to view the page.
:param args: The possible roles.
"""
roles = args roles = args
perms = [Permission(RoleNeed(role)) for role in roles] perms = [Permission(RoleNeed(role)) for role in roles]
def wrapper(fn): def wrapper(fn):
@@ -166,6 +195,7 @@ def roles_accepted(*args):
class RoleMixin(object): class RoleMixin(object):
"""Mixin for `Role` model definitions"""
def __eq__(self, other): def __eq__(self, other):
return self.name == other.name return self.name == other.name
@@ -177,10 +207,16 @@ class RoleMixin(object):
class UserMixin(BaseUserMixin): class UserMixin(BaseUserMixin):
"""Mixin for `User` model definitions"""
def is_active(self): def is_active(self):
"""Returns `True` if the user is active."""
return self.active return self.active
def has_role(self, role): 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): if not isinstance(role, Role):
role = Role(name=role) role = Role(name=role)
return role in self.roles return role in self.roles
@@ -196,18 +232,25 @@ class AnonymousUser(AnonymousUserBase):
self.roles = [] # TODO: Make this immutable? self.roles = [] # TODO: Make this immutable?
def has_role(self, *args): def has_role(self, *args):
"""Returns `False`"""
return False return False
class Security(object): 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): def __init__(self, app=None, datastore=None):
self.init_app(app, datastore) self.init_app(app, datastore)
def init_app(self, app, datastore): def init_app(self, app, datastore):
"""Initialize the application """Initializes the Flask-Security extension for the specified
application and datastore implentation.
:param app: An instance of an application
:param datastore: An instance of a datastore for your users :param app: The application.
:param datastore: An instance of a user datastore.
""" """
if app is None or datastore is None: return if app is None or datastore is None: return
@@ -220,9 +263,6 @@ class Security(object):
app.config.update(configured) app.config.update(configured)
config = app.config config = app.config
#config = default_config.copy()
#config.update(app.config.get(AUTH_CONFIG_KEY, {}))
#app.config[AUTH_CONFIG_KEY] = config
# setup the login manager extension # setup the login manager extension
login_manager = LoginManager() login_manager = LoginManager()
@@ -301,7 +341,7 @@ class Security(object):
class LoginForm(Form): class LoginForm(Form):
"""Default login form""" """The default login form"""
username = TextField("Username or Email", username = TextField("Username or Email",
validators=[Required(message="Username not provided")]) validators=[Required(message="Username not provided")])
@@ -317,16 +357,27 @@ class LoginForm(Form):
class AuthenticationProvider(object): class AuthenticationProvider(object):
"""Default authentication provider""" """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): def __init__(self, login_form_class=None):
self.login_form_class = login_form_class or LoginForm self.login_form_class = login_form_class or LoginForm
def login_form(self, formdata=None): 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) return self.login_form_class(formdata)
def authenticate(self, form): def authenticate(self, form):
# first some basic validation """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(): if not form.validate():
if form.username.errors: if form.username.errors:
raise BadCredentialsError(form.username.errors[0]) raise BadCredentialsError(form.username.errors[0])
@@ -336,6 +387,13 @@ class AuthenticationProvider(object):
return self.do_authenticate(form.username.data, form.password.data) return self.do_authenticate(form.username.data, form.password.data)
def do_authenticate(self, user_identifier, password): 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: try:
user = user_datastore.find_user(user_identifier) user = user_datastore.find_user(user_identifier)
except AttributeError, e: except AttributeError, e:
@@ -355,11 +413,15 @@ class AuthenticationProvider(object):
raise BadCredentialsError("Password does not match") raise BadCredentialsError("Password does not match")
def auth_error(self, msg): def auth_error(self, msg):
"""Sends an error log message and raises an authentication error.
:param msg: An authentication error message"""
logger.error(msg) logger.error(msg)
raise AuthenticationError(msg) raise AuthenticationError(msg)
def get_class_by_name(clazz): def get_class_by_name(clazz):
"""Get a reference to a class by its string representation."""
parts = clazz.split('.') parts = clazz.split('.')
module = ".".join(parts[:-1]) module = ".".join(parts[:-1])
m = __import__( module ) m = __import__( module )
@@ -368,6 +430,7 @@ def get_class_by_name(clazz):
return m return m
def get_class_from_config(key, config): def get_class_from_config(key, config):
"""Get a reference to a class by its configuration key name."""
try: try:
return get_class_by_name(config[key]) return get_class_by_name(config[key])
except Exception, e: except Exception, e:
@@ -375,22 +438,27 @@ def get_class_from_config(key, config):
"Could not get class '%s' for Auth setting '%s' >> %s" % "Could not get class '%s' for Auth setting '%s' >> %s" %
(config[key], key, e)) (config[key], key, e))
def get_url(value): def get_url(endpoint_or_url):
# try building the url or assume its a url already """Returns a URL if a valid endpoint is found. Otherwise, returns the
try: return url_for(value) provided value."""
except: return value try:
return url_for(endpoint_or_url)
except:
return endpoint_or_url
def get_post_login_redirect(): 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 return (get_url(request.args.get('next')) or
get_url(request.form.get('next')) or get_url(request.form.get('next')) or
find_redirect(POST_LOGIN_KEY)) find_redirect(POST_LOGIN_KEY))
def find_redirect(key): def find_redirect(key):
# Look in the session first, and if not there go to the config, and """Returns the URL to redirect to after a user logs in successfully"""
# if its not there either just go to the root url result = (get_url(session.pop(key.lower(), None)) or
result = (get_url(session.get(key.lower(), None)) or get_url(current_app.config[key.upper()] or None) or '/')
get_url(current_app.config[key] or None) or '/')
# Try and delete the session value if it was used try:
try: del session[key.lower()] del session[key.lower()]
except: pass except:
pass
return result return result
+71 -4
View File
@@ -1,10 +1,34 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.datastore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains an abstracted user datastore.
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
from datetime import datetime from datetime import datetime
from flask.ext import security from flask.ext import security
from flask.ext.security import UserCreationError, RoleCreationError, pwd_context from flask.ext.security import UserCreationError, RoleCreationError, pwd_context
class UserDatastore(object): class UserDatastore(object):
"""Abstracted user datastore. Always extend this and implement """Abstracted user datastore. Always extend this class and implement the
missing methods""" :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
extension such as Flask-SQLAlchemy or Flask-MongoEngine"""
def __init__(self, db):
self.db = db
def get_models(self):
"""Returns configured `User` and `Role` models for the datastore
implementation"""
raise NotImplementedError(
"User datastore does not implement get_models method")
def _save_model(self, model, **kwargs): def _save_model(self, model, **kwargs):
raise NotImplementedError( raise NotImplementedError(
@@ -96,36 +120,79 @@ class UserDatastore(object):
return kwargs return kwargs
def with_id(self, id): def with_id(self, id):
"""Returns a user with the specified ID.
:param id: User ID"""
user = self._do_with_id(id) user = self._do_with_id(id)
if user: return user if user: return user
raise security.UserIdNotFoundError() raise security.UserIdNotFoundError()
def find_user(self, user): def find_user(self, user):
"""Returns a user based on the specified identifier.
:param user: User identifier, usually a username or email address
"""
user = self._do_find_user(user) user = self._do_find_user(user)
if user: return user if user: return user
raise security.UserNotFoundError() raise security.UserNotFoundError()
def find_role(self, role): def find_role(self, role):
"""Returns a role based on its name.
:param role: Role name
"""
role = self._do_find_role(role) role = self._do_find_role(role)
if role: return role if role: return role
raise security.RoleNotFoundError() raise security.RoleNotFoundError()
def create_role(self, commit=True, **kwargs): 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 = security.Role(**self._prepare_create_role_args(kwargs))
return self._save_model(role) return self._save_model(role)
def create_user(self, commit=True, **kwargs): 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 = security.User(**self._prepare_create_user_args(kwargs))
return self._save_model(user) return self._save_model(user)
def add_role_to_user(self, user, role): def add_role_to_user(self, user, role):
"""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)) return self._save_model(self._do_add_role(user, role))
def remove_role_from_user(self, user, role, commit=True): def remove_role_from_user(self, user, role, commit=True):
"""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)) return self._save_model(self._do_remove_role(user, role))
def deactivate_user(self, user): 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)) return self._save_model(self._do_deactive_user(user))
def activate_user(self, user, commit=True): 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)) return self._save_model(self._do_active_user(user))
+30 -3
View File
@@ -1,21 +1,48 @@
# -*- 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 import security
from flask.ext.security import UserMixin, RoleMixin from flask.ext.security import UserMixin, RoleMixin
from flask.ext.security.datastore import UserDatastore from flask.ext.security.datastore import UserDatastore
class MongoEngineUserDatastore(UserDatastore): class MongoEngineUserDatastore(UserDatastore):
"""MongoEngine datastore""" """A MongoEngine datastore implementation for Flask-Security. Example:
def __init__(self, db): from flask import Flask
self.db = db 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): def get_models(self):
db = self.db db = self.db
class Role(db.Document, RoleMixin): class Role(db.Document, RoleMixin):
"""MongoEngine Role model"""
name = db.StringField(required=True, unique=True, max_length=80) name = db.StringField(required=True, unique=True, max_length=80)
description = db.StringField(max_length=255) description = db.StringField(max_length=255)
class User(db.Document, UserMixin): class User(db.Document, UserMixin):
"""MongoEngine User model"""
username = db.StringField(unique=True, max_length=255) username = db.StringField(unique=True, max_length=255)
email = db.StringField(unique=True, max_length=255) email = db.StringField(unique=True, max_length=255)
password = db.StringField(required=True, max_length=120) password = db.StringField(required=True, max_length=120)
+28 -3
View File
@@ -1,13 +1,34 @@
# -*- 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 import security
from flask.ext.security import UserMixin, RoleMixin from flask.ext.security import UserMixin, RoleMixin
from flask.ext.security.datastore import UserDatastore from flask.ext.security.datastore import UserDatastore
class SQLAlchemyUserDatastore(UserDatastore): class SQLAlchemyUserDatastore(UserDatastore):
"""SQLAlchemy datastore""" """A SQLAlchemy datastore implementation for Flask-Security. Example:
def __init__(self, db): from flask import Flask
self.db = db 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): def get_models(self):
db = self.db db = self.db
@@ -16,6 +37,8 @@ class SQLAlchemyUserDatastore(UserDatastore):
db.Column('role_id', db.Integer(), db.ForeignKey('user.id'))) db.Column('role_id', db.Integer(), db.ForeignKey('user.id')))
class Role(db.Model, RoleMixin): class Role(db.Model, RoleMixin):
"""SQLAlchemy Role model"""
id = db.Column(db.Integer(), primary_key=True) id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True) name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255)) description = db.Column(db.String(255))
@@ -25,6 +48,8 @@ class SQLAlchemyUserDatastore(UserDatastore):
self.description = description self.description = description
class User(db.Model, UserMixin): class User(db.Model, UserMixin):
"""SQLAlchemy User model"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), unique=True) username = db.Column(db.String(255), unique=True)
email = db.Column(db.String(255), unique=True) email = db.Column(db.String(255), unique=True)
+11
View File
@@ -1,3 +1,14 @@
# -*- coding: utf-8 -*-
"""
flask.ext.security.script
~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains commands for use with the Flask-Script extension
:copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details.
"""
import json import json
import re import re
from flask.ext.script import Command, Option from flask.ext.script import Command, Option
+2 -1
View File
@@ -2,7 +2,8 @@
Flask-Security Flask-Security
-------------- --------------
Simple security for Flask apps Flask-Security is a Flask extension that aims to add quick and simple security
via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
Links Links
````` `````