Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions api/nodes/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 2 additions & 1 deletion api/nodes/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
NodeLinksShowIfVersion,
ReadOnlyIfWithdrawn,
IsWritableContributorToRegisterDrafts,
AdminOrPublicOrSuperUser,
)
from api.nodes.serializers import (
NodeSerializer,
Expand Down Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions api_tests/nodes/views/test_node_contributors_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions osf/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions osf/models/nodelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions website/static/js/anonymousLogActionsList.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions website/static/js/logActionsList.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
3 changes: 3 additions & 0 deletions website/translations/en/LC_MESSAGES/js_messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
3 changes: 3 additions & 0 deletions website/translations/ja/LC_MESSAGES/js_messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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}に追加しました"
3 changes: 3 additions & 0 deletions website/translations/js_messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""