Skip to content

Commit

Permalink
Remove long-deprecated _unauthorized_callback method. (#878)
Browse files Browse the repository at this point in the history
Also - add deprecation notice for BACKWARDS_COMPAT_UNAUTHN configuration.
  • Loading branch information
jwag956 authored Nov 11, 2023
1 parent 6d353db commit 2a234e2
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 81 deletions.
4 changes: 3 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
++++++
Expand All @@ -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
Expand Down
23 changes: 11 additions & 12 deletions flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -1463,17 +1462,26 @@ def init_app(
if cv("AUTO_LOGIN_AFTER_RESET", app=app):
warnings.warn(
"The auto-login after successful password reset functionality"
"has been deprecated and will be removed in a future release.",
" has been deprecated and will be removed in a future release.",
DeprecationWarning,
stacklevel=2,
)
if cv("AUTO_LOGIN_AFTER_CONFIRM", app=app):
warnings.warn(
"The auto-login after successful confirmation functionality"
"has been deprecated and will be removed in a future release.",
" has been deprecated and will be removed in a future release.",
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"
Expand Down Expand Up @@ -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:
Expand Down
39 changes: 6 additions & 33 deletions flask_security/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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)
)
Expand Down
32 changes: 16 additions & 16 deletions tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<h1>Unauthorized</h1>" 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
Expand All @@ -631,21 +629,19 @@ 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={
"Authorization": "Basic %s"
% base64.b64encode(b"bogus:bogus").decode("utf-8")
},
)
assert b"<h1>Unauthorized</h1>" 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(
Expand All @@ -664,30 +660,28 @@ 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={
"Authorization": "Basic %s"
% base64.b64encode(b"[email protected]:bogus").decode("utf-8")
},
)
assert b"<h1>Unauthorized</h1>" 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={
"Authorization": "Basic %s"
% base64.b64encode(b"[email protected]:bogus").decode("utf-8")
},
)
assert b"<h1>Unauthorized</h1>" 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"]

Expand All @@ -709,16 +703,15 @@ 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={
"Authorization": "Basic %s"
% base64.b64encode(b"bogus:bogus").decode("utf-8")
},
)
assert b"<h1>Unauthorized</h1>" 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"]

Expand Down Expand Up @@ -1079,3 +1072,10 @@ def test_auth_token_decorator(in_app_context):
headers={"Content-Type": "application/json", "Authentication-Token": token},
)
assert response.status_code == 200


@pytest.mark.filterwarnings("ignore:.*BACKWARDS_COMPAT_UNAUTHN:DeprecationWarning")
@pytest.mark.settings(backwards_compat_unauthn=True)
def test_unauthn_compat(client):
response = client.get("/profile")
assert response.status_code == 401
19 changes: 0 additions & 19 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,25 +353,6 @@ def test_password_unicode_password_salt(client):
assert b"Welcome [email protected]" 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, "[email protected]")
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):
Expand Down

0 comments on commit 2a234e2

Please sign in to comment.