Skip to content

Commit

Permalink
add NodeRequest improvements for Institutional Access project
Browse files Browse the repository at this point in the history
  • Loading branch information
John Tordoff committed Dec 6, 2024
1 parent 869c146 commit af18d95
Show file tree
Hide file tree
Showing 13 changed files with 601 additions and 216 deletions.
3 changes: 2 additions & 1 deletion api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
RegistrationSerializer,
RegistrationCreateSerializer,
)
from api.requests.permissions import NodeRequestPermission
from api.requests.permissions import NodeRequestPermission, InstitutionalAdminRequestTypePermission
from api.requests.serializers import NodeRequestSerializer, NodeRequestCreateSerializer
from api.requests.views import NodeRequestMixin
from api.resources import annotations as resource_annotations
Expand Down Expand Up @@ -2239,6 +2239,7 @@ class NodeRequestListCreate(JSONAPIBaseView, generics.ListCreateAPIView, ListFil
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
NodeRequestPermission,
InstitutionalAdminRequestTypePermission,
)

required_read_scopes = [CoreScopes.NODE_REQUESTS_READ]
Expand Down
38 changes: 34 additions & 4 deletions api/requests/permissions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from rest_framework import permissions as drf_permissions

from api.base.utils import get_user_auth
from osf.models.action import NodeRequestAction, PreprintRequestAction
from osf.models import (
Node,
NodeRequestAction,
PreprintRequestAction,
Preprint,
Institution,
)
from osf.models.mixins import NodeRequestableMixin, PreprintRequestableMixin
from osf.models.node import Node
from osf.models.preprint import Preprint
from osf.utils.workflows import DefaultTriggers
from osf.utils.workflows import DefaultTriggers, NodeRequestTypes
from osf.utils import permissions as osf_permissions


Expand Down Expand Up @@ -52,8 +56,34 @@ def has_object_permission(self, request, view, obj):
# Requesters may not be contributors
# Requesters may edit their comment or submit their request
return is_requester and auth.user not in node.contributors


class InstitutionalAdminRequestTypePermission(drf_permissions.BasePermission):
"""
Permission class for handling object permissions related to Node requests and actions.
"""

def has_object_permission(self, request, view, obj):
# Skip if not institutional_request request_type
request_type = request.data.get('request_type')
if request_type != NodeRequestTypes.INSTITUTIONAL_REQUEST.value:
return True

auth = get_user_auth(request)
if not auth.user:
return False

institution_id = request.data.get('institution')
if not institution_id:
return False

try:
institution = Institution.objects.get(_id=institution_id)
except Institution.DoesNotExist:
return False

return auth.user.is_institutional_admin(institution)


class PreprintRequestPermission(drf_permissions.BasePermission):
def has_object_permission(self, request, view, obj):
Expand Down
115 changes: 99 additions & 16 deletions api/requests/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,22 @@

from api.base.exceptions import Conflict
from api.base.utils import absolute_reverse, get_user_auth
from api.base.serializers import JSONAPISerializer, LinksField, VersionedDateTimeField, RelationshipField
from osf.models import NodeRequest, PreprintRequest
from osf.utils.workflows import DefaultStates, RequestTypes
from api.base.serializers import (
JSONAPISerializer,
LinksField,
VersionedDateTimeField,
RelationshipField,
)
from osf.models import (
NodeRequest,
PreprintRequest,
Institution,
OSFUser,
)
from osf.utils.workflows import DefaultStates, RequestTypes, NodeRequestTypes
from osf.utils import permissions as osf_permissions
from website import settings
from website.mails import send_mail, NODE_REQUEST_INSTITUTIONAL_ACCESS_REQUEST


class RequestSerializer(JSONAPISerializer):
Expand Down Expand Up @@ -66,6 +78,19 @@ class Meta:
source='target__guids___id',
)

requested_permissions = ser.ChoiceField(
help_text='These are supposed to represent the default permission suggested when the Node admin sees users '
'listed in an `Request Access` list.',
choices=osf_permissions.API_CONTRIBUTOR_PERMISSIONS,
required=False,
)
message_recipient = RelationshipField(
help_text='An optional user who will recieve a message probably an email exaplining the nature of the request.',
required=False,
related_view='users:user-detail',
related_view_kwargs={'user_id': '<user._id>'},
)

def get_target_url(self, obj):
return absolute_reverse('nodes:node-detail', kwargs={'node_id': obj.target._id, 'version': self.context['request'].parser_context['kwargs']['version']})

Expand All @@ -89,42 +114,100 @@ def get_target_url(self, obj):
},
)

class NodeRequestCreateSerializer(NodeRequestSerializer):
request_type = ser.ChoiceField(required=True, choices=RequestTypes.choices())
from typing import Any, Dict, Optional
from django.db import IntegrityError
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.serializers import Serializer
from osf.models import Node, NodeRequest, Institution, OSFUser
from osf.utils.workflows import DefaultStates, NodeRequestTypes
from website import settings
from website.mails import send_mail, NODE_REQUEST_INSTITUTIONAL_ACCESS_REQUEST
from api.base.exceptions import Conflict
from api.base.utils import get_user_auth

