forked from lingthio/Flask-User
-
Notifications
You must be signed in to change notification settings - Fork 0
/
user_manager.py
471 lines (377 loc) · 19.9 KB
/
user_manager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
"""This module implements the main UserManager class for Flask-User.
"""
# Author: Ling Thio <[email protected]>
# Copyright (c) 2013 Ling Thio'
import datetime
from flask import abort, Blueprint, current_app, Flask, session
from flask_login import LoginManager
from wtforms import ValidationError
from . import ConfigError
from . import forms
from .db_manager import DBManager
from .email_manager import EmailManager
from .password_manager import PasswordManager
from .token_manager import TokenManager
from .translation_utils import lazy_gettext as _ # map _() to lazy_gettext()
from .user_manager__settings import UserManager__Settings
from .user_manager__utils import UserManager__Utils
from .user_manager__views import UserManager__Views
# The UserManager is implemented across several source code files.
# Mixins are used to aggregate all member functions into the one UserManager class for ease of customization.
class UserManager(UserManager__Settings, UserManager__Utils, UserManager__Views):
""" Customizable User Authentication and Management.
"""
def __init__(self, app, db, UserClass, **kwargs):
"""
Args:
app(Flask): The Flask application instance.
db: An Object-Database Mapper instance such as SQLAlchemy or MongoEngine.
UserClass: The User class (*not* an instance!).
Keyword Args:
UserEmailClass: The optional UserEmail class (*not* an instance!).
Required for the 'multiple emails per user' feature.
UserInvitationClass: The optional UserInvitation class (*not* an instance!).
Required for the 'register by invitation' feature.
Example:
``user_manager = UserManager(app, db, User, UserEmailClass=UserEmail)``
.. This hack shows a header above the _next_ section
.. code-block:: none
Customizable UserManager methods
"""
#see http://flask.pocoo.org/docs/0.12/extensiondev/#the-extension-code """
self.app = app
if app:
self.init_app(app, db, UserClass, **kwargs)
def init_app(
self, app, db, UserClass,
UserInvitationClass=None,
UserEmailClass=None,
RoleClass=None, # Only used for testing
):
# See http://flask.pocoo.org/docs/0.12/extensiondev/#the-extension-code
# Perform Class type checking
if not isinstance(app, Flask):
raise TypeError("flask_user.UserManager.init_app(): Parameter 'app' is an instance of class '%s' "
"instead of a subclass of class 'flask.Flask'."
% app.__class__.__name__)
# Bind Flask-User to app
app.user_manager = self
# Remember all data-models
# ------------------------
self.db = db
# self.db_manager.UserClass = UserClass
# self.db_manager.UserEmailClass = UserEmailClass
# self.UserInvitationClass = UserInvitationClass
# self.RoleClass=RoleClass
# Load app config settings
# ------------------------
# For each 'UserManager.USER_...' property: load settings from the app config.
for attrib_name in dir(self):
if attrib_name[0:5] == 'USER_':
default_value = getattr(UserManager, attrib_name)
setattr(self, attrib_name, app.config.get(attrib_name, default_value))
# If USER_EMAIL_SENDER_EMAIL is not set, try to construct it from
# MAIL_DEFAULT_SENDER or DEFAULT_MAIL_SENDER
if not self.USER_EMAIL_SENDER_EMAIL:
default_sender = app.config.get('DEFAULT_MAIL_SENDER', None)
default_sender = app.config.get('MAIL_DEFAULT_SENDER', default_sender)
if default_sender:
# Accept two formats: '{name}<{email}>' or plain '{email}'
if default_sender[-1:] == '>':
start = default_sender.rfind('<')
if start >= 1:
self.USER_EMAIL_SENDER_EMAIL = default_sender[start + 1:-1]
if not self.USER_EMAIL_SENDER_NAME:
self.USER_EMAIL_SENDER_NAME = default_sender[0:start].strip(' "')
else:
self.USER_EMAIL_SENDER_EMAIL = default_sender
# If USER_EMAIL_SENDER_NAME is not set, default it to USER_APP_NAME
if not self.USER_EMAIL_SENDER_NAME:
self.USER_EMAIL_SENDER_NAME = self.USER_APP_NAME
# Configure Flask session behavior
# --------------------------------
if self.USER_USER_SESSION_EXPIRATION:
app.permanent_session_lifetime = datetime.timedelta(seconds=self.USER_USER_SESSION_EXPIRATION)
@app.before_request
def advance_session_timeout():
session.permanent = True # Timeout after app.permanent_session_lifetime period
session.modified = True # Advance session timeout each time a user visits a page
# Configure Flask-Login
# --------------------
# Setup default LoginManager using Flask-Login
self.login_manager = LoginManager(app)
self.login_manager.login_view = 'user.login'
# Flask-Login calls this function to retrieve a User record by token.
@self.login_manager.user_loader
def load_user_by_user_token(user_token):
user = self.db_manager.UserClass.get_user_by_token(user_token)
return user
# Configure Flask-BabelEx
# -----------------------
self.babel = app.extensions.get('babel', None)
from .translation_utils import init_translations
init_translations(self.babel)
# Configure Jinja2
# ----------------
# If the application has not initialized BabelEx,
# we must provide a NULL translation to Jinja2
if not hasattr(app.jinja_env, 'install_gettext_callables'):
app.jinja_env.add_extension('jinja2.ext.i18n')
app.jinja_env.install_null_translations()
# Define a context processor to provide custom variable and functions available to Jinja2 templates
def flask_user_context_processor():
# In Flask-Login 0.2 ``is_authenticated`` and ``is_active`` were implemented as functions,
# while in 0.3+ they are implemented as properties.
def call_or_get(function_or_property):
return function_or_property() if callable(function_or_property) else function_or_property
return dict(
user_manager=current_app.user_manager,
call_or_get=call_or_get,
)
# Register context processor with Jinja2
app.context_processor(flask_user_context_processor)
# Create a dummy Blueprint to add the app/templates/flask_user dir to the template search path
blueprint = Blueprint('flask_user', __name__, template_folder='templates')
app.register_blueprint(blueprint)
# Set default form classes
# ------------------------
self.AddEmailFormClass = forms.AddEmailForm
self.ChangePasswordFormClass = forms.ChangePasswordForm
self.ChangeUsernameFormClass = forms.ChangeUsernameForm
self.EditUserProfileFormClass = forms.EditUserProfileForm
self.ForgotPasswordFormClass = forms.ForgotPasswordForm
self.InviteUserFormClass = forms.InviteUserForm
self.LoginFormClass = forms.LoginForm
self.RegisterFormClass = forms.RegisterForm
self.ResendEmailConfirmationFormClass = forms.ResendEmailConfirmationForm
self.ResetPasswordFormClass = forms.ResetPasswordForm
# Set default managers
# --------------------
# Setup DBManager
self.db_manager = DBManager(app, db, UserClass, UserEmailClass, UserInvitationClass, RoleClass)
# Setup PasswordManager
self.password_manager = PasswordManager(app)
# Set default EmailAdapter
if self.USER_ENABLE_EMAIL:
from .email_adapters.smtp_email_adapter import SMTPEmailAdapter
self.email_adapter = SMTPEmailAdapter(app)
# Setup EmailManager
if self.USER_ENABLE_EMAIL:
self.email_manager = EmailManager(app)
# Setup TokenManager
self.token_manager = TokenManager(app)
# Allow developers to customize UserManager
self.customize(app)
# Make sure the settings are valid -- raise ConfigError if not
self._check_settings(app)
# Configure a list of URLs to route to their corresponding view method.
self._add_url_routes(app)
def customize(self, app):
""" Override this method to customize properties.
Example::
# Customize Flask-User
class CustomUserManager(UserManager):
def customize(self, app):
# Add custom managers and email mailers here
self.email_manager = CustomEmailManager(app)
self.password_manager = CustomPasswordManager(app)
self.token_manager = CustomTokenManager(app)
self.email_adapter = CustomEmailAdapter(app)
# Setup Flask-User
user_manager = CustomUserManager(app, db, User)
"""
def password_validator(self, form, field):
"""Ensure that passwords have at least 6 characters with one lowercase letter, one uppercase letter and one number.
Override this method to customize the password validator.
"""
# Convert string to list of characters
password = list(field.data)
password_length = len(password)
# Count lowercase, uppercase and numbers
lowers = uppers = digits = 0
for ch in password:
if ch.islower(): lowers += 1
if ch.isupper(): uppers += 1
if ch.isdigit(): digits += 1
# Password must have one lowercase letter, one uppercase letter and one digit
is_valid = password_length >= 6 and lowers and uppers and digits
if not is_valid:
raise ValidationError(
_('Password must have at least 6 characters with one lowercase letter, one uppercase letter and one number'))
# If you prefer using Regex:
# from re import compile
# PASSWORD_REGEX = compile(r'\A(?=\S*?\d)(?=\S*?[A-Z])(?=\S*?[a-z])\S{6,}\Z')
# def password_is_valid(password):
# return PASSWORD_REGEX.match(password) is not None
def username_validator(self, form, field):
"""Ensure that Usernames contains at least 3 alphanumeric characters.
Override this method to customize the username validator.
"""
username = field.data
if len(username) < 3:
raise ValidationError(
_('Username must be at least 3 characters long'))
valid_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._'
chars = list(username)
for char in chars:
if char not in valid_chars:
raise ValidationError(
_("Username may only contain letters, numbers, '-', '.' and '_'"))
# If you prefer using Regex:
# from re import compile
# USERNAME_REGEX = compile(r'\A[\w\-\.]{3,}\Z')
# def username_is_valid(username):
# return USERNAME_REGEX.match(username) is not None
# ***** Private methods *****
def _check_settings(self, app):
"""Verify required settings. Produce a helpful error messages for incorrect settings."""
# Check for invalid settings
# --------------------------
# Check UserInvitationClass and USER_ENABLE_INVITE_USER
if self.USER_ENABLE_INVITE_USER and not self.db_manager.UserInvitationClass:
raise ConfigError(
'UserInvitationClass is missing while USER_ENABLE_INVITE_USER is True.' \
' Specify UserInvitationClass with UserManager(app, db, User, UserInvitationClass=...' \
' or set USER_ENABLE_INVITE_USER=False.')
# Check for deprecated settings
# -----------------------------
# Check for deprecated USER_ENABLE_CONFIRM_EMAIL
setting = app.config.get('USER_ENABLE_LOGIN_WITHOUT_CONFIRM_EMAIL', None)
if setting is not None:
print(
'Deprecation warning: USER_ENABLE_LOGIN_WITHOUT_CONFIRM_EMAIL'\
' will be deprecated.' \
' It has been replaced by USER_ALLOW_LOGIN_WITHOUT_CONFIRMED_EMAIL.'\
' Please change this as soon as possible.')
self.USER_ALLOW_LOGIN_WITHOUT_CONFIRMED_EMAIL = setting
# Check for deprecated USER_ENABLE_RETYPE_PASSWORD
setting = app.config.get('USER_ENABLE_RETYPE_PASSWORD', None)
if setting is not None:
print(
'Deprecation warning: USER_ENABLE_RETYPE_PASSWORD'\
' will be deprecated.' \
' It has been replaced with USER_REQUIRE_RETYPE_PASSWORD.'\
' Please change this as soon as possible.')
self.USER_REQUIRE_RETYPE_PASSWORD = setting
# Check for deprecated USER_SHOW_USERNAME_EMAIL_DOES_NOT_EXIST
setting = app.config.get('USER_SHOW_USERNAME_EMAIL_DOES_NOT_EXIST', None)
if setting is not None:
print(
'Deprecation warning: USER_SHOW_USERNAME_EMAIL_DOES_NOT_EXIST' \
' will be deprecated.' \
' It has been replaced with USER_SHOW_USERNAME_DOES_NOT_EXIST'
' and USER_SHOW_EMAIL_DOES_NOT_EXIST.'
' Please change this as soon as possible.')
self.USER_SHOW_USERNAME_DOES_NOT_EXIST = setting
self.USER_SHOW_EMAIL_DOES_NOT_EXIST = setting
# Check for deprecated USER_PASSWORD_HASH
setting = app.config.get('USER_PASSWORD_HASH', None)
if setting is not None:
print(
"Deprecation warning: USER_PASSWORD_HASH=<string>"\
" will be deprecated."\
" It has been replaced with USER_PASSLIB_CRYPTCONTEXT_SCHEMES=<list>."
" Please change USER_PASSWORD_HASH='something' to"\
" USER_PASSLIB_CRYPTCONTEXT_SCHEMES=['something'] as soon as possible.")
self.USER_PASSLIB_CRYPTCONTEXT_SCHEMES = [setting]
# Check that USER_EMAIL_SENDER_EMAIL is set when USER_ENABLE_EMAIL is True
if not self.USER_EMAIL_SENDER_EMAIL and self.USER_ENABLE_EMAIL:
raise ConfigError(
'USER_EMAIL_SENDER_EMAIL is missing while USER_ENABLE_EMAIL is True.'\
' specify USER_EMAIL_SENDER_EMAIL (and USER_EMAIL_SENDER_NAME) or set USER_ENABLE_EMAIL to False.')
# Disable settings that rely on a feature setting that's not enabled
# ------------------------------------------------------------------
# USER_ENABLE_REGISTER=True must have USER_ENABLE_USERNAME=True or USER_ENABLE_EMAIL=True.
if not self.USER_ENABLE_USERNAME and not self.USER_ENABLE_EMAIL:
self.USER_ENABLE_REGISTER = False
# Settings that depend on USER_ENABLE_EMAIL
if not self.USER_ENABLE_EMAIL:
self.USER_ENABLE_CONFIRM_EMAIL = False
self.USER_ENABLE_MULTIPLE_EMAILS = False
self.USER_ENABLE_FORGOT_PASSWORD = False
self.USER_SEND_PASSWORD_CHANGED_EMAIL = False
self.USER_SEND_REGISTERED_EMAIL = False
self.USER_SEND_USERNAME_CHANGED_EMAIL = False
self.USER_REQUIRE_INVITATION = False
# Settings that depend on USER_ENABLE_USERNAME
if not self.USER_ENABLE_USERNAME:
self.USER_ENABLE_CHANGE_USERNAME = False
def _add_url_routes(self, app):
"""Configure a list of URLs to route to their corresponding view method.."""
# Because methods contain an extra ``self`` parameter, URL routes are mapped
# to stub functions, which simply call the corresponding method.
# For testing purposes, we map all available URLs to stubs, but the stubs
# contain config checks to return 404 when a feature is disabled.
# Define the stubs
# ----------------
# def auth0_callback_stub():
# if not self.USER_ENABLE_AUTH0: abort(404)
# return self.auth0_callback_view()
def change_password_stub():
if not self.USER_ENABLE_CHANGE_PASSWORD: abort(404)
return self.change_password_view()
def change_username_stub():
if not self.USER_ENABLE_CHANGE_USERNAME: abort(404)
return self.change_username_view()
def confirm_email_stub(token):
if not self.USER_ENABLE_CONFIRM_EMAIL: abort(404)
return self.confirm_email_view(token)
def edit_user_profile_stub():
return self.edit_user_profile_view()
def email_action_stub(id, action):
if not self.USER_ENABLE_MULTIPLE_EMAILS or not self.db_manager.UserEmailClass: abort(404)
return self.email_action_view(id, action)
def forgot_password_stub():
if not self.USER_ENABLE_FORGOT_PASSWORD: abort(404)
return self.forgot_password_view()
def manage_emails_stub():
if not self.USER_ENABLE_MULTIPLE_EMAILS or not self.db_manager.UserEmailClass: abort(404)
return self.manage_emails_view()
def invite_user_stub():
if not self.USER_ENABLE_INVITE_USER: abort(404)
return self.invite_user_view()
def login_stub():
return self.login_view()
def logout_stub():
return self.logout_view()
def register_stub():
if not self.USER_ENABLE_REGISTER: abort(404)
return self.register_view()
def resend_email_confirmation_stub():
if not self.USER_ENABLE_CONFIRM_EMAIL: abort(404)
return self.resend_email_confirmation_view()
def reset_password_stub(token):
if not self.USER_ENABLE_FORGOT_PASSWORD: abort(404)
return self.reset_password_view(token)
# def unconfirmed_email_stub():
# return self.unconfirmed_email_view()
def unauthorized_stub():
return self.unauthorized_view()
# Add the URL routes
# ------------------
# app.add_url_rule('/callbacks/auth0', 'user.auth0_callback', auth0_callback_stub)
app.add_url_rule(self.USER_CHANGE_PASSWORD_URL, 'user.change_password', change_password_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_CHANGE_USERNAME_URL, 'user.change_username', change_username_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_CONFIRM_EMAIL_URL, 'user.confirm_email', confirm_email_stub)
app.add_url_rule(self.USER_EDIT_USER_PROFILE_URL, 'user.edit_user_profile', edit_user_profile_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_EMAIL_ACTION_URL, 'user.email_action', email_action_stub)
app.add_url_rule(self.USER_FORGOT_PASSWORD_URL, 'user.forgot_password', forgot_password_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_INVITE_USER_URL, 'user.invite_user', invite_user_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_LOGIN_URL, 'user.login', login_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_LOGOUT_URL, 'user.logout', logout_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_MANAGE_EMAILS_URL, 'user.manage_emails', manage_emails_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_REGISTER_URL, 'user.register', register_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_RESEND_EMAIL_CONFIRMATION_URL, 'user.resend_email_confirmation',
resend_email_confirmation_stub,
methods=['GET', 'POST'])
app.add_url_rule(self.USER_RESET_PASSWORD_URL, 'user.reset_password', reset_password_stub,
methods=['GET', 'POST'])