diff --git a/api/nodes/permissions.py b/api/nodes/permissions.py index c11932f1aee..c7ba374a984 100644 --- a/api/nodes/permissions.py +++ b/api/nodes/permissions.py @@ -353,3 +353,33 @@ def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return obj.is_public or obj.can_view(auth) return obj.has_permission(auth.user, osf_permissions.WRITE) + + +class AdminOrPublicOrSuperUser(permissions.BasePermission): + """ + Permission class that grants access based on user's role and object's visibility. + + This class implements a permission system that allows access if: + 1. The user is a superuser making a POST request + 2. The request is using safe methods (GET, HEAD, OPTIONS) and the object is either public or viewable by the user + 3. The user has admin permissions on the object + """ + acceptable_models = (AbstractNode, OSFUser, Institution, BaseAddonSettings, DraftRegistration,) + + def has_object_permission(self, request, view, obj): + """ + Determines if the user has permission to access the specified object. + Returns: + bool: True if the user has permission to access the object, False otherwise. + """ + if isinstance(obj, dict) and 'self' in obj: + obj = obj['self'] + + assert_resource_type(obj, self.acceptable_models) + auth = get_user_auth(request) + + if request.method == 'POST' and auth.user.is_superuser: + return True + if request.method in permissions.SAFE_METHODS: + return obj.is_public or obj.can_view(auth) + return obj.has_permission(auth.user, osf_permissions.ADMIN) diff --git a/api/nodes/serializers.py b/api/nodes/serializers.py index 95036f2198d..dcf73d2c5fc 100644 --- a/api/nodes/serializers.py +++ b/api/nodes/serializers.py @@ -1287,7 +1287,8 @@ def create(self, validated_data): 'auth': auth, 'user_id': id, 'email': email, 'full_name': full_name, 'send_email': send_email, 'bibliographic': bibliographic, 'index': index, 'save': True, } - + if auth.user.is_superuser: + contributor_dict['is_admin'] = True contributor_dict['permissions'] = permissions contributor_obj = node.add_contributor_registered_or_not(**contributor_dict) except ValidationError as e: diff --git a/api/nodes/views.py b/api/nodes/views.py index 794bf85dcbf..72882d252db 100644 --- a/api/nodes/views.py +++ b/api/nodes/views.py @@ -85,6 +85,7 @@ NodeLinksShowIfVersion, ReadOnlyIfWithdrawn, IsWritableContributorToRegisterDrafts, + AdminOrPublicOrSuperUser, ) from api.nodes.serializers import ( NodeSerializer, @@ -472,7 +473,7 @@ class NodeContributorsList(BaseContributorList, bulk_views.BulkUpdateJSONAPIView """The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/nodes_contributors_list). """ permission_classes = ( - AdminOrPublic, + AdminOrPublicOrSuperUser, drf_permissions.IsAuthenticatedOrReadOnly, ReadOnlyIfRegistration, base_permissions.TokenHasScope, diff --git a/api_tests/nodes/views/test_node_contributors_list.py b/api_tests/nodes/views/test_node_contributors_list.py index 5bd6b76c575..239c97c0ad9 100644 --- a/api_tests/nodes/views/test_node_contributors_list.py +++ b/api_tests/nodes/views/test_node_contributors_list.py @@ -1288,6 +1288,49 @@ def test_add_unconfirmed_user_by_guid( assert res.json['data']['attributes']['unregistered_contributor'] == 'Susan B. Anthony' +@pytest.mark.django_db +@pytest.mark.enable_quickfiles_creation +@pytest.mark.enable_implicit_clean +class TestNodeContributorAddBySuperAdmin(NodeCRUDTestCase): + + @pytest.fixture() + def url_private(self, project_private): + return '/{}nodes/{}/contributors/?send_email=false'.format( + API_BASE, project_private._id) + + @pytest.fixture() + def data_user_two(self, user_two): + return { + 'data': { + 'type': 'contributors', + 'attributes': { + 'bibliographic': True, + }, + 'relationships': { + 'users': { + 'data': { + 'type': 'users', + 'id': user_two._id, + } + } + } + } + } + + def test_adds_contributor_by_super_admin( + self, app, user, user_two, project_private, + data_user_two, url_private): + user.is_superuser = True + user.save() + res = app.post_json_api(url_private, data_user_two, auth=user.auth) + assert res.status_code == 201 + assert res.json['data']['id'] == '{}-{}'.format( + project_private._id, user_two._id) + + project_private.reload() + assert user_two in project_private.contributors + + @pytest.mark.django_db @pytest.mark.enable_quickfiles_creation @pytest.mark.enable_implicit_clean diff --git a/osf/models/mixins.py b/osf/models/mixins.py index f945a1d7456..36e56444759 100644 --- a/osf/models/mixins.py +++ b/osf/models/mixins.py @@ -1341,7 +1341,7 @@ def _get_admin_contributors_query(self, users): return self.contributor_class.objects.select_related('user').filter(**query_dict) def add_contributor(self, contributor, permissions=None, visible=True, - send_email=None, auth=None, log=True, save=False): + send_email=None, auth=None, log=True, save=False, is_admin=False): """Add a contributor to the project. :param User contributor: The contributor to be added @@ -1386,7 +1386,16 @@ def add_contributor(self, contributor, permissions=None, visible=True, self.add_permission(contrib_to_add, permissions, save=True) contributor_obj.save() - if log: + if log and is_admin: + params = self.log_params + params['contributors'] = [contrib_to_add._id] + self.add_log( + action=self.log_class.ADMIN_CONTRIB_ADDED, + params=params, + auth=None, + save=False, + ) + elif log: params = self.log_params params['contributors'] = [contrib_to_add._id] self.add_log( @@ -1493,7 +1502,7 @@ def add_unregistered_contributor(self, fullname, email, auth, send_email=None, def add_contributor_registered_or_not(self, auth, user_id=None, full_name=None, email=None, send_email=None, - permissions=None, bibliographic=True, index=None, save=False): + permissions=None, bibliographic=True, index=None, save=False, is_admin=False): OSFUser = apps.get_model('osf.OSFUser') send_email = send_email or self.contributor_email_template @@ -1507,7 +1516,8 @@ def add_contributor_registered_or_not(self, auth, user_id=None, if contributor.is_registered: contributor = self.add_contributor(contributor=contributor, auth=auth, visible=bibliographic, - permissions=permissions, send_email=send_email, save=True) + permissions=permissions, send_email=send_email, save=True, + is_admin=is_admin) else: if not full_name: raise ValueError( @@ -1527,7 +1537,7 @@ def add_contributor_registered_or_not(self, auth, user_id=None, if contributor and contributor.is_registered: self.add_contributor(contributor=contributor, auth=auth, visible=bibliographic, - send_email=send_email, permissions=permissions, save=True) + send_email=send_email, permissions=permissions, save=True, is_admin=is_admin) else: contributor = self.add_unregistered_contributor( fullname=full_name, email=email, auth=auth, diff --git a/osf/models/nodelog.py b/osf/models/nodelog.py index 1a993590479..8780ae73ad6 100644 --- a/osf/models/nodelog.py +++ b/osf/models/nodelog.py @@ -48,6 +48,7 @@ class NodeLog(ObjectIDMixin, BaseModel): MADE_WIKI_PRIVATE = 'made_wiki_private' CONTRIB_ADDED = 'contributor_added' + ADMIN_CONTRIB_ADDED = 'admin_contributor_added' CONTRIB_REMOVED = 'contributor_removed' CONTRIB_REJECTED = 'contributor_rejected' CONTRIB_REORDERED = 'contributors_reordered' diff --git a/website/static/js/anonymousLogActionsList.json b/website/static/js/anonymousLogActionsList.json index 623070a737c..6112b457a00 100644 --- a/website/static/js/anonymousLogActionsList.json +++ b/website/static/js/anonymousLogActionsList.json @@ -26,6 +26,7 @@ "custom_citation_edited" : "A user updated a custom citation for a project", "custom_citation_removed" : "A user removed a custom citation from a project", "contributor_added" : "A user added contributor(s) to a project", + "admin_contributor_added" : "A user added contributor(s) to a project", "contributor_removed" : "A user removed contributor(s) from a project", "contributor_rejected": "Contributor(s) cancelled invitation from a project", "contributors_reordered" : "A user reordered contributors for a project", diff --git a/website/static/js/logActionsList.json b/website/static/js/logActionsList.json index 961ed64adf9..d8a11334943 100644 --- a/website/static/js/logActionsList.json +++ b/website/static/js/logActionsList.json @@ -25,6 +25,7 @@ "external_ids_added": "${user} created external identifier(s) ${identifiers} on ${node}", "custom_citation_added" : "${user} created a custom citation for ${node}", "custom_citation_edited" : "${user} edited a custom citation for ${node}", + "admin_contributor_added": "The Integrated Admin added ${contributors} as contributor(s) to ${node}", "custom_citation_removed" : "${user} removed a custom citation from ${node}", "contributor_added": "${user} added ${contributors} as contributor(s) to ${node}", "contributor_removed": "${user} removed ${contributors} as contributor(s) from ${node}", diff --git a/website/translations/en/LC_MESSAGES/js_messages.po b/website/translations/en/LC_MESSAGES/js_messages.po index 34ae9dd4eae..b93363783fb 100644 --- a/website/translations/en/LC_MESSAGES/js_messages.po +++ b/website/translations/en/LC_MESSAGES/js_messages.po @@ -9259,3 +9259,6 @@ msgstr "" msgid "The new project cannot be created due to the created project number is greater than or equal the project number can create." msgstr "" + +msgid "The Integrated Admin added ${contributors} as contributor(s) to ${node}" +msgstr "" diff --git a/website/translations/ja/LC_MESSAGES/js_messages.po b/website/translations/ja/LC_MESSAGES/js_messages.po index 5eb1a096a21..e7bf8f18afc 100644 --- a/website/translations/ja/LC_MESSAGES/js_messages.po +++ b/website/translations/ja/LC_MESSAGES/js_messages.po @@ -10540,3 +10540,6 @@ msgstr "プロジェクト制限数が不正な値です。" msgid "The new project cannot be created due to the created project number is greater than or equal the project number can create." msgstr "作成したプロジェクト数が作成可能なプロジェクトの数の上限に達しているため、新規プロジェクトを作成できません。" + +msgid "The Integrated Admin added ${contributors} as contributor(s) to ${node}" +msgstr "統合管理者代理アカウントが${contributors}をコントリビューターとして${node}に追加しました" diff --git a/website/translations/js_messages.pot b/website/translations/js_messages.pot index e992ef06157..437908aaa3d 100644 --- a/website/translations/js_messages.pot +++ b/website/translations/js_messages.pot @@ -9214,3 +9214,6 @@ msgstr "" msgid "The new project cannot be created due to the created project number is greater than or equal the project number can create." msgstr "" + +msgid "The Integrated Admin added ${contributors} as contributor(s) to ${node}" +msgstr ""