Skip to content

Commit b85faf0

Browse files
mark-adamsdbaxa
authored andcommitted
Sem-Ver: api-break Rework Django and Flask support
Previously, contributors introduced Django and Flask support via differing implementations into the contrib/ package. The implementations were similar (sometimes almost copied and pasted from one another) but also diverged in terms of their functionality and feature sets. This change's primary goal is to take the contrib/{django,flask} and refactor them into a shared implementation with framework-specific backends to abstract away the differences between the two. To demonstrate this formalization of support for these frameworks, they have also been moved from contrib into frameworks/{django,flask}. In addition the following changes were made in this commit: - Django and Flask decorators no longer require an issuer to be present in BOTH the global setting and the decorator argument to be valid. Decorator arguments now override the global setting. - The proxied middleware seems like a one-off special case and remains in contrib while a more general ASAP middleware which applies the decorator logic to all views is included in the supported package. - The @require_asap decorator has been renamed to @with_asap since you can technically pass the required=False argument to make ASAP not enforced. - The @validate_asap decorator has been renamed to a more descriptive @restrict_asap to symbolize that it applies additional restrictions onto an already processed ASAP authenticated request. - The 'subjects' arg to the @validate_asap decorator has been removed since it appears to be unused and is inconsistent with the existing @with_asap decorator. Sem-Ver: api-break frameworks: Rework how settings overrides work Instead of passing in an argument for each overrideable setting all the way down the functional call chain when using the decorators, this change duplicates the settings object and then overrides the applicable properties and passes that settings object in to the validation code. The following changes are also included in this commit: - SettingsDict attributes are now immutable - ASAP_VALID_ISSUERS is now converted into a set Fixed a typo in comment Remove an unused import Refactored some of the common logic into something simpler pep8 fix up. Signed-off-by: David Black <dblack@atlassian.com> _validate_claims does not need to perform the subject and issuer check as that has already been done by the verifier. Signed-off-by: David Black <dblack@atlassian.com> s/_validate_claims/_verify_issuers/ Signed-off-by: David Black <dblack@atlassian.com> delete the server helpers module as it is no longer used. Signed-off-by: David Black <dblack@atlassian.com> Slightly less churn in the the flask tests. Signed-off-by: David Black <dblack@atlassian.com> style fix up. Signed-off-by: David Black <dblack@atlassian.com> Sem-Ver: feature Restore the previous requires_asap decorators to make migration easier. Signed-off-by: David Black <dblack@atlassian.com> set required to True by default. Signed-off-by: David Black <dblack@atlassian.com> make func the last argument. Signed-off-by: David Black <dblack@atlassian.com> default ASAP_SUBJECT_SHOULD_MATCH_ISSUER to None. Signed-off-by: David Black <dblack@atlassian.com> also default to required=True in _update_settings_from_kwargs. Signed-off-by: David Black <dblack@atlassian.com> restore the restricted_subject_view test. Signed-off-by: David Black <dblack@atlassian.com> frameworks: Rework how settings overrides work Instead of passing in an argument for each overrideable setting all the way down the functional call chain when using the decorators, this change duplicates the settings object and then overrides the applicable properties and passes that settings object in to the validation code. The following changes are also included in this commit: - SettingsDict attributes are now immutable - ASAP_VALID_ISSUERS is now converted into a set Fixed a typo in comment Remove an unused import Refactored some of the common logic into something simpler Add usage of django.utils.deprecation.MiddlewareMixin to ProxiedAsapMiddleware Move NoTokenProvidedError to the top exceptions module. Signed-off-by: David Black <dblack@atlassian.com> (cherry picked from commit 36bab18) Move the contrib deprecation warnings to the respective deprecated __init__.py files. Signed-off-by: David Black <dblack@atlassian.com> Move NoTokenProvidedError to the top exceptions module. Signed-off-by: David Black <dblack@atlassian.com>
1 parent 93879d4 commit b85faf0

29 files changed

Lines changed: 559 additions & 304 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import warnings
2+
3+
4+
warnings.warn(
5+
"The atlassian_jwt_auth.contrib.django package is deprecated in 4.0.0 "
6+
"in favour of atlassian_jwt_auth.frameworks.django.",
7+
DeprecationWarning, stacklevel=2
8+
)
Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
from functools import wraps
22

3-
from django.conf import settings
43
from django.http.response import HttpResponse
54

