-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ENG-5183] Implement Auth strategy w/ OSF (#10)
Identify the Session user and confirm access to Resources with help from OSF --------- Co-authored-by: John Tordoff <> Co-authored-by: Jon Walz <jon@cos.io>
- Loading branch information
1 parent
e3eddfe
commit 794c660
Showing
21 changed files
with
544 additions
and
240 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,24 @@ | ||
from rest_framework_json_api.views import ModelViewSet | ||
from addon_service.common.permissions import ( | ||
CanCreateASA, | ||
SessionUserIsOwner, | ||
) | ||
from addon_service.common.viewsets import RetrieveWriteViewSet | ||
|
||
from .models import AuthorizedStorageAccount | ||
from .serializers import AuthorizedStorageAccountSerializer | ||
|
||
|
||
class AuthorizedStorageAccountViewSet(ModelViewSet): | ||
class AuthorizedStorageAccountViewSet(RetrieveWriteViewSet): | ||
queryset = AuthorizedStorageAccount.objects.all() | ||
serializer_class = AuthorizedStorageAccountSerializer | ||
|
||
def get_permissions(self): | ||
if not self.action: | ||
return super().get_permissions() | ||
|
||
if self.action in ["retrieve", "retrieve_related", "update", "destroy"]: | ||
return [SessionUserIsOwner()] | ||
elif self.action == "create": | ||
return [CanCreateASA()] | ||
else: | ||
raise NotImplementedError("view action permission not implemented") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from rest_framework import ( | ||
exceptions, | ||
permissions, | ||
) | ||
|
||
from addon_service.models import UserReference | ||
from app.authentication import authenticate_resource | ||
|
||
|
||
class SessionUserIsOwner(permissions.BasePermission): | ||
""" | ||
Decorator to fetch 'user_reference_uri' from the session and pass it to the permission check function. | ||
""" | ||
|
||
def has_object_permission(self, request, view, obj): | ||
session_user_uri = request.session.get("user_reference_uri") | ||
if session_user_uri: | ||
return session_user_uri == obj.owner_reference | ||
return False | ||
|
||
|
||
class SessionUserIsResourceReferenceOwner(permissions.BasePermission): | ||
def has_object_permission(self, request, view, obj): | ||
resource_uri = authenticate_resource(request, obj.resource_uri, "read") | ||
return obj.resource_uri == resource_uri | ||
|
||
|
||
class CanCreateCSA(permissions.BasePermission): | ||
def has_permission(self, request, view): | ||
authorized_resource_id = request.data.get("authorized_resource", {}).get("id") | ||
if authenticate_resource(request, authorized_resource_id, "admin"): | ||
return True | ||
return False | ||
|
||
|
||
class CanCreateASA(permissions.BasePermission): | ||
def has_permission(self, request, view): | ||
session_user_uri = request.session.get("user_reference_uri") | ||
request_user_uri = request.data.get("account_owner", {}).get("id") | ||
if not session_user_uri == request_user_uri: | ||
raise exceptions.NotAuthenticated( | ||
"Account owner ID is missing in the request." | ||
) | ||
try: | ||
UserReference.objects.get(user_uri=request_user_uri) | ||
return True | ||
except UserReference.DoesNotExist: | ||
raise exceptions.NotAuthenticated("User does not exist.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
from rest_framework import mixins as drf_mixins | ||
from rest_framework.viewsets import GenericViewSet | ||
from rest_framework_json_api.views import ( | ||
AutoPrefetchMixin, | ||
PreloadIncludesMixin, | ||
RelatedMixin, | ||
) | ||
|
||
|
||
class _DrfJsonApiHelpers(AutoPrefetchMixin, PreloadIncludesMixin, RelatedMixin): | ||
pass | ||
|
||
|
||
class RetrieveOnlyViewSet( | ||
_DrfJsonApiHelpers, drf_mixins.RetrieveModelMixin, GenericViewSet | ||
): | ||
http_method_names = ["get", "head", "options"] | ||
|
||
|
||
class RetrieveWriteViewSet( | ||
_DrfJsonApiHelpers, | ||
drf_mixins.CreateModelMixin, | ||
drf_mixins.RetrieveModelMixin, | ||
drf_mixins.UpdateModelMixin, | ||
drf_mixins.DestroyModelMixin, | ||
GenericViewSet, | ||
): | ||
http_method_names = ["get", "post", "patch", "delete", "head", "options"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,24 @@ | ||
from rest_framework_json_api.views import ModelViewSet | ||
from addon_service.common.permissions import ( | ||
CanCreateCSA, | ||
SessionUserIsOwner, | ||
) | ||
from addon_service.common.viewsets import RetrieveWriteViewSet | ||
|
||
from .models import ConfiguredStorageAddon | ||
from .serializers import ConfiguredStorageAddonSerializer | ||
|
||
|
||
class ConfiguredStorageAddonViewSet(ModelViewSet): | ||
class ConfiguredStorageAddonViewSet(RetrieveWriteViewSet): | ||
queryset = ConfiguredStorageAddon.objects.all() | ||
serializer_class = ConfiguredStorageAddonSerializer | ||
|
||
def get_permissions(self): | ||
if not self.action: | ||
return super().get_permissions() | ||
|
||
if self.action in ["retrieve", "retrieve_related", "update", "destroy"]: | ||
return [SessionUserIsOwner()] | ||
elif self.action == "create": | ||
return [CanCreateCSA()] | ||
else: | ||
raise NotImplementedError("view action permission not implemented") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,9 @@ | ||
from rest_framework_json_api.views import ModelViewSet | ||
from rest_framework_json_api.views import ReadOnlyModelViewSet | ||
|
||
from .models import ExternalStorageService | ||
from .serializers import ExternalStorageServiceSerializer | ||
|
||
|
||
class ExternalStorageServiceViewSet(ModelViewSet): | ||
class ExternalStorageServiceViewSet(ReadOnlyModelViewSet): | ||
queryset = ExternalStorageService.objects.all() | ||
serializer_class = ExternalStorageServiceSerializer | ||
# TODO: permissions_classes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,11 @@ | ||
from rest_framework_json_api.views import ReadOnlyModelViewSet | ||
from addon_service.common.permissions import SessionUserIsResourceReferenceOwner | ||
from addon_service.common.viewsets import RetrieveOnlyViewSet | ||
from addon_service.serializers import ResourceReferenceSerializer | ||
|
||
from .models import ResourceReference | ||
from .serializers import ResourceReferenceSerializer | ||
|
||
|
||
class ResourceReferenceViewSet(ReadOnlyModelViewSet): | ||
class ResourceReferenceViewSet(RetrieveOnlyViewSet): | ||
queryset = ResourceReference.objects.all() | ||
serializer_class = ResourceReferenceSerializer | ||
# TODO: permissions_classes | ||
permission_classes = [SessionUserIsResourceReferenceOwner] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,57 @@ | ||
from rest_framework.test import ( | ||
APIRequestFactory, | ||
force_authenticate, | ||
) | ||
from functools import wraps | ||
from unittest.mock import patch | ||
|
||
import httpx | ||
from django.contrib.sessions.backends.db import SessionStore | ||
from rest_framework.test import APIRequestFactory | ||
|
||
def get_test_request(user=None, method="get", path=""): | ||
from app import settings | ||
|
||
|
||
def get_test_request(user=None, method="get", path="", cookies=None): | ||
_factory_method = getattr(APIRequestFactory(), method) | ||
_request = _factory_method(path) # note that path is optional for view tests | ||
if user is not None: | ||
force_authenticate(_request, user=user) | ||
_request.session = SessionStore() # Add cookies if provided | ||
if cookies: | ||
for name, value in cookies.items(): | ||
_request.COOKIES[name] = value | ||
return _request | ||
|
||
|
||
def with_mocked_httpx_get(response_status=200): | ||
"""Decorator to mock httpx.Client get requests with a customizable response status.""" | ||
|
||
def decorator(func): | ||
@wraps(func) | ||
def wrapper(self, *args, **kwargs): | ||
with patch( | ||
"httpx.Client.get", | ||
new=lambda *args, **kwargs: mock_httpx_response( | ||
*args, response_status=response_status | ||
), | ||
): | ||
return func(self, *args, **kwargs) | ||
|
||
return wrapper | ||
|
||
return decorator | ||
|
||
|
||
def mock_httpx_response(url, current_user, response_status, *args, **kwargs): | ||
"""Generates mock httpx.Response based on the requested URL and response status.""" | ||
if response_status == 200: | ||
if url == settings.USER_REFERENCE_LOOKUP_URL: | ||
payload = {"data": {"links": {"iri": current_user.user_uri}}} | ||
else: | ||
guid = url.rstrip("/").split("/")[-1] | ||
payload = { | ||
"data": { | ||
"attributes": { | ||
"current_user_permissions": ["read", "write", "admin"] | ||
}, | ||
"links": {"iri": f"{settings.URI_ID}{guid}"}, | ||
} | ||
} | ||
return httpx.Response(status_code=200, json=payload) | ||
else: # Handles 403 and other statuses explicitly | ||
return httpx.Response(status_code=response_status) |
Oops, something went wrong.