diff --git a/src/asfquart/auth.py b/src/asfquart/auth.py index 7380cfb..fd7423b 100644 --- a/src/asfquart/auth.py +++ b/src/asfquart/auth.py @@ -76,7 +76,7 @@ def requirements_to_iter(args: typing.Any): args = [args] # Test that each requirement is an allowed one (belongs to the Requirements class) for req in args: - if not callable(req) or req != getattr(Requirements, req.__name__, None): + if not callable(req) or not issubclass(req.__self__, Requirements): raise TypeError( f"Authentication requirement {req} is not valid. Must belong to the asfquart.auth.Requirements class." ) diff --git a/tests/auth.py b/tests/auth.py index 341eb4d..9d6e9c3 100644 --- a/tests/auth.py +++ b/tests/auth.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import time +import re import pytest import quart @@ -8,6 +9,39 @@ import asfquart.auth from asfquart.auth import Requirements as R +class MyR(R): + """Test auth methods in a Requirements subclass""" + + E_ALWAYS_FALSE = "Always False" + + @classmethod + def true(cls, _session): + return True, "" + + @classmethod + def false(cls, _session): + return False, cls.E_ALWAYS_FALSE + +class LoneR(): + """Test auth methods in an independent class""" + + E_ALWAYS_FALSE = "Always False" + + @classmethod + def true(cls, _session): + return True, "" + + @classmethod + def false(cls, _session): + return False, cls.E_ALWAYS_FALSE + +def _string_to_re(s): + """convert arbitrary string to fullmatch regex""" + return re.escape(s) + '$' + +def _string_to_re(s): + """convert arbitrary string to fullmatch regex""" + return re.escape(s) + '$' @pytest.mark.auth async def test_auth_basics(): @@ -20,10 +54,8 @@ async def requires_session(): # Test with no session, should fail quart.session = {} - try: + with pytest.raises(asfquart.auth.AuthenticationFailed, match=_string_to_re(R.E_NOT_LOGGED_IN)): await requires_session() - except asfquart.auth.AuthenticationFailed as e: - assert e.message is R.E_NOT_LOGGED_IN # Test with session, should work. quart.session = {app.app_id: {"uts": time.time(), "foo": "bar"}} @@ -53,17 +85,13 @@ async def requires_mfa(): # Test MFA with no session, should fail exactly like auth_required quart.session = {} - try: + with pytest.raises(asfquart.auth.AuthenticationFailed, match=_string_to_re(R.E_NOT_LOGGED_IN)): await requires_mfa() - except asfquart.auth.AuthenticationFailed as e: - assert e.message is R.E_NOT_LOGGED_IN # Test with session without MFA, should fail. quart.session = {app.app_id: {"uts": time.time(), "foo": "bar"}} - try: + with pytest.raises(asfquart.auth.AuthenticationFailed, match=_string_to_re(R.E_NO_MFA)): await requires_mfa() - except asfquart.auth.AuthenticationFailed as e: - assert e.message is R.E_NO_MFA # Test with session with MFA, should work. quart.session = {app.app_id: {"uts": time.time(), "foo": "bar", "mfa": True}} @@ -95,27 +123,21 @@ async def test_member_or_chair_auth(): # Test role with no session, should fail exactly like auth_required quart.session = {} - try: + with pytest.raises(asfquart.auth.AuthenticationFailed, match=_string_to_re(R.E_NOT_LOGGED_IN)): await test_committer_auth() - except asfquart.auth.AuthenticationFailed as e: - assert e.message is R.E_NOT_LOGGED_IN # Test with session , should work quart.session = {app.app_id: {"uts": time.time(), "foo": "bar"}} await test_committer_auth() # Test with a role we don't have, should fail - try: + with pytest.raises(asfquart.auth.AuthenticationFailed, match=_string_to_re(R.E_NOT_MEMBER)): await test_member_auth() - except asfquart.auth.AuthenticationFailed as e: - assert e.message is R.E_NOT_MEMBER # Test with for both member and chair, while only being member. should pass on member check, fail on chair quart.session = {app.app_id: {"uts": time.time(), "foo": "bar", "isMember": True}} - try: + with pytest.raises(asfquart.auth.AuthenticationFailed, match=_string_to_re(R.E_NOT_CHAIR)): await test_member_and_chair_auth() - except asfquart.auth.AuthenticationFailed as e: - assert e.message is R.E_NOT_CHAIR # Test for either member of chair, should work as we have chair (but not member) quart.session = {app.app_id: {"uts": time.time(), "foo": "bar", "isChair": True}} @@ -124,3 +146,36 @@ async def test_member_or_chair_auth(): # Test for both member and chair, when we are both. should work. quart.session = {app.app_id: {"uts": time.time(), "foo": "bar", "isMember": True, "isChair": True}} await test_member_and_chair_auth() + +@pytest.mark.auth +async def test_extended_auth(): + """Extended auth tests""" + + @asfquart.auth.require(MyR.true) + async def test_true(): + pass + + @asfquart.auth.require(MyR.false) + async def test_false(): + pass + + # Should always work + await test_true() + + with pytest.raises(asfquart.auth.AuthenticationFailed, match=_string_to_re(MyR.E_ALWAYS_FALSE)): + await test_false() + +@pytest.mark.auth +async def test_lone_auth(): + """Extended auth tests using independent class""" + + # cannot use independent class as a decorator + with pytest.raises(TypeError): + @asfquart.auth.require(LoneR.true) + async def test_true(): + pass + + with pytest.raises(TypeError): + @asfquart.auth.require(LoneR.false) + async def test_false(): + pass