6-
import atlassian_jwt_auth
7-
from .utils import parse_jwt, verify_issuers, _build_response
8-
from ..server.helpers import _requires_asap
5+
from atlassian_jwt_auth.frameworks.django.decorators import with_asap
96

107

118
def validate_asap(issuers=None, subjects=None, required=True):
@@ -46,44 +43,8 @@ def validate_asap_wrapper(request, *args, **kwargs):
4643
return validate_asap_decorator
4744

4845

49-
def requires_asap(issuers=None, subject_should_match_issuer=None):
50-
"""Decorator for Django endpoints to require ASAP
51-
52-
:param list issuers: *required The 'iss' claims that this endpoint is from.
53-
"""
54-
def requires_asap_decorator(func):
55-
@wraps(func)
56-
def requires_asap_wrapper(request, *args, **kwargs):
57-
verifier = _get_verifier(subject_should_match_issuer)
58-
auth_header = request.META.get('HTTP_AUTHORIZATION', b'')
59-
err_response = _requires_asap(
60-
verifier=verifier,
61-
auth=auth_header,
62-
parse_jwt_func=parse_jwt,
63-
build_response_func=_build_response,
64-
asap_claim_holder=request,
65-
verify_issuers_func=verify_issuers,
66-
issuers=issuers,
67-
)
68-
if err_response is None:
69-
return func(request, *args, **kwargs)
70-
return err_response
71-
72-
return requires_asap_wrapper
73-
return requires_asap_decorator
74-
75-
76-
def _get_verifier(subject_should_match_issuer=None):
77-
"""Return a verifier for ASAP JWT tokens based on settings"""
78-
retriever_cls = getattr(settings, 'ASAP_KEY_RETRIEVER_CLASS',
79-
atlassian_jwt_auth.HTTPSPublicKeyRetriever)
80-
retriever = retriever_cls(
81-
base_url=getattr(settings, 'ASAP_PUBLICKEY_REPOSITORY')
82-
)
83-
if subject_should_match_issuer is None:
84-
subject_should_match_issuer = getattr(
85-
settings, 'ASAP_SUBJECT_SHOULD_MATCH_ISSUER', None)
86-
v_kwargs = {}
87-
if subject_should_match_issuer is not None:
88-
v_kwargs['subject_should_match_issuer'] = subject_should_match_issuer
89-
return atlassian_jwt_auth.JWTAuthVerifier(retriever, **v_kwargs)
46+
def requires_asap(issuers=None, subject_should_match_issuer=None, func=None):
47+
return with_asap(func=func,
48+
required=True,
49+
issuers=issuers,
50+
subject_should_match_issuer=subject_should_match_issuer)
Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
from django.conf import settings
2+
from django.utils.deprecation import MiddlewareMixin
23

3-
import atlassian_jwt_auth
4-
from ..server.helpers import _requires_asap
5-
from .utils import parse_jwt, verify_issuers, _build_response
6-
from .decorators import _get_verifier
4+
from atlassian_jwt_auth.frameworks.django.middleware import (
5+
OldStyleASAPMiddleware
6+
)
77

88

