mirror of
https://github.com/wassname/flask-security.git
synced 2026-07-23 12:50:33 +08:00
+32
-1
@@ -59,6 +59,37 @@ The following is a list of all the available context processor decorators:
|
||||
* ``send_login_context_processor``: Send login view
|
||||
|
||||
|
||||
Forms
|
||||
-----
|
||||
|
||||
All forms can be overridden in much the same way as context
|
||||
processors. For each form used, you can specify a function that
|
||||
returns the form class. This allows you to override/extend forms. For
|
||||
example, to add extra fields to the register form::
|
||||
|
||||
security = Security(app, user_datastore)
|
||||
|
||||
from flask_security.forms import RegisterForm
|
||||
|
||||
class ExtendedRegisterForm(RegisterForm):
|
||||
first_name = TextField('First Name', [Required()])
|
||||
last_name = TextField('Last Name', [Required()])
|
||||
|
||||
@security.register_form
|
||||
def security_register_form():
|
||||
return ExtendedRegisterForm
|
||||
|
||||
The following is a list of all the available form override decorators:
|
||||
|
||||
* ``login_form``: Login form
|
||||
* ``confirm_register_form``: Confirmable register form
|
||||
* ``register_form``: Register form
|
||||
* ``forgot_password_form``: Forgot password form
|
||||
* ``reset_password_form``: Reset password form
|
||||
* ``send_confirmation_form``: Send confirmation form
|
||||
* ``passwordless_login_form``: Passwordless login form
|
||||
|
||||
|
||||
Emails
|
||||
------
|
||||
|
||||
@@ -93,4 +124,4 @@ templates you can specify an email context processor with the
|
||||
# This processor is added to all emails
|
||||
@security.email_context_processor
|
||||
def security_mail_processor():
|
||||
return dict(hello="world")
|
||||
return dict(hello="world")
|
||||
|
||||
+32
-2
@@ -21,6 +21,9 @@ from werkzeug.local import LocalProxy
|
||||
|
||||
from .utils import config_value as cv, get_config, md5, url_for_security
|
||||
from .views import create_blueprint
|
||||
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
|
||||
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
|
||||
PasswordlessLoginForm
|
||||
|
||||
# Convenient references
|
||||
_security = LocalProxy(lambda: current_app.extensions['security'])
|
||||
@@ -105,6 +108,17 @@ _allowed_password_hash_schemes = [
|
||||
'plaintext'
|
||||
]
|
||||
|
||||
_default_forms = {
|
||||
'login_form': LoginForm,
|
||||
'confirm_register_form': ConfirmRegisterForm,
|
||||
'register_form': RegisterForm,
|
||||
'forgot_password_form': ForgotPasswordForm,
|
||||
'reset_password_form': ResetPasswordForm,
|
||||
'send_confirmation_form': SendConfirmationForm,
|
||||
'passwordless_login_form': PasswordlessLoginForm,
|
||||
}
|
||||
|
||||
|
||||
def _user_loader(user_id):
|
||||
return _security.datastore.find_user(id=user_id)
|
||||
|
||||
@@ -189,6 +203,10 @@ def _get_state(app, datastore, **kwargs):
|
||||
_send_mail_task=None
|
||||
))
|
||||
|
||||
for key, value in _default_forms.items():
|
||||
if key not in kwargs or not kwargs[key]:
|
||||
kwargs[key] = value
|
||||
|
||||
return _SecurityState(**kwargs)
|
||||
|
||||
|
||||
@@ -296,12 +314,17 @@ class Security(object):
|
||||
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):
|
||||
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, send_confirmation_form=None,
|
||||
passwordless_login_form=None):
|
||||
"""Initializes the Flask-Security extension for the specified
|
||||
application and datastore implentation.
|
||||
|
||||
:param app: The application.
|
||||
:param datastore: An instance of a user datastore.
|
||||
:param register_blueprint: to register the Security blueprint or not.
|
||||
"""
|
||||
datastore = datastore or self.datastore
|
||||
|
||||
@@ -313,7 +336,14 @@ class Security(object):
|
||||
|
||||
identity_loaded.connect_via(app)(_on_identity_loaded)
|
||||
|
||||
state = _get_state(app, datastore)
|
||||
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,
|
||||
send_confirmation_form=send_confirmation_form,
|
||||
passwordless_login_form=passwordless_login_form)
|
||||
|
||||
if register_blueprint:
|
||||
app.register_blueprint(create_blueprint(state, __name__))
|
||||
|
||||
+11
-5
@@ -9,10 +9,12 @@
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
|
||||
from flask import request, current_app
|
||||
from flask.ext.wtf import Form as BaseForm, TextField, PasswordField, \
|
||||
SubmitField, HiddenField, Required, BooleanField, EqualTo, Email, \
|
||||
ValidationError, Length
|
||||
ValidationError, Length, Field
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .confirmable import requires_confirmation
|
||||
@@ -93,6 +95,13 @@ class NextFormMixin():
|
||||
class RegisterFormMixin():
|
||||
submit = SubmitField("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"""
|
||||
@@ -176,10 +185,7 @@ class LoginForm(Form, NextFormMixin):
|
||||
|
||||
class ConfirmRegisterForm(Form, RegisterFormMixin,
|
||||
UniqueEmailFormMixin, NewPasswordFormMixin):
|
||||
def to_dict(self):
|
||||
return dict(email=self.email.data,
|
||||
password=self.password.data)
|
||||
|
||||
pass
|
||||
|
||||
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin):
|
||||
pass
|
||||
|
||||
+19
-14
@@ -17,9 +17,6 @@ from werkzeug.local import LocalProxy
|
||||
from .confirmable import send_confirmation_instructions, \
|
||||
confirm_user, confirm_email_token_status
|
||||
from .decorators import login_required, anonymous_user_required
|
||||
from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
|
||||
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
|
||||
PasswordlessLoginForm
|
||||
from .passwordless import send_login_instructions, \
|
||||
login_token_status
|
||||
from .recoverable import reset_password_token_status, \
|
||||
@@ -64,10 +61,12 @@ def _ctx(endpoint):
|
||||
def login():
|
||||
"""View function for login view"""
|
||||
|
||||
form_class = _security.login_form
|
||||
|
||||
if request.json:
|
||||
form = LoginForm(MultiDict(request.json))
|
||||
form = form_class(MultiDict(request.json))
|
||||
else:
|
||||
form = LoginForm()
|
||||
form = form_class()
|
||||
|
||||
if form.validate_on_submit():
|
||||
login_user(form.user, remember=form.remember.data)
|
||||
@@ -101,9 +100,9 @@ def register():
|
||||
"""View function which handles a registration request."""
|
||||
|
||||
if _security.confirmable or request.json:
|
||||
form_class = ConfirmRegisterForm
|
||||
form_class = _security.confirm_register_form
|
||||
else:
|
||||
form_class = RegisterForm
|
||||
form_class = _security.register_form
|
||||
|
||||
if request.json:
|
||||
form_data = MultiDict(request.json)
|
||||
@@ -136,10 +135,12 @@ def register():
|
||||
def send_login():
|
||||
"""View function that sends login instructions for passwordless login"""
|
||||
|
||||
form_class = _security.passwordless_login_form
|
||||
|
||||
if request.json:
|
||||
form = PasswordlessLoginForm(MultiDict(request.json))
|
||||
form = form_class(MultiDict(request.json))
|
||||
else:
|
||||
form = PasswordlessLoginForm()
|
||||
form = form_class()
|
||||
|
||||
if form.validate_on_submit():
|
||||
send_login_instructions(form.user)
|
||||
@@ -179,10 +180,12 @@ def token_login(token):
|
||||
def send_confirmation():
|
||||
"""View function which sends confirmation instructions."""
|
||||
|
||||
form_class = _security.send_confirmation_form
|
||||
|
||||
if request.json:
|
||||
form = SendConfirmationForm(MultiDict(request.json))
|
||||
form = form_class(MultiDict(request.json))
|
||||
else:
|
||||
form = SendConfirmationForm()
|
||||
form = form_class()
|
||||
|
||||
if form.validate_on_submit():
|
||||
send_confirmation_instructions(form.user)
|
||||
@@ -225,10 +228,12 @@ def confirm_email(token):
|
||||
def forgot_password():
|
||||
"""View function that handles a forgotten password request."""
|
||||
|
||||
form_class = _security.forgot_password_form
|
||||
|
||||
if request.json:
|
||||
form = ForgotPasswordForm(MultiDict(request.json))
|
||||
form = form_class(MultiDict(request.json))
|
||||
else:
|
||||
form = ForgotPasswordForm()
|
||||
form = form_class()
|
||||
|
||||
if form.validate_on_submit():
|
||||
send_reset_password_instructions(form.user)
|
||||
@@ -257,7 +262,7 @@ def reset_password(token):
|
||||
if invalid or expired:
|
||||
return redirect(url_for('forgot_password'))
|
||||
|
||||
form = ResetPasswordForm()
|
||||
form = _security.reset_password_form()
|
||||
|
||||
if form.validate_on_submit():
|
||||
after_this_request(_commit)
|
||||
|
||||
+7
-3
@@ -5,20 +5,24 @@ from tests.test_app.sqlalchemy import create_app
|
||||
|
||||
class SecurityTest(TestCase):
|
||||
|
||||
APP_KWARGS = {
|
||||
'register_blueprint': True,
|
||||
}
|
||||
AUTH_CONFIG = None
|
||||
|
||||
def setUp(self):
|
||||
super(SecurityTest, self).setUp()
|
||||
|
||||
app = self._create_app(self.AUTH_CONFIG or {})
|
||||
app_kwargs = self.APP_KWARGS
|
||||
app = self._create_app(self.AUTH_CONFIG or {}, **app_kwargs)
|
||||
app.debug = False
|
||||
app.config['TESTING'] = True
|
||||
|
||||
self.app = app
|
||||
self.client = app.test_client()
|
||||
|
||||
def _create_app(self, auth_config, register_blueprint=True):
|
||||
return create_app(auth_config, register_blueprint)
|
||||
def _create_app(self, auth_config, **kwargs):
|
||||
return create_app(auth_config, **kwargs)
|
||||
|
||||
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
|
||||
return self.client.get(route, follow_redirects=follow_redirects,
|
||||
|
||||
+115
-3
@@ -6,6 +6,11 @@ import simplejson as json
|
||||
|
||||
from flask.ext.security.utils import capture_registrations, \
|
||||
capture_reset_password_requests, capture_passwordless_login_requests
|
||||
from flask.ext.security.forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
|
||||
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
|
||||
PasswordlessLoginForm
|
||||
from flask.ext.security.forms import TextField, SubmitField, valid_user_email
|
||||
|
||||
|
||||
from tests import SecurityTest
|
||||
|
||||
@@ -468,13 +473,14 @@ class AsyncMailTaskTests(SecurityTest):
|
||||
|
||||
class NoBlueprintTests(SecurityTest):
|
||||
|
||||
APP_KWARGS = {
|
||||
'register_blueprint': False,
|
||||
}
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'USER_COUNT': 1
|
||||
}
|
||||
|
||||
def _create_app(self, auth_config):
|
||||
return super(NoBlueprintTests, self)._create_app(auth_config, False)
|
||||
|
||||
def test_login_endpoint_is_404(self):
|
||||
r = self._get('/login')
|
||||
self.assertEqual(404, r.status_code)
|
||||
@@ -483,3 +489,109 @@ class NoBlueprintTests(SecurityTest):
|
||||
auth = 'Basic ' + base64.b64encode("matt@lp.com:password")
|
||||
r = self._get('/http', headers={'Authorization': auth})
|
||||
self.assertIn('HTTP Authentication', r.data)
|
||||
|
||||
|
||||
class ExtendFormsTest(SecurityTest):
|
||||
|
||||
class MyLoginForm(LoginForm):
|
||||
email = TextField('My Login Email Address Field')
|
||||
|
||||
class MyRegisterForm(RegisterForm):
|
||||
email = TextField('My Register Email Address Field')
|
||||
|
||||
APP_KWARGS = {
|
||||
'login_form': MyLoginForm,
|
||||
'register_form': MyRegisterForm,
|
||||
}
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_CONFIRMABLE': False,
|
||||
'SECURITY_REGISTERABLE': True,
|
||||
}
|
||||
|
||||
def test_login_view(self):
|
||||
r = self._get('/login', follow_redirects=True)
|
||||
self.assertIn("My Login Email Address Field", r.data)
|
||||
|
||||
def test_register(self):
|
||||
r = self._get('/register', follow_redirects=True)
|
||||
self.assertIn("My Register Email Address Field", r.data)
|
||||
|
||||
|
||||
class RecoverableExtendFormsTest(SecurityTest):
|
||||
|
||||
class MyForgotPasswordForm(ForgotPasswordForm):
|
||||
email = TextField('My Forgot Password Email Address Field',
|
||||
validators=[valid_user_email])
|
||||
|
||||
class MyResetPasswordForm(ResetPasswordForm):
|
||||
submit = SubmitField("My Reset Password Submit Field")
|
||||
|
||||
APP_KWARGS = {
|
||||
'forgot_password_form': MyForgotPasswordForm,
|
||||
'reset_password_form': MyResetPasswordForm,
|
||||
}
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_RECOVERABLE': True,
|
||||
}
|
||||
|
||||
def test_forgot_password(self):
|
||||
r = self._get('/reset', follow_redirects=True)
|
||||
self.assertIn("My Forgot Password Email Address Field", r.data)
|
||||
|
||||
def test_reset_password(self):
|
||||
with capture_reset_password_requests() as requests:
|
||||
self.client.post('/reset',
|
||||
data=dict(email='joe@lp.com'),
|
||||
follow_redirects=True)
|
||||
token = requests[0]['token']
|
||||
r = self._get('/reset/' + token)
|
||||
self.assertIn("My Reset Password Submit Field", r.data)
|
||||
|
||||
|
||||
class PasswordlessExtendFormsTest(SecurityTest):
|
||||
|
||||
class MyPasswordlessLoginForm(PasswordlessLoginForm):
|
||||
email = TextField('My Passwordless Login Email Address Field')
|
||||
|
||||
APP_KWARGS = {
|
||||
'passwordless_login_form': MyPasswordlessLoginForm,
|
||||
}
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_PASSWORDLESS': True,
|
||||
}
|
||||
|
||||
def test_passwordless_login(self):
|
||||
r = self._get('/login', follow_redirects=True)
|
||||
self.assertIn("My Passwordless Login Email Address Field", r.data)
|
||||
|
||||
|
||||
class ConfirmableExtendFormsTest(SecurityTest):
|
||||
|
||||
class MyConfirmRegisterForm(ConfirmRegisterForm):
|
||||
email = TextField('My Confirm Register Email Address Field')
|
||||
|
||||
class MySendConfirmationForm(SendConfirmationForm):
|
||||
email = TextField('My Send Confirmation Email Address Field')
|
||||
|
||||
APP_KWARGS = {
|
||||
'confirm_register_form': MyConfirmRegisterForm,
|
||||
'send_confirmation_form': MySendConfirmationForm,
|
||||
}
|
||||
|
||||
AUTH_CONFIG = {
|
||||
'SECURITY_CONFIRMABLE': True,
|
||||
'SECURITY_REGISTERABLE': True,
|
||||
}
|
||||
|
||||
def test_register(self):
|
||||
r = self._get('/register', follow_redirects=True)
|
||||
self.assertIn("My Confirm Register Email Address Field", r.data)
|
||||
|
||||
|
||||
def test_send_confirmation(self):
|
||||
r = self._get('/confirm', follow_redirects=True)
|
||||
self.assertIn("My Send Confirmation Email Address Field", r.data)
|
||||
|
||||
|
||||
@@ -201,9 +201,9 @@ class DefaultSecurityTests(SecurityTest):
|
||||
|
||||
class MongoEngineSecurityTests(DefaultSecurityTests):
|
||||
|
||||
def _create_app(self, auth_config):
|
||||
def _create_app(self, auth_config, **kwargs):
|
||||
from tests.test_app.mongoengine import create_app
|
||||
return create_app(auth_config)
|
||||
return create_app(auth_config, **kwargs)
|
||||
|
||||
|
||||
class DefaultDatastoreTests(SecurityTest):
|
||||
@@ -231,6 +231,6 @@ class DefaultDatastoreTests(SecurityTest):
|
||||
|
||||
class MongoEngineDatastoreTests(DefaultDatastoreTests):
|
||||
|
||||
def _create_app(self, auth_config):
|
||||
def _create_app(self, auth_config, **kwargs):
|
||||
from tests.test_app.mongoengine import create_app
|
||||
return create_app(auth_config)
|
||||
return create_app(auth_config, **kwargs)
|
||||
|
||||
@@ -13,7 +13,7 @@ from flask.ext.security import Security, UserMixin, RoleMixin, \
|
||||
from tests.test_app import create_app as create_base_app, populate_data, \
|
||||
add_context_processors
|
||||
|
||||
def create_app(config):
|
||||
def create_app(config, **kwargs):
|
||||
app = create_base_app(config)
|
||||
|
||||
app.config['MONGODB_SETTINGS'] = dict(
|
||||
@@ -46,7 +46,7 @@ def create_app(config):
|
||||
Role.drop_collection()
|
||||
populate_data(app.config.get('USER_COUNT', None))
|
||||
|
||||
app.security = Security(app, MongoEngineUserDatastore(db, User, Role))
|
||||
app.security = Security(app, datastore=MongoEngineUserDatastore(db, User, Role), **kwargs)
|
||||
|
||||
add_context_processors(app.security)
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ from flask.ext.security import Security, UserMixin, RoleMixin, \
|
||||
from tests.test_app import create_app as create_base_app, populate_data, \
|
||||
add_context_processors
|
||||
|
||||
def create_app(config, register_blueprint=True):
|
||||
def create_app(config, **kwargs):
|
||||
app = create_base_app(config)
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
|
||||
#app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root@localhost/flask_security_test'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
@@ -50,8 +51,7 @@ def create_app(config, register_blueprint=True):
|
||||
db.create_all()
|
||||
populate_data(app.config.get('USER_COUNT', None))
|
||||
|
||||
app.security = Security(app, SQLAlchemyUserDatastore(db, User, Role),
|
||||
register_blueprint=register_blueprint)
|
||||
app.security = Security(app, datastore=SQLAlchemyUserDatastore(db, User, Role), **kwargs)
|
||||
|
||||
add_context_processors(app.security)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user