def create(self, validated_data):

class NodeRequestCreateSerializer(Serializer):
def create(self, validated_data: Dict[str, Any]) -> NodeRequest:
auth = get_user_auth(self.context['request'])
if not auth.user:
raise exceptions.PermissionDenied
raise PermissionDenied("Authentication required.")

try:
node = self.context['view'].get_target()
except exceptions.PermissionDenied:
except PermissionDenied:
node = self.context['view'].get_target(check_object_permissions=False)
if auth.user in node.contributors:
raise exceptions.PermissionDenied('You cannot request access to a node you contribute to.')
raise PermissionDenied("You cannot request access to a node you contribute to.")
raise

comment = validated_data.pop('comment', '')
request_type = validated_data.pop('request_type', None)
request_type = validated_data.get('request_type')
if not request_type:
raise ValidationError("You must specify a valid request_type.")

match request_type:
case NodeRequestTypes.ACCESS.value:
return self.make_node_access_request(node, validated_data)
case NodeRequestTypes.INSTITUTIONAL_REQUEST.value:
return self.make_node_institutional_access_request(node, validated_data)
case _:
raise NotImplementedError(f"Request type '{request_type}' not implemented.")

def make_node_access_request(self, node: Node, validated_data: Dict[str, Any]) -> NodeRequest:
return self._create_node_request(node, validated_data)

def make_node_institutional_access_request(self, node: Node, validated_data: Dict[str, Any]) -> NodeRequest:
node_request = self._create_node_request(node, validated_data)
sender = self.context['request'].user
message_recipient_id = validated_data.get('message_recipient')
institution_id = validated_data.get('institution')

message_recipient: Optional[OSFUser] = OSFUser.load(message_recipient_id) if message_recipient_id else None
institution: Institution = Institution.objects.get(_id=institution_id)

if message_recipient:
if not message_recipient.is_affiliated_with_institution(institution):
raise PermissionDenied(f"User {message_recipient._id} is not affiliated with the institution.")

send_mail(
to_addr=message_recipient.username,
mail=NODE_REQUEST_INSTITUTIONAL_ACCESS_REQUEST,
user=message_recipient,
sender=sender,
recipient=message_recipient,
comment=validated_data['comment'],
institution=institution,
osf_url=settings.DOMAIN,
node=node_request.target,
)

if request_type != RequestTypes.ACCESS.value:
raise exceptions.ValidationError('You must specify a valid request_type.')
return node_request

def _create_node_request(self, node: Node, validated_data: Dict[str, Any]) -> NodeRequest:
creator = self.context['request'].user
request_type = validated_data['request_type']
comment = validated_data.get('comment', '')
requested_permissions = validated_data.get('requested_permissions')
message_recipient_id = validated_data.get('message_recipient')
message_recipient = OSFUser.load(message_recipient_id) if message_recipient_id else None

try:
node_request = NodeRequest.objects.create(
target=node,
creator=auth.user,
creator=creator,
comment=comment,
machine_state=DefaultStates.INITIAL.value,
request_type=request_type,
requested_permissions=requested_permissions,
message_recipient=message_recipient,
)
node_request.save()
except IntegrityError:
raise Conflict(f'Users may not have more than one {request_type} request per node.')
node_request.run_submit(auth.user)
raise Conflict(f"Users may not have more than one '{request_type}' request per node.")

node_request.run_submit(creator)
return node_request


class PreprintRequestSerializer(RequestSerializer):
class Meta:
type_ = 'preprint-requests'
Expand Down
6 changes: 3 additions & 3 deletions api_tests/registries_moderation/test_submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from api.base.settings.defaults import API_BASE

from api.providers.workflows import Workflows
from osf.utils.workflows import RequestTypes, RegistrationModerationTriggers, RegistrationModerationStates
from osf.utils.workflows import NodeRequestTypes, RegistrationModerationTriggers, RegistrationModerationStates


from osf_tests.factories import (
Expand Down Expand Up @@ -66,7 +66,7 @@ def registration_with_withdraw_request(self, provider):
registration = RegistrationFactory(provider=provider)

NodeRequest.objects.create(
request_type=RequestTypes.WITHDRAWAL.value,
request_type=NodeRequestTypes.WITHDRAWAL.value,
target=registration,
creator=registration.creator
)
Expand All @@ -75,7 +75,7 @@ def registration_with_withdraw_request(self, provider):

@pytest.fixture()
def access_request(self, provider):
request = NodeRequestFactory(request_type=RequestTypes.ACCESS.value)
request = NodeRequestFactory(request_type=NodeRequestTypes.ACCESS.value)
request.target.provider = provider
request.target.save()

Expand Down
Loading

0 comments on commit af18d95

Please sign in to comment.