9-
class ASAPForwardedMiddleware(object):
9+
class ProxiedAsapMiddleware(OldStyleASAPMiddleware, MiddlewareMixin):
1010
"""Enable client auth for ASAP-enabled services that are forwarding
1111
non-ASAP client requests.
1212
13-
This must come before any authentication middleware.
14-
15-
DEPRECATED: use ASAPMiddleware instead.
16-
"""
13+
This must come before any authentication middleware."""
1714

1815
def __init__(self, get_response=None):
16+
super(ProxiedAsapMiddleware, self).__init__()
1917
self.get_response = get_response
2018

2119
# Rely on this header to tell us if a request has been forwarded
@@ -27,13 +25,14 @@ def __init__(self, get_response=None):
2725
self.xauth = getattr(settings, 'ASAP_PROXIED_AUTHORIZATION_HEADER',
2826
'HTTP_X_ASAP_AUTHORIZATION')
2927

30-
def __call__(self, request):
31-
early_response = self.process_request(request)
32-
if early_response:
33-
return early_response
34-
return self.get_response(request)
35-
3628
def process_request(self, request):
29+
error_response = super(ProxiedAsapMiddleware, super).process_request(
30+
request
31+
)
32+
33+
if error_response:
34+
return error_response
35+
3736
forwarded_for = request.META.pop(self.xfwd, None)
3837
if forwarded_for is None:
3938
return
@@ -62,35 +61,3 @@ def process_view(self, request, view_func, view_args, view_kwargs):
6261
request.META['HTTP_AUTHORIZATION'] = asap_auth
6362
if orig_auth is not None:
6463
request.META[self.xauth] = orig_auth
65-
66-
67-
class ASAPMiddleware(ASAPForwardedMiddleware):
68-
"""Enable ASAP for Django applications.
69-
70-
To use proxied credentials, this must come before any authentication
71-
middleware.
72-
"""
73-
74-
def __init__(self, get_response=None):
75-
super(ASAPMiddleware, self).__init__(get_response=get_response)
76-
self.required = getattr(settings, 'ASAP_REQUIRED', True)
77-
self.client_auth = getattr(settings, 'ASAP_CLIENT_AUTH', False)
78-
79-
# Configure verifier based on settings
80-
self.verifier = _get_verifier()
81-
82-
def process_request(self, request):
83-
auth_header = request.META.get('HTTP_AUTHORIZATION', b'')
84-
asap_err = _requires_asap(
85-
verifier=self.verifier,
86-
auth=auth_header,
87-
parse_jwt_func=parse_jwt,
88-
build_response_func=_build_response,
89-
asap_claim_holder=request,
90-
verify_issuers_func=verify_issuers,
91-
)
92-
93-
if asap_err and self.required:
94-
return asap_err
95-
elif self.client_auth:
96-
super(ASAPMiddleware, self).process_request(request)

atlassian_jwt_auth/contrib/django/utils.py

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
1+
import warnings
12

2-
from atlassian_jwt_auth.contrib.flask_app.decorators import requires_asap
3+
from .decorators import requires_asap
4+
5+
6+
warnings.warn(
7+
"The atlassian_jwt_auth.contrib.flask_app package is deprecated in 4.0.0 "
8+
"in favour of atlassian_jwt_auth.frameworks.flask.",
9+
DeprecationWarning, stacklevel=2
10+
)
Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,8 @@
1-
from functools import wraps
1+
from atlassian_jwt_auth.frameworks.flask.decorators import with_asap
22

3-
from flask import Response, current_app, g, request
43

5-
import atlassian_jwt_auth
6-
from .utils import parse_jwt
7-
from ..server.helpers import _requires_asap
8-
9-
10-
def requires_asap(f):
11-
"""
12-
Wrapper for Flask endpoints to make them require asap authentication to
13-
access.
14-
"""
15-
16-
@wraps(f)
17-
def decorated(*args, **kwargs):
18-
verifier = _get_verifier()
19-
auth = request.headers.get('AUTHORIZATION', '')
20-
err_response = _requires_asap(
21-
verifier=verifier,
22-
auth=auth,
23-
parse_jwt_func=parse_jwt,
24-
build_response_func=_build_response,
25-
asap_claim_holder=g,
26-
)
27-
if err_response is None:
28-
return f(*args, **kwargs)
29-
return err_response
30-
31-
return decorated
32-
33-
34-
def _get_verifier():
35-
"""Returns a verifier for ASAP JWT tokens basd on application settings"""
36-
retriever_cls = current_app.config.get(
37-
'ASAP_KEY_RETRIEVER_CLASS', atlassian_jwt_auth.HTTPSPublicKeyRetriever
38-
)
39-
retriever = retriever_cls(
40-
base_url=current_app.config.get('ASAP_PUBLICKEY_REPOSITORY')
41-
)
42-
return atlassian_jwt_auth.JWTAuthVerifier(retriever)
43-
44-
45-
def _build_response(message, status, headers=None):
46-
return Response(message, status=status, headers=headers)
4+
def requires_asap(f, issuers=None, subject_should_match_issuer=None):
5+
return with_asap(func=f,
6+
required=True,
7+
issuers=issuers,
8+
subject_should_match_issuer=subject_should_match_issuer)

atlassian_jwt_auth/contrib/flask_app/utils.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

atlassian_jwt_auth/contrib/server/helpers.py

Lines changed: 0 additions & 64 deletions
This file was deleted.

atlassian_jwt_auth/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,8 @@ class PrivateKeyRetrieverException(_WithStatus, ASAPAuthenticationException):
5151

5252
class KeyIdentifierException(ASAPAuthenticationException):
5353
"""Raise when there are issues validating the key identifier"""
54+
55+
56+
class NoTokenProvidedError(ASAPAuthenticationException):
57+
"""Raise when no token is provided"""
58+
pass
File renamed without changes.

0 commit comments

Comments
 (0)