Skip to content

Commit b5d66c2

Browse files
authored
Make the instances of overridable extendable classes visible in the p… (#1081)
* Make the instances of overridable extendable classes visible in the public API, and document them. Mark as deprecated the ability to set these classes as part of init_ap(kwargs) or global app.config (only via Flask-Security construction). close #1076 * Make the instances of overridable extendable classes visible in the public API, and document them. Mark as deprecated the ability to set these classes as part of init_ap(kwargs) or global app.config (only via Flask-Security construction). Bunmp flask-babel 'low' version. close #1076
1 parent 2dfd2a4 commit b5d66c2

21 files changed

+178
-146
lines changed

CHANGES.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ Fixes
1212
+++++
1313
- (:issue:`1077`) Fix runtime modification of a config string (TWO_FACTOR_METHODS)
1414
- (:issue:`1078`) Fix CLI user_create when model doesn't contain username
15+
- (:issue:`1076`) xxx_util_cls instances should be public and documented.
16+
17+
Backwards Compatibility Concerns
18+
+++++++++++++++++++++++++++++++++
19+
As part of :issue:`1076` the following cleanup was done:
20+
21+
- The xxx_util_cls arguments are now stored in 'private' instance variables - they are never
22+
used after Flask-Security initialization and have never been documented.
23+
- The xxx_util_cls options should only be set as part of Flask-Security construction.
24+
Setting them via init_app(kwargs) or app.config["SECURITY_XX"] has been deprecated.
1525

1626
Version 5.6.0
1727
-------------

docs/customizing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ appropriate input attributes can be set)::
281281
# Side-effect - field.data is updated to normalized value.
282282
# Use proxy to we can declare this prior to initializing Security.
283283
_security = LocalProxy(lambda: app.extensions["security"])
284-
msg, field.data = _security._username_util.validate(field.data)
284+
msg, field.data = _security.username_util.validate(field.data)
285285
if msg:
286286
raise ValidationError(msg)
287287

