Skip to content

Commit

Permalink
Change the unauthz_handler to take a function name, not the function! (
Browse files Browse the repository at this point in the history
…#736)

This is a possible backwards compat issue and is documented as such.

Minor other documentation improvements.
  • Loading branch information
jwag956 authored Jan 23, 2023
1 parent 42b4e74 commit 56573f2
Show file tree
Hide file tree
Showing 7 changed files with 39 additions and 21 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Fixes
- (:issue:`732`) If `SECURITY_USERNAME_REQUIRED` was ``True`` then users couldn't login
with just an email.
- (:issue:`734`) If `SECURITY_USERNAME_ENABLE` is set, bleach is a requirement.
- (:pr:`xxx`) The default_unauthz_handler now takes a function name, not the function!

Backwards Compatibility Concerns
+++++++++++++++++++++++++++++++++
Expand All @@ -50,6 +51,8 @@ Backwards Compatibility Concerns
- After a successful update/change of a two-factor method, the user was redirected to
`SECURITY_POST_LOGIN_VIEW`. Now it redirects to `SECURITY_TWO_FACTOR_POST_SETUP_VIEW`
which defaults to `".two_factor_setup"`.
- The :meth:`.Security.unauthz_handler` now takes a function name - not the function -
which never made sense.

Version 5.0.2
-------------
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

# General information about the project.
project = "Flask-Security"
copyright = "2012-2022"
copyright = "2012-2023"
author = "Matt Wright & Chris Wagner"

# The version info for the project you're documenting, acts as replacement for
Expand Down
9 changes: 5 additions & 4 deletions docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,9 @@ The decision on whether to return JSON is based on:
Redirects
---------
Flask-Security uses redirects frequently (when using forms), and most of the redirect
destinations are configurable. When FS initiates a redirect it always (mostly) flashes a message
that provides some context. In addition, FS - both in its views and default templates attempt to propagate
any `next` query param and in fact, an existing `?next=/xx` with override most of the configuration redirect URLs.
destinations are configurable. When Flask-Security initiates a redirect it always (mostly) flashes a message
that provides some context. In addition, Flask-Security - both in its views and default templates attempt to propagate
any `next` query param and in fact, an existing `?next=/xx` will override most of the configuration redirect URLs.

As a complex example consider an unauthenticated user accessing a `@auth_required` endpoint, and the user has
two-factor authentication set up.:
Expand All @@ -550,4 +550,5 @@ two-factor authentication set up.:
* The two_factor_validation_form/template also pulls any `?next=/xx` and appends to the form action.
* When the `tf-validate` form is submitted it will do a POST("/tf-validate?next=/protected").
* Assuming a correct code, the user is authenticated and is redirected. That redirection first
looks for a 'next' in the request.args then in request.form and finally will use the value of `SECURITY_POST_LOGIN_VIEW`
looks for a 'next' in the request.args then in request.form and finally will use the value of `SECURITY_POST_LOGIN_VIEW`.
In this example it will find the ``next=/protected`` in the request.args and redirect to ``/protected``.
9 changes: 9 additions & 0 deletions docs/patterns.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Security Patterns
=================

.. danger::
Be aware that starting in Flask 2.2.0, they recommend extensions store context information
on ``g`` which is the application context. Prior to this many extensions (including
Flask-Security and Flask-Login) stored things like user credential information on the
request context. These are now stored on ``g`` i.e. the application context. It is imperative
that applications not mistakenly push their own application context and forget to pop it - in that
case Flask won't push a new application context nor will it pop it at the end of the request - thus
credential information could leak from one user request to another.

Authentication and Authorization
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security provides a set of authentication decorators:
Expand Down
17 changes: 10 additions & 7 deletions flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,8 @@ class Security:
:param totp_cls: Class to use as TOTP factory. Defaults to :class:`Totp`
:param username_util_cls: Class to use for normalizing and validating usernames.
Defaults to :class:`UsernameUtil`
:param webauthn_util_cls: Class to use for customizing WebAuthn registration
and signin. Defaults to :class:`WebauthnUtil`
:param mf_recovery_codes_util_cls: Class for generating, checking, encrypting
and decrypting recovery codes. Defaults to :class:`MfRecoveryCodesUtil`
:param oauth: An instance of authlib.integrations.flask_client.OAuth
Expand Down Expand Up @@ -1120,7 +1122,7 @@ class Security:
``password_validator`` removed in favor of the new ``password_util_cls``.
.. deprecated:: 5.0.0
Passing in a LoginManager instance.
Passing in a LoginManager instance. Removed in 5.1.0
.. deprecated:: 5.0.0
json_encoder_cls is no longer honored since Flask 2.2 has deprecated it.
"""
Expand Down Expand Up @@ -1236,7 +1238,7 @@ def __init__(
[timedelta, timedelta], "ResponseValue"
] = default_reauthn_handler
self._unauthz_handler: t.Callable[
[t.Callable[[t.Any], t.Any], t.Optional[t.List[str]]], "ResponseValue"
[str, t.Optional[t.List[str]]], "ResponseValue"
] = default_unauthz_handler
self._unauthorized_callback: t.Optional[t.Callable[[], "ResponseValue"]] = None
self._render_json: t.Callable[
Expand Down Expand Up @@ -1732,7 +1734,7 @@ def set_form_info(self, name: str, form_info: FormInfo) -> None:
Do not perform any validation as part of instantiation - many views have
a bunch of logic PRIOR to calling the form validator.
.. versionadded:: 5.x.x
.. versionadded:: 5.1.0
"""
if name not in self.forms.keys():
raise ValueError(f"Unknown form name {name}")
Expand Down Expand Up @@ -1797,9 +1799,7 @@ def want_json(self, fn: t.Callable[["flask.Request"], bool]) -> None:

def unauthz_handler(
self,
cb: t.Callable[
[t.Callable[[t.Any], t.Any], t.Optional[t.List[str]]], "ResponseValue"
],
cb: t.Callable[[str, t.Optional[t.List[str]]], "ResponseValue"],
) -> None:
"""
Callback for failed authorization.
Expand All @@ -1809,7 +1809,7 @@ def unauthz_handler(
:param cb: Callback function with signature (func, params)
:func: the decorator function (e.g. roles_required)
:func_name: the decorator function name (e.g. 'roles_required')
:params: list of what (if any) was passed to the decorator.
Should return a Response or something Flask can create a Response from.
Expand All @@ -1820,6 +1820,9 @@ def unauthz_handler(
message.
.. versionadded:: 3.3.0
.. versionchanged:: 5.1.0
Pass in the function name, not the function!
"""
self._unauthz_handler = cb

Expand Down
14 changes: 9 additions & 5 deletions flask_security/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def default_reauthn_handler(within, grace):
return redirect(redirect_url)


def default_unauthz_handler(func, params):
def default_unauthz_handler(func_name, params):
unauthz_message, unauthz_message_type = get_message("UNAUTHORIZED")
if _security._want_json(request):
payload = json_error_response(errors=unauthz_message)
Expand Down Expand Up @@ -482,7 +482,9 @@ def decorated_view(*args, **kwargs):
if _security._unauthorized_callback:
# Backwards compat - deprecated
return _security._unauthorized_callback()
return _security._unauthz_handler(roles_required, list(roles))
return _security._unauthz_handler(
roles_required.__name__, list(roles)
)
return fn(*args, **kwargs)

return decorated_view
Expand Down Expand Up @@ -514,7 +516,7 @@ def decorated_view(*args, **kwargs):
if _security._unauthorized_callback:
# Backwards compat - deprecated
return _security._unauthorized_callback()
return _security._unauthz_handler(roles_accepted, list(roles))
return _security._unauthz_handler(roles_accepted.__name__, list(roles))

return decorated_view

Expand Down Expand Up @@ -550,7 +552,7 @@ def decorated_view(*args, **kwargs):
# Backwards compat - deprecated
return _security._unauthorized_callback()
return _security._unauthz_handler(
permissions_required, list(fsperms)
permissions_required.__name__, list(fsperms)
)

return fn(*args, **kwargs)
Expand Down Expand Up @@ -588,7 +590,9 @@ def decorated_view(*args, **kwargs):
if _security._unauthorized_callback:
# Backwards compat - deprecated
return _security._unauthorized_callback()
return _security._unauthz_handler(permissions_accepted, list(fsperms))
return _security._unauthz_handler(
permissions_accepted.__name__, list(fsperms)
)

return decorated_view

Expand Down
6 changes: 2 additions & 4 deletions tests/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,9 @@ def my_json(payload, code, headers=None, user=None):

def test_my_unauthz_handler(app, client):
@app.security.unauthz_handler
def my_unauthz(func, params):
def my_unauthz(func_name, params):
return (
jsonify(
dict(myresponse={"func": func.__name__, "params": params}, code=403)
),
jsonify(dict(myresponse={"func": func_name, "params": params}, code=403)),
403,
)

Expand Down

0 comments on commit 56573f2

Please sign in to comment.