Add a bunch of doc strings and add some more configuration values

This commit is contained in:
Matt Wright
2012-06-29 12:37:22 -04:00
parent 53dd4f0b1b
commit 2ea835ec9f
9 changed files with 148 additions and 76 deletions
+27 -1
View File
@@ -26,12 +26,20 @@ _datastore = LocalProxy(lambda: app.security.datastore)
def find_user_by_confirmation_token(token):
"""Returns a user with a matching confirmation token.
:param token: The reset password token
"""
if not token:
raise ConfirmationError('Confirmation token required')
return _datastore.find_user(confirmation_token=token)
def send_confirmation_instructions(user):
"""Sends the confirmation instructions email for the specified user.
:param user: The user to send the instructions to
"""
url = url_for('flask_security.confirm',
confirmation_token=user.confirmation_token)
@@ -47,6 +55,10 @@ def send_confirmation_instructions(user):
def generate_confirmation_token(user):
"""Generates a unique confirmation token for the specified user.
:param user: The user to work with
"""
while True:
token = generate_token()
try:
@@ -67,8 +79,9 @@ def generate_confirmation_token(user):
def should_confirm_email(fn):
"""Handy decorator that returns early if confirmation should not occur."""
def wrapped(*args, **kwargs):
if _security.confirm_email:
if _security.confirmable:
return fn(*args, **kwargs)
return False
return wrapped
@@ -76,16 +89,24 @@ def should_confirm_email(fn):
@should_confirm_email
def requires_confirmation(user):
"""Returns `True` if the user requires confirmation."""
return user.confirmed_at == None
@should_confirm_email
def confirmation_token_is_expired(user):
"""Returns `True` if the user's confirmation token is expired."""
token_expires = datetime.utcnow() - _security.confirm_email_within
return user.confirmation_sent_at < token_expires
def confirm_by_token(token):
"""Confirm the user given the specified token. If the token is invalid or
the user is already confirmed a `ConfirmationError` error will be raised.
If the token is expired a `TokenExpiredError` error will be raised.
:param token: The user's confirmation token
"""
try:
user = find_user_by_confirmation_token(token)
except UserNotFoundError:
@@ -110,5 +131,10 @@ def confirm_by_token(token):
def reset_confirmation_token(user):
"""Resets the specified user's confirmation token and sends the user
an email with instructions explaining next steps.
:param user: The user to work with
"""
_datastore._save_model(generate_confirmation_token(user))
send_confirmation_instructions(user)
+42 -33
View File
@@ -49,7 +49,9 @@ _default_config = {
'POST_REGISTER_VIEW': None,
'POST_CONFIRM_VIEW': None,
'DEFAULT_ROLES': [],
'CONFIRM_EMAIL': False,
'CONFIRMABLE': False,
'REGISTERABLE': True,
'RECOVERABLE': True,
'CONFIRM_EMAIL_WITHIN': '5 days',
'RESET_PASSWORD_WITHIN': '2 days',
'LOGIN_WITHOUT_CONFIRMATION': False,
@@ -90,6 +92,8 @@ class UserMixin(BaseUserMixin):
class AnonymousUser(AnonymousUserBase):
"""AnonymousUser definition"""
def __init__(self):
super(AnonymousUser, self).__init__()
self.roles = ImmutableList()
@@ -99,7 +103,7 @@ class AnonymousUser(AnonymousUserBase):
return False
def load_user(user_id):
def _load_user(user_id):
try:
return current_app.security.datastore.with_id(user_id)
except Exception, e:
@@ -107,7 +111,7 @@ def load_user(user_id):
return None
def on_identity_loaded(sender, identity):
def _on_identity_loaded(sender, identity):
if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))
@@ -126,12 +130,16 @@ class Security(object):
def __init__(self, app=None, datastore=None, **kwargs):
self.init_app(app, datastore, **kwargs)
def init_app(self, app, datastore, registerable=True, recoverable=True):
def init_app(self, app, datastore):
"""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 confirmable: Set to `True` to enable email confirmation
:param registerable: Set to `False` to disable registration endpoints
:param recoverable: Set to `False` to disable password recovery
endpoints
"""
if app is None or datastore is None:
return
@@ -142,7 +150,7 @@ class Security(object):
login_manager = LoginManager()
login_manager.anonymous_user = AnonymousUser
login_manager.login_view = utils.config_value(app, 'LOGIN_VIEW')
login_manager.user_loader(load_user)
login_manager.user_loader(_load_user)
login_manager.init_app(app)
Provider = utils.get_class_from_string(app, 'AUTH_PROVIDER')
@@ -150,7 +158,7 @@ class Security(object):
self.login_manager = login_manager
self.pwd_context = CryptContext(schemes=[pw_hash], default=pw_hash)
self.auth_provider = Provider(Form)
self.auth_provider = Provider()
self.principal = Principal(app)
self.datastore = datastore
self.LoginForm = utils.get_class_from_string(app, 'LOGIN_FORM')
@@ -171,7 +179,9 @@ class Security(object):
self.reset_password_error_view = utils.config_value(app, 'RESET_PASSWORD_ERROR_VIEW')
self.default_roles = utils.config_value(app, "DEFAULT_ROLES")
self.login_without_confirmation = utils.config_value(app, 'LOGIN_WITHOUT_CONFIRMATION')
self.confirm_email = utils.config_value(app, 'CONFIRM_EMAIL')
self.confirmable = utils.config_value(app, 'CONFIRMABLE')
self.registerable = utils.config_value(app, 'REGISTERABLE')
self.recoverable = utils.config_value(app, 'RECOVERABLE')
self.email_sender = utils.config_value(app, 'EMAIL_SENDER')
self.token_authentication_key = utils.config_value(app, 'TOKEN_AUTHENTICATION_KEY')
self.token_authentication_header = utils.config_value(app, 'TOKEN_AUTHENTICATION_HEADER')
@@ -184,7 +194,7 @@ class Security(object):
values = self.reset_password_within_text.split()
self.reset_password_within = timedelta(**{values[1]: int(values[0])})
identity_loaded.connect_via(app)(on_identity_loaded)
identity_loaded.connect_via(app)(_on_identity_loaded)
bp = Blueprint('flask_security', __name__, template_folder='templates')
@@ -195,21 +205,21 @@ class Security(object):
bp.route(self.logout_url,
endpoint='logout')(login_required(views.logout))
self.setup_registerable(bp) if registerable else None
self.setup_recoverable(bp) if recoverable else None
self.setup_confirmable(bp) if self.confirm_email else None
self._setup_registerable(bp) if self.registerable else None
self._setup_recoverable(bp) if self.recoverable else None
self._setup_confirmable(bp) if self.confirmable else None
app.register_blueprint(bp,
url_prefix=utils.config_value(app, 'URL_PREFIX'))
app.security = self
def setup_registerable(self, bp):
def _setup_registerable(self, bp):
bp.route(self.register_url,
methods=['POST'],
endpoint='register')(views.register)
def setup_recoverable(self, bp):
def _setup_recoverable(self, bp):
bp.route(self.forgot_url,
methods=['POST'],
endpoint='forgot')(views.forgot)
@@ -217,32 +227,30 @@ class Security(object):
methods=['POST'],
endpoint='reset')(views.reset)
def setup_confirmable(self, bp):
def _setup_confirmable(self, bp):
bp.route(self.confirm_url,
endpoint='confirm')(views.confirm)
class AuthenticationProvider(object):
"""The default authentication provider implementation.
"""The default authentication provider implementation."""
def _get_user(self, username_or_email):
datastore = current_app.security.datastore
: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)
try:
return datastore.find_user(email=username_or_email)
except exceptions.UserNotFoundError:
try:
return datastore.find_user(username=username_or_email)
except:
raise exceptions.UserNotFoundError()
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
:param form: A populated WTForm instance that contains `email` and
`password` form fields
"""
if not form.validate():
if form.email.errors:
@@ -252,15 +260,16 @@ class AuthenticationProvider(object):
return self.do_authenticate(form.email.data, form.password.data)
def do_authenticate(self, email, password):
def do_authenticate(self, username_or_email, password):
"""Returns the authenticated user if authentication is successfull. If
authentication fails an appropriate error is raised
authentication fails an appropriate `AuthenticationError` is raised
:param user_identifier: The user's identifier, usuall an email address
:param password: The user's unencrypted password
:param username_or_email: The username or email address of the user
:param password: The password supplied by the authentication request
"""
try:
user = current_app.security.datastore.find_user(email=email)
user = self._get_user(username_or_email)
except AttributeError, e:
self.auth_error("Could not find user datastore: %s" % e)
except exceptions.UserNotFoundError, e:
+7 -3
View File
@@ -90,7 +90,7 @@ class UserDatastore(object):
kwargs.setdefault('active', True)
kwargs.setdefault('roles', current_app.security.default_roles)
if current_app.security.confirm_email:
if current_app.security.confirmable:
confirmable.generate_confirmation_token(kwargs)
if email is None:
@@ -196,7 +196,9 @@ class UserDatastore(object):
class SQLAlchemyUserDatastore(UserDatastore):
"""A SQLAlchemy datastore implementation for Flask-Security.
"""A SQLAlchemy datastore implementation for Flask-Security that assumes the
use of the Flask-SQLAlchemy extension.
Example usage::
from flask import Flask
@@ -249,7 +251,9 @@ class SQLAlchemyUserDatastore(UserDatastore):
class MongoEngineUserDatastore(UserDatastore):
"""A MongoEngine datastore implementation for Flask-Security.
"""A MongoEngine datastore implementation for Flask-Security that assumes
the use of the Flask-MongoEngine extension.
Example usage::
from flask import Flask
+5 -4
View File
@@ -55,6 +55,7 @@ def _check_http_auth():
def http_auth_required(fn):
"""Decorator that protects endpoints using Basic HTTP authentication."""
headers = {'WWW-Authenticate': 'Basic realm="Login Required"'}
@wraps(fn)
@@ -68,7 +69,7 @@ def http_auth_required(fn):
def auth_token_required(fn):
"""Decorator that protects endpoints using token authentication."""
@wraps(fn)
def decorated(*args, **kwargs):
if _check_token():
@@ -80,8 +81,8 @@ def auth_token_required(fn):
def roles_required(*roles):
"""View decorator which specifies that a user must have all the specified
roles. Example::
"""Decorator which specifies that a user must have all the specified roles.
Example::
@app.route('/dashboard')
@roles_required('admin', 'editor')
@@ -113,7 +114,7 @@ def roles_required(*roles):
def roles_accepted(*roles):
"""View decorator which specifies that a user must have at least one of the
"""Decorator which specifies that a user must have at least one of the
specified roles. Example::
@app.route('/create_post')
+29
View File
@@ -28,12 +28,20 @@ _datastore = LocalProxy(lambda: app.security.datastore)
def find_user_by_reset_token(token):
"""Returns a user with a matching reset password token.
:param token: The reset password token
"""
if not token:
raise ResetPasswordError('Reset password token required')
return _datastore.find_user(reset_password_token=token)
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
"""
url = url_for('flask_security.reset',
email=user.email,
reset_token=user.reset_password_token)
@@ -51,6 +59,10 @@ def send_reset_password_instructions(user):
def generate_reset_password_token(user):
"""Generates a unique reset password token for the specified user.
:param user: The user to work with
"""
while True:
token = generate_token()
try:
@@ -71,11 +83,23 @@ def generate_reset_password_token(user):
def password_reset_token_is_expired(user):
"""Returns `True` if the specified user's reset password token is expired.
:param user: The user to examine
"""
token_expires = datetime.utcnow() - _security.reset_password_within
return user.reset_password_sent_at < token_expires
def reset_by_token(token, email, password):
"""Resets the password of the user given the specified token, email and
password. If the token is invalid a `ResetPasswordError` error will be
raised. If the token is expired a `TokenExpiredError` error will be raised.
:param token: The user's reset password token
:param email: The user's email address
:param password: The user's new password
"""
try:
user = find_user_by_reset_token(token)
except UserNotFoundError:
@@ -98,6 +122,11 @@ def reset_by_token(token, email, password):
def reset_password_reset_token(user):
"""Resets the specified user's reset password token and sends the user
an email with instructions explaining next steps.
:param user: The user to work with
"""
_datastore._save_model(generate_reset_password_token(user))
send_reset_password_instructions(user)
password_reset_requested.send(user, app=app._get_current_object())
+17
View File
@@ -23,12 +23,20 @@ _datastore = LocalProxy(lambda: app.security.datastore)
def find_user_by_authentication_token(token):
"""Returns a user with a matching authentication token.
:param token: The authentication token
"""
if not token:
raise BadCredentialsError('Authentication token required')
return _datastore.find_user(authentication_token=token)
def generate_authentication_token(user):
"""Generates a unique authentication token for the specified user.
:param user: The user to work with
"""
while True:
token = generate_token()
try:
@@ -49,12 +57,21 @@ def generate_authentication_token(user):
def reset_authentication_token(user):
"""Resets a user's authentication token and returns the new token value.
:param user: The user to work with
"""
user = generate_authentication_token(user)
_datastore._save_model(user)
return user.authentication_token
def ensure_authentication_token(user):
"""Ensures that a user has an authentication token. If the user has an
authentication token already, nothing is performed.
:param user: The user to work with
"""
if not user.authentication_token:
reset_authentication_token(user)
return user.authentication_token
+7 -7
View File
@@ -20,7 +20,7 @@ from .signals import user_registered, password_reset_requested
def generate_token():
"""Generate an arbitrary URL safe token"""
"""Generate an arbitrary URL safe token."""
return base64.urlsafe_b64encode(os.urandom(30))
@@ -59,14 +59,14 @@ def get_url(endpoint_or_url):
def get_post_login_redirect():
"""Returns the URL to redirect to after a user logs in successfully"""
"""Returns the URL to redirect to after a user logs in successfully."""
return (get_url(request.args.get('next')) or
get_url(request.form.get('next')) or
find_redirect('SECURITY_POST_LOGIN_VIEW'))
def find_redirect(key):
"""Returns the URL to redirect to after a user logs in successfully
"""Returns the URL to redirect to after a user logs in successfully.
:param key: The session or application configuration key to search for
"""
@@ -79,7 +79,7 @@ def find_redirect(key):
def config_value(app, key, default=None):
"""Get a Flask-Security configuration value
"""Get a Flask-Security configuration value.
:param app: The application to retrieve the configuration from
:param key: The configuration key without the prefix `SECURITY_`
@@ -89,7 +89,7 @@ def config_value(app, key, default=None):
def send_mail(subject, recipient, template, context=None):
"""Send an email via the Flask-Mail extension
"""Send an email via the Flask-Mail extension.
:param subject: Email subject
:param recipient: Email recipient
@@ -113,7 +113,7 @@ def send_mail(subject, recipient, template, context=None):
@contextmanager
def capture_registrations(confirmation_sent_at=None):
"""Testing utility for capturing registrations
"""Testing utility for capturing registrations.
:param confirmation_sent_at: An optional datetime object to set the
user's `confirmation_sent_at` to
@@ -137,7 +137,7 @@ def capture_registrations(confirmation_sent_at=None):
@contextmanager
def capture_reset_password_requests(reset_password_sent_at=None):
"""Testing utility for capturing password reset requests
"""Testing utility for capturing password reset requests.
:param reset_password_sent_at: An optional datetime object to set the
user's `reset_password_sent_at` to
+13 -27
View File
@@ -34,6 +34,8 @@ _logger = LocalProxy(lambda: app.logger)
def _do_login(user, remember=True):
"""Performs the login and sends the appropriate signal."""
if login_user(user, remember):
identity_changed.send(app._get_current_object(),
identity=Identity(user.id))
@@ -44,13 +46,8 @@ def _do_login(user, remember=True):
def authenticate():
"""View function which handles an authentication attempt. If authentication
is successful the user is redirected to, if set, the value of the `next`
form parameter. If that value is not set the user is redirected to the
value of the `SECURITY_POST_LOGIN_VIEW` configuration value. If
authenticate fails the user an appropriate error message is flashed and
the user is redirected to the referring page or the login view.
"""
"""View function which handles an authentication request."""
form = _security.LoginForm()
try:
@@ -74,10 +71,8 @@ def authenticate():
def logout():
"""View function which logs out the current user. When completed the user
is redirected to the value of the `next` query string parameter or the
`SECURITY_POST_LOGIN_VIEW` configuration value.
"""
"""View function which handles a logout request."""
for key in ('identity.name', 'identity.auth_type'):
session.pop(key, None)
@@ -92,13 +87,8 @@ def logout():
def register():
"""View function which registers a new user and, if configured so, the user
isautomatically logged in. If required confirmation instructions are sent
via email. After registration is completed the user is redirected to, if
set, the value of the `SECURITY_POST_REGISTER_VIEW` configuration value.
Otherwise the user is redirected to the `SECURITY_POST_LOGIN_VIEW`
configuration value.
"""
"""View function which handles a registration request."""
form = _security.RegisterForm(csrf_enabled=not app.testing)
# Exit early if the form doesn't validate
@@ -109,13 +99,13 @@ def register():
user_registered.send(user, app=app._get_current_object())
# Send confirmation instructions if necessary
if _security.confirm_email:
if _security.confirmable:
send_confirmation_instructions(user)
_logger.debug('User %s registered' % user)
# Login the user if allowed
if not _security.confirm_email or _security.login_without_confirmation:
if not _security.confirmable or _security.login_without_confirmation:
_do_login(user)
return redirect(_security.post_register_view or
@@ -126,9 +116,7 @@ def register():
def confirm():
"""View function which confirms a user's email address using a token taken
from the value of the `confirmation_token` query string argument.
"""
"""View function which handles a account confirmation request."""
try:
token = request.args.get('confirmation_token', None)
@@ -156,8 +144,7 @@ def confirm():
def forgot():
"""View function that handles the generation of a password reset token.
"""
"""View function that handles a forgotten password request."""
form = _security.ForgotPasswordForm(csrf_enabled=not app.testing)
@@ -180,8 +167,7 @@ def forgot():
def reset():
"""View function that handles the reset of a user's password.
"""
"""View function that handles a reset password request."""
form = _security.ResetPasswordForm(csrf_enabled=not app.testing)
+1 -1
View File
@@ -142,7 +142,7 @@ class RegisterableTests(SecurityTest):
class ConfirmableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRM_EMAIL': True
'SECURITY_CONFIRMABLE': True
}
def test_register_sends_confirmation_email(self):