From 9467fab92b0b90b4d8355c6814a6491ae585588b Mon Sep 17 00:00:00 2001 From: Chris Wagner Date: Fri, 10 Nov 2023 20:11:23 -0800 Subject: [PATCH] Remove long-deprecated _unauthorized_callback method. Also - add deprecation notice for BACKWARDS_COMPAT_UNAUTHN configuration. --- CHANGES.rst | 4 +++- flask_security/core.py | 19 +++++++++--------- flask_security/decorators.py | 39 ++++++------------------------------ tests/test_common.py | 25 +++++++++-------------- tests/test_misc.py | 19 ------------------ 5 files changed, 27 insertions(+), 79 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2e7150d4..d2ec467e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,8 +13,9 @@ Fixes - (:issue:`845`) us-signin magic link should use fs_uniquifier (not email) - (:pr:`873`) Update Spanish and Italian translations (gissimo) -- (:pr:`xxx`) Make AnonymousUser optional and deprecated +- (:pr:`877`) Make AnonymousUser optional and deprecated - (:issue:`875`) user_datastore.create_user has side effects on mutable inputs (NoRePercussions) +- (:pr:`xxx`) The long deprecated _unauthorized_callback/handler handler has been removed. Notes ++++++ @@ -31,6 +32,7 @@ Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Passing in an AnonymousUser class as part of Security initialization has been removed. +- The never-public method _get_unauthorized_response method has been removed. Version 5.3.2 diff --git a/flask_security/core.py b/flask_security/core.py index c43ca96d..9f17a7b9 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -1239,7 +1239,6 @@ def __init__( self._unauthz_handler: t.Callable[ [str, t.Optional[t.List[str]]], "ResponseValue" ] = default_unauthz_handler - self._unauthorized_callback: t.Optional[t.Callable[[], "ResponseValue"]] = None self._render_json: t.Callable[ [t.Dict[str, t.Any], int, t.Optional[t.Dict[str, str]], t.Optional["User"]], "ResponseValue", @@ -1474,6 +1473,15 @@ def init_app( DeprecationWarning, stacklevel=2, ) + if cv("BACKWARDS_COMPAT_UNAUTHN", app=app): + warnings.warn( + "The BACKWARDS_COMPAT_UNAUTHN configuration variable is" + "deprecated as of version 5.4.0 and will be removed in a future" + "release.", + DeprecationWarning, + stacklevel=2, + ) + if cv("USERNAME_ENABLE", app): if hasattr(self.datastore, "user_model") and not hasattr( self.datastore.user_model, "username" @@ -1878,15 +1886,6 @@ def reauthn_handler( """ self._reauthn_handler = cb - def unauthorized_handler(self, cb: t.Callable[[], "ResponseValue"]) -> None: - warnings.warn( - "'unauthorized_handler' has been replaced with" - " 'unauthz_handler' and 'unauthn_handler' and will be removed in 5.4", - DeprecationWarning, - stacklevel=2, - ) - self._unauthorized_callback = cb - def _add_ctx_processor( self, endpoint: str, fn: t.Callable[[], t.Dict[str, t.Any]] ) -> None: diff --git a/flask_security/decorators.py b/flask_security/decorators.py index 9716eca6..bfd06860 100644 --- a/flask_security/decorators.py +++ b/flask_security/decorators.py @@ -59,11 +59,6 @@ def _get_unauthenticated_response(text=None, headers=None): return Response(text, 401, headers) -def _get_unauthorized_response(text=None, headers=None): # pragma: no cover - # People called this - even though it isn't public - no harm in keeping it. - return _get_unauthenticated_response(text, headers) - - def default_unauthn_handler(mechanisms, headers=None): """Default callback for failures to authenticate @@ -244,12 +239,9 @@ def wrapper(*args, **kwargs): handle_csrf("basic") set_request_attr("fs_authn_via", "basic") return fn(*args, **kwargs) - if _security._unauthorized_callback: - return _security._unauthorized_callback() - else: - r = _security.default_http_auth_realm if callable(realm) else realm - h = {"WWW-Authenticate": f'Basic realm="{r}"'} - return _security._unauthn_handler(["basic"], headers=h) + r = _security.default_http_auth_realm if callable(realm) else realm + h = {"WWW-Authenticate": f'Basic realm="{r}"'} + return _security._unauthn_handler(["basic"], headers=h) return wrapper @@ -274,10 +266,7 @@ def decorated(*args, **kwargs): handle_csrf("token") set_request_attr("fs_authn_via", "token") return fn(*args, **kwargs) - if _security._unauthorized_callback: - return _security._unauthorized_callback() - else: - return _security._unauthn_handler(["token"]) + return _security._unauthn_handler(["token"]) return t.cast(DecoratedView, decorated) @@ -324,8 +313,7 @@ def dashboard(): The first mechanism that succeeds is used, following that, depending on configuration, CSRF protection will be tested. - On authentication failure `.Security.unauthorized_callback` (deprecated) - or :meth:`.Security.unauthn_handler` will be called. + On authentication failure :meth:`.Security.unauthn_handler` will be called. As a side effect, upon successful authentication, the request global ``fs_authn_via`` will be set to the method ("basic", "token", "session") @@ -403,10 +391,7 @@ def decorated_view( handle_csrf(method) set_request_attr("fs_authn_via", method) return fn(*args, **kwargs) - if _security._unauthorized_callback: - return _security._unauthorized_callback() - else: - return _security._unauthn_handler(ams, headers=h) + return _security._unauthn_handler(ams, headers=h) return decorated_view @@ -489,9 +474,6 @@ def decorated_view(*args, **kwargs): perms = [Permission(RoleNeed(role)) for role in roles] for perm in perms: if not perm.can(): - if _security._unauthorized_callback: - # Backwards compat - deprecated - return _security._unauthorized_callback() return _security._unauthz_handler( roles_required.__name__, list(roles) ) @@ -523,9 +505,6 @@ def decorated_view(*args, **kwargs): perm = Permission(*(RoleNeed(role) for role in roles)) if perm.can(): return fn(*args, **kwargs) - if _security._unauthorized_callback: - # Backwards compat - deprecated - return _security._unauthorized_callback() return _security._unauthz_handler(roles_accepted.__name__, list(roles)) return decorated_view @@ -558,9 +537,6 @@ def decorated_view(*args, **kwargs): perms = [Permission(FsPermNeed(fsperm)) for fsperm in fsperms] for perm in perms: if not perm.can(): - if _security._unauthorized_callback: - # Backwards compat - deprecated - return _security._unauthorized_callback() return _security._unauthz_handler( permissions_required.__name__, list(fsperms) ) @@ -597,9 +573,6 @@ def decorated_view(*args, **kwargs): perm = Permission(*(FsPermNeed(fsperm) for fsperm in fsperms)) if perm.can(): return fn(*args, **kwargs) - if _security._unauthorized_callback: - # Backwards compat - deprecated - return _security._unauthorized_callback() return _security._unauthz_handler( permissions_accepted.__name__, list(fsperms) ) diff --git a/tests/test_common.py b/tests/test_common.py index c971c6c1..d4716a42 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -612,16 +612,14 @@ def test_http_auth_no_authorization_json(client, get_message): assert response.headers["Content-Type"] == "application/json" -@pytest.mark.settings(backwards_compat_unauthn=True) def test_http_auth_no_authentication(client, get_message): response = client.get("/http", headers={}) assert response.status_code == 401 - assert b"

Unauthorized

" in response.data + assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] -@pytest.mark.settings(backwards_compat_unauthn=False) def test_http_auth_no_authentication_json(client, get_message): response = client.get("/http", headers={"accept": "application/json"}) assert response.status_code == 401 @@ -631,8 +629,7 @@ def test_http_auth_no_authentication_json(client, get_message): assert response.headers["Content-Type"] == "application/json" -@pytest.mark.settings(backwards_compat_unauthn=True) -def test_invalid_http_auth_invalid_username(client): +def test_invalid_http_auth_invalid_username(client, get_message): response = client.get( "/http", headers={ @@ -640,12 +637,11 @@ def test_invalid_http_auth_invalid_username(client): % base64.b64encode(b"bogus:bogus").decode("utf-8") }, ) - assert b"

Unauthorized

" in response.data + assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] -@pytest.mark.settings(backwards_compat_unauthn=False) def test_invalid_http_auth_invalid_username_json(client, get_message): # Even with JSON - Basic Auth required a WWW-Authenticate header response. response = client.get( @@ -664,8 +660,7 @@ def test_invalid_http_auth_invalid_username_json(client, get_message): assert "WWW-Authenticate" in response.headers -@pytest.mark.settings(backwards_compat_unauthn=True) -def test_invalid_http_auth_bad_password(client): +def test_invalid_http_auth_bad_password(client, get_message): response = client.get( "/http", headers={ @@ -673,13 +668,12 @@ def test_invalid_http_auth_bad_password(client): % base64.b64encode(b"joe@lp.com:bogus").decode("utf-8") }, ) - assert b"

Unauthorized

" in response.data + assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] -@pytest.mark.settings(backwards_compat_unauthn=True) -def test_custom_http_auth_realm(client): +def test_custom_http_auth_realm(client, get_message): response = client.get( "/http_custom_realm", headers={ @@ -687,7 +681,7 @@ def test_custom_http_auth_realm(client): % base64.b64encode(b"joe@lp.com:bogus").decode("utf-8") }, ) - assert b"

Unauthorized

" in response.data + assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="My Realm"' == response.headers["WWW-Authenticate"] @@ -709,8 +703,7 @@ def test_multi_auth_basic(client): assert "WWW-Authenticate" in response.headers -@pytest.mark.settings(backwards_compat_unauthn=True) -def test_multi_auth_basic_invalid(client): +def test_multi_auth_basic_invalid(client, get_message): response = client.get( "/multi_auth", headers={ @@ -718,7 +711,7 @@ def test_multi_auth_basic_invalid(client): % base64.b64encode(b"bogus:bogus").decode("utf-8") }, ) - assert b"

Unauthorized

" in response.data + assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] diff --git a/tests/test_misc.py b/tests/test_misc.py index ff6603b3..24ad801c 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -353,25 +353,6 @@ def test_password_unicode_password_salt(client): assert b"Welcome matt@lp.com" in response.data -@pytest.mark.filterwarnings( - "ignore:.*'unauthorized_handler' has been replaced.*:DeprecationWarning" -) -def test_set_unauthorized_handler(app, client): - @app.security.unauthorized_handler - def unauthorized(): - app.unauthorized_handler_set = True - return "unauthorized-handler-set", 401 - - app.unauthorized_handler_set = False - - authenticate(client, "joe@lp.com") - response = client.get("/admin", follow_redirects=True) - - assert app.unauthorized_handler_set is True - assert b"unauthorized-handler-set" in response.data - assert response.status_code == 401 - - @pytest.mark.registerable() def test_custom_forms_via_config(app, sqlalchemy_datastore): class MyLoginForm(LoginForm):