flask_security/core.py

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,14 +1317,14 @@ def __init__(
13171317
self.app = app
13181318
self._datastore = datastore
13191319
self._register_blueprint = register_blueprint
1320-
self.mail_util_cls = mail_util_cls
1321-
self.password_util_cls = password_util_cls
1322-
self.phone_util_cls = phone_util_cls
1320+
self._mail_util_cls = mail_util_cls
1321+
self._password_util_cls = password_util_cls
1322+
self._phone_util_cls = phone_util_cls
13231323
self.render_template = render_template
1324-
self.totp_cls = totp_cls
1325-
self.username_util_cls = username_util_cls
1326-
self.webauthn_util_cls = webauthn_util_cls
1327-
self.mf_recovery_codes_util_cls = mf_recovery_codes_util_cls
1324+
self._totp_cls = totp_cls
1325+
self._username_util_cls = username_util_cls
1326+
self._webauthn_util_cls = webauthn_util_cls
1327+
self._mf_recovery_codes_util_cls = mf_recovery_codes_util_cls
13281328
self._oauth = oauth
13291329

13301330
# Forms - we create a list from constructor.
@@ -1543,7 +1543,7 @@ def init_app(
15431543
):
15441544
self._use_confirm_form = False
15451545

1546-
# The following will be set as attributes and initialized from either
1546+
# The following will be set as attributes and initialized from constructor or
15471547
# kwargs or config.
15481548
attr_names = [
15491549
"trackable",
@@ -1558,17 +1558,32 @@ def init_app(
15581558
"username_recovery",
15591559
"passwordless",
15601560
"webauthn",
1561+
"render_template",
1562+
"datetime_factory",
1563+
]
1564+
for attr in attr_names:
1565+
if ov := kwargs.get(attr, cv(attr.upper(), app, strict=False)):
1566+
setattr(self, attr, ov)
1567+
1568+
# Deprecated BC attrs - only settable at constructor time. Noter that newer
1569+
# _util classes (username_util_cls) already are this way
1570+
dep_attr_names = [
15611571
"mail_util_cls",
15621572
"password_util_cls",
15631573
"phone_util_cls",
1564-
"render_template",
15651574
"totp_cls",
15661575
"webauthn_util_cls",
1567-
"datetime_factory",
15681576
]
1569-
for attr in attr_names:
1577+
for attr in dep_attr_names:
15701578
if ov := kwargs.get(attr, cv(attr.upper(), app, strict=False)):
1571-
setattr(self, attr, ov)
1579+
setattr(self, f"_{attr}", ov)
1580+
warnings.warn(
1581+
f"Setting {attr} via kwargs or config is"
1582+
" deprecated as of version 5.6.1 and will be removed in a future"
1583+
" release. Use the Flask-Security constructor.",
1584+
DeprecationWarning,
1585+
stacklevel=2,
1586+
)
15721587

15731588
identity_loaded.connect_via(app)(_on_identity_loaded)
15741589

@@ -1597,12 +1612,12 @@ def init_app(
15971612
)
15981613

15991614
self.login_manager = _get_login_manager(app, self)
1600-
self._phone_util = self.phone_util_cls(app)
1601-
self._mail_util = self.mail_util_cls(app)
1602-
self._password_util = self.password_util_cls(app)
1603-
self._username_util = self.username_util_cls(app)
1604-
self._webauthn_util = self.webauthn_util_cls(app)
1605-
self._mf_recovery_codes_util = self.mf_recovery_codes_util_cls(app)
1615+
self._phone_util = self._phone_util_cls(app)
1616+
self._mail_util = self._mail_util_cls(app)
1617+
self._password_util = self._password_util_cls(app)
1618+
self._username_util = self._username_util_cls(app)
1619+
self._webauthn_util = self._webauthn_util_cls(app)
1620+
self._mf_recovery_codes_util = self._mf_recovery_codes_util_cls(app)
16061621
self.remember_token_serializer = _get_serializer(app, "remember")
16071622
self.login_serializer = _get_serializer(app, "login")
16081623
self.reset_serializer = _get_serializer(app, "reset")
@@ -1768,22 +1783,22 @@ def init_app(
17681783
sms_service = cv("SMS_SERVICE", app=app)
17691784
if sms_service == "Twilio": # pragma: no cover
17701785
self._check_modules("twilio", "SMS")
1771-
if self.phone_util_cls == PhoneUtil:
1786+
if self._phone_util_cls == PhoneUtil:
17721787
self._check_modules("phonenumbers", "SMS")
17731788

17741789
secrets = cv("TOTP_SECRETS", app=app)
17751790
issuer = cv("TOTP_ISSUER", app=app)
17761791
if not secrets or not issuer:
17771792
raise ValueError("Both TOTP_SECRETS and TOTP_ISSUER must be set")
1778-
self._totp_factory = self.totp_cls(secrets, issuer)
1793+
self._totp_factory = self._totp_cls(secrets, issuer)
17791794

17801795
if cv("PASSWORD_COMPLEXITY_CHECKER", app=app) == "zxcvbn":
17811796
self._check_modules("zxcvbn", "PASSWORD_COMPLEXITY_CHECKER")
17821797

17831798
if cv("WEBAUTHN", app=app):
17841799
self._check_modules("webauthn", "WEBAUTHN")
17851800

1786-
if cv("USERNAME_ENABLE", app=app) and self.username_util_cls == UsernameUtil:
1801+
if cv("USERNAME_ENABLE", app=app) and self._username_util_cls == UsernameUtil:
17871802
self._check_modules("bleach", "USERNAME_ENABLE")
17881803

17891804
# Register so other packages can reference our translations.
@@ -2048,6 +2063,48 @@ def reauthn_handler(
20482063
"""
20492064
self._reauthn_handler = cb
20502065

2066+
@property
2067+
def mail_util(self) -> MailUtil:
2068+
"""Instance of mail_util_cls created at init_app() time.
2069+
See :ref:`api:extendable classes`"""
2070+
return self._mail_util
2071+
2072+
@property
2073+
def password_util(self) -> PasswordUtil:
2074+
"""Instance of password_util_cls created at init_app() time.
2075+
See :ref:`api:extendable classes`"""
2076+
return self._password_util
2077+
2078+
@property
2079+
def username_util(self) -> UsernameUtil:
2080+
"""Instance of username_util_cls created at init_app() time.
2081+
See :ref:`api:extendable classes`"""
2082+
return self._username_util
2083+
2084+
@property
2085+
def phone_util(self) -> PhoneUtil:
2086+
"""Instance of phone_util_cls created at init_app() time.
2087+
See :ref:`api:extendable classes`"""
2088+
return self._phone_util
2089+
2090+
@property
2091+
def totp_factory(self) -> Totp:
2092+
"""Instance of totp_factory created at init_app() time.
2093+
See :ref:`api:extendable classes`"""
2094+
return self._totp_factory
2095+
2096+
@property
2097+
def mf_recovery_codes_util(self) -> MfRecoveryCodesUtil:
2098+
"""Instance of mf_recovery_codes_util_cls created at init_app() time.
2099+
See :ref:`api:extendable classes`"""
2100+
return self._mf_recovery_codes_util
2101+
2102+
@property
2103+
def webauthn_util(self) -> WebauthnUtil:
2104+
"""Instance of webauthn_util_cls created at init_app() time.
2105+
See :ref:`api:extendable classes`"""
2106+
return self._webauthn_util
2107+
20512108
def _add_ctx_processor(
20522109
self, endpoint: str, fn: t.Callable[[], dict[str, t.Any]]
20532110
) -> None:

flask_security/datastore.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ def create_user(self, **kwargs: t.Any) -> UserMixin:
446446
Best practice is::
447447
448448
try:
449-
enorm = app.security._mail_util.validate(email)
449+
enorm = app.security.mail_util.validate(email)
450450
except ValueError:
451451
452452
.. danger::
@@ -459,7 +459,7 @@ def create_user(self, **kwargs: t.Any) -> UserMixin:
459459
460460
Best practice is::
461461
462-
pbad, pnorm = app.security._password_util.validate(password, True)
462+
pbad, pnorm = app.security.password_util.validate(password, True)
463463
464464
Look for `pbad` being None. Pass the normalized password `pnorm` to this
465465
method.
@@ -533,7 +533,7 @@ def tf_set(
533533
This could be called from an application to apiori setup a user for two factor
534534
without the user having to go through the setup process.
535535
536-
To get a totp_secret - use ``app.security._totp_factory.generate_totp_secret()``
536+
To get a totp_secret - use ``app.security.totp_factory.generate_totp_secret()``
537537
538538
.. versionadded: 3.4.1
539539
"""
@@ -629,7 +629,7 @@ def us_set(
629629
This could be called from an application to apiori setup a user for unified
630630
sign in without the user having to go through the setup process.
631631
632-
To get a totp_secret - use ``app.security._totp_factory.generate_totp_secret()``
632+
To get a totp_secret - use ``app.security.totp_factory.generate_totp_secret()``
633633
634634
.. versionadded:: 3.4.1
635635
"""
@@ -673,7 +673,7 @@ def us_setup_email(self, user: UserMixin) -> bool:
673673
if not cv("UNIFIED_SIGNIN") or "email" not in cv("US_ENABLED_METHODS"):
674674
return False
675675
totp_secrets = self.us_get_totp_secrets(user)
676-
totp_secrets["email"] = _security._totp_factory.generate_totp_secret()
676+
totp_secrets["email"] = _security.totp_factory.generate_totp_secret()
677677
self.us_put_totp_secrets(user, totp_secrets)
678678
return True
679679

flask_security/forms.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,9 @@ def __call__(self, form, field):
164164

165165
try:
166166
if self.verify:
167-
field.data = _security._mail_util.validate(field.data)
167+
field.data = _security.mail_util.validate(field.data)
168168
else:
169-
field.data = _security._mail_util.normalize(field.data)
169+
field.data = _security.mail_util.normalize(field.data)
170170
except EmailValidateException as e:
171171
# we stop further validators if email isn't valid.
172172
# TODO: email_validator provides some really nice error messages - however
@@ -228,7 +228,7 @@ def unique_user_email(form, field):
228228

229229
def username_validator(form, field):
230230
# Side-effect - field.data is updated to normalized value.
231-
msg, field.data = _security._username_util.validate(field.data)
231+
msg, field.data = _security.username_util.validate(field.data)
232232
if msg:
233233
raise ValidationError(msg)
234234

@@ -599,7 +599,7 @@ def validate(self, **kwargs: t.Any) -> bool:
599599
# Reduce timing variation between existing and non-existing users
600600
hash_password(self.password.data)
601601
return False
602-
self.password.data = _security._password_util.normalize(self.password.data)
602+
self.password.data = _security.password_util.normalize(self.password.data)
603603
if not self.user.verify_and_update_password(self.password.data):
604604
self.password.errors.append(get_message("INVALID_PASSWORD")[0])
605605
return False
@@ -657,7 +657,7 @@ def validate(self, **kwargs: t.Any) -> bool:
657657
return False
658658

659659
assert self.password.data is not None
660-
self.password.data = _security._password_util.normalize(self.password.data)
660+
self.password.data = _security.password_util.normalize(self.password.data)
661661
assert isinstance(self.password.errors, list)
662662
if not self.user.verify_and_update_password(self.password.data):
663663
self.password.errors.append(get_message("INVALID_PASSWORD")[0])
@@ -713,7 +713,7 @@ def validate(self, **kwargs: t.Any) -> bool:
713713
if hasattr(_datastore.user_model, k):
714714
rfields[k] = v
715715
del rfields["password"]
716-
pbad, self.password.data = _security._password_util.validate(
716+
pbad, self.password.data = _security.password_util.validate(
717717
self.password.data, True, **rfields
718718
)
719719
if pbad:
@@ -823,7 +823,7 @@ def validate(self, **kwargs: t.Any) -> bool:
823823
if hasattr(_datastore.user_model, k):
824824
rfields[k] = v
825825
del rfields["password"]
826-
pbad, self.password.data = _security._password_util.validate(
826+
pbad, self.password.data = _security.password_util.validate(
827827
self.password.data, True, **rfields
828828
)
829829
if pbad:
@@ -877,7 +877,7 @@ def validate(self, **kwargs: t.Any) -> bool:
877877

878878
assert isinstance(self.password.errors, list)
879879
assert self.password.data is not None
880-
pbad, self.password.data = _security._password_util.validate(
880+
pbad, self.password.data = _security.password_util.validate(
881881
self.password.data, False, user=self.user
882882
)
883883
if pbad:
@@ -922,7 +922,7 @@ def validate(self, **kwargs: t.Any) -> bool:
922922
self.password.errors.append(get_message("PASSWORD_NOT_PROVIDED")[0])
923923
return False
924924

925-
self.password.data = _security._password_util.normalize(self.password.data)
925+
self.password.data = _security.password_util.normalize(self.password.data)
926926
if not verify_password(self.password.data, current_user.password):
927927
self.password.errors.append(get_message("INVALID_PASSWORD")[0])
928928
return False
@@ -931,7 +931,7 @@ def validate(self, **kwargs: t.Any) -> bool:
931931
return False
932932

933933
assert self.new_password.data is not None
934-
pbad, self.new_password.data = _security._password_util.validate(
934+
pbad, self.new_password.data = _security.password_util.validate(
935935
self.new_password.data, False, user=current_user
936936
)
937937
if pbad:
@@ -980,7 +980,7 @@ def validate(self, **kwargs: t.Any) -> bool:
980980
if not self.phone.data:
981981
self.phone.errors.append(get_message("PHONE_INVALID")[0])
982982
return False
983-
msg = _security._phone_util.validate_phone_number(self.phone.data)
983+
msg = _security.phone_util.validate_phone_number(self.phone.data)
984984
if msg:
985985
self.phone.errors.append(msg)
986986
return False
@@ -1020,7 +1020,7 @@ def validate(self, **kwargs: t.Any) -> bool:
10201020
assert self.user is not None
10211021
assert self.code.data is not None
10221022
assert isinstance(self.code.errors, list)
1023-
if not _security._totp_factory.verify_totp(
1023+
if not _security.totp_factory.verify_totp(
10241024
token=self.code.data,
10251025
totp_secret=self.tf_totp_secret,
10261026
user=self.user,

flask_security/recovery_codes.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def create_recovery_codes(self, user: UserMixin) -> list[str]:
7777
If configured (:data:`SECURITY_MULTI_FACTOR_RECOVERY_CODES_KEYS`),
7878
codes are stored encrypted - but plainttext versions are returned.
7979
"""
80-
new_codes = _security._totp_factory.generate_recovery_codes(
80+
new_codes = _security.totp_factory.generate_recovery_codes(
8181
cv("MULTI_FACTOR_RECOVERY_CODES_N")
8282
)
8383
_datastore.mf_set_recovery_codes(user, self._encrypt_codes(new_codes))
@@ -173,7 +173,7 @@ def validate(self, **kwargs: t.Any) -> bool:
173173
assert self.user is not None
174174
assert self.code.data is not None # RequiredLocalize validator
175175
assert isinstance(self.code.errors, list)
176-
if not _security._mf_recovery_codes_util.check_recovery_code(
176+
if not _security.mf_recovery_codes_util.check_recovery_code(
177177
self.user, self.code.data
178178
):
179179
self.code.errors.append(get_message("INVALID_RECOVERY_CODE")[0])
@@ -198,7 +198,7 @@ def mf_recovery_codes() -> ResponseValue:
198198

199199
if form.validate_on_submit():
200200
# generate new codes
201-
codes = _security._mf_recovery_codes_util.create_recovery_codes(current_user)
201+
codes = _security.mf_recovery_codes_util.create_recovery_codes(current_user)
202202
after_this_request(view_commit)
203203
if _security._want_json(request):
204204
payload = dict(recovery_codes=codes)
@@ -210,7 +210,7 @@ def mf_recovery_codes() -> ResponseValue:
210210
**_security._run_ctx_processor("mf_recovery_codes"),
211211
)
212212

213-
codes = _security._mf_recovery_codes_util.get_recovery_codes(current_user)
213+
codes = _security.mf_recovery_codes_util.get_recovery_codes(current_user)
214214
if _security._want_json(request):
215215
return base_render_json(
216216
form, include_user=False, additional=dict(recovery_codes=codes)
@@ -243,9 +243,7 @@ def mf_recovery():
243243

244244
if form.validate_on_submit():
245245
# Valid code - we want these to be one time - so remove it from list
246-
_security._mf_recovery_codes_util.delete_recovery_code(
247-
form.user, form.code.data
248-
)
246+
_security.mf_recovery_codes_util.delete_recovery_code(form.user, form.code.data)
249247
after_this_request(view_commit)
250248

251249
# In the recovery case - don't set/offer validity token.

flask_security/twofactor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def tf_send_security_token(user, method, totp_secret, phone_number):
6060
Flask-Security code should NOT call this directly -
6161
call :meth:`.UserMixin.tf_send_security_token`
6262
"""
63-
token_to_be_sent = _security._totp_factory.generate_totp_password(totp_secret)
63+
token_to_be_sent = _security.totp_factory.generate_totp_password(totp_secret)
6464
if method == "email" or method == "mail":
6565
send_mail(
6666
cv("EMAIL_SUBJECT_TWO_FACTOR"),

0 commit comments

Comments
 (0)