Skip to content

Commit 1c2c2d9

Browse files
adliusihorsokhanexoftbrianjgeigerantkrytbodintsov
authored
Feature/pbs 25 19 (#11347)
* [ENG-9020] Custom html host for legacy OSF (#11338) * custom html host for legacy OSF * fixed html links, added a test * anyone can duplicate public project (structure only) * remove capture_notifications as the result of rebasing from pbs-25-19 * allow READ users to create guid for files * validate only on permission change (#11336) * Fix fail from first attempt (#11342) * only moderator and admin can access moderation tabs (#11343) --------- Co-authored-by: ihorsokhanexoft <isokhan@exoft.net> Co-authored-by: Brian J. Geiger <bgeiger@pobox.com> Co-authored-by: Brian J. Geiger <bgeiger@cos.io> Co-authored-by: Anton Krytskyi <ant.krytskyi@gmail.com> Co-authored-by: bodintsov <bodintsov@exoft.net>
1 parent df68af5 commit 1c2c2d9

13 files changed

Lines changed: 236 additions & 31 deletions

File tree

api/base/serializers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,13 @@ def to_representation(self, obj):
11831183
if hasattr(obj, 'get_absolute_info_url'):
11841184
ret['info'] = self._extend_url_with_vol_key(obj.get_absolute_info_url())
11851185

1186+
request = self.context['request']
1187+
referer = request.headers.get('Referer', '')
1188+
if 'html' in ret and 'legacy' in referer:
1189+
parsed_html_url = urlparse(ret['html'])
1190+
legacy_url = urlparse(referer)
1191+
ret['html'] = parsed_html_url._replace(scheme=legacy_url.scheme, netloc=legacy_url.netloc).geturl()
1192+
11861193
return ret
11871194

11881195

api/files/views.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
FileDetailSerializer,
3131
FileVersionSerializer,
3232
)
33-
from osf.utils.permissions import ADMIN
3433

3534

3635
class FileMixin:
@@ -87,11 +86,11 @@ def get_target(self):
8786

8887
# overrides RetrieveAPIView
8988
def get_object(self):
90-
user = utils.get_user_auth(self.request).user
9189
file = self.get_file()
9290

9391
if self.request.GET.get('create_guid', False):
94-
if (self.get_target().has_permission(user, ADMIN) and utils.has_admin_scope(self.request)):
92+
auth = utils.get_user_auth(self.request)
93+
if self.get_target().can_view(auth):
9594
file.get_guid(create=True)
9695

9796
# We normally would pass this through `get_file` as an annotation, but the `select_for_update` feature prevents

api/nodes/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ def create(self, validated_data):
787787
template_node = Node.load(template_from)
788788
if template_node is None:
789789
raise exceptions.NotFound
790-
if not template_node.has_permission(user, osf_permissions.READ, check_parent=False):
790+
if not template_node.is_public and not template_node.is_contributor(user):
791791
raise exceptions.PermissionDenied
792792
validated_data.pop('creator')
793793
changed_data = {template_from: validated_data}

api/preprints/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
)
5353
from osf.utils import permissions as osf_permissions
5454
from osf.utils.workflows import DefaultStates
55+
from osf.models.contributor import get_user_permission
5556

5657

5758
class PrimaryFileRelationshipField(RelationshipField):
@@ -653,6 +654,7 @@ def validate_permission(self, value):
653654
user # if user is None then probably we're trying to make bulk update and this validation is not relevant
654655
and preprint.machine_state == DefaultStates.INITIAL.value
655656
and preprint.creator_id == user.id
657+
and get_user_permission(user, preprint) != value
656658
):
657659
raise ValidationError(
658660
'You cannot change your permission setting at this time. '

api/providers/permissions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from guardian.shortcuts import get_perms
21
from rest_framework import permissions as drf_permissions
32

43
from api.base.utils import get_user_auth
@@ -36,4 +35,7 @@ def has_permission(self, request, view):
3635
class MustBeModerator(drf_permissions.BasePermission):
3736
def has_permission(self, request, view):
3837
auth = get_user_auth(request)
39-
return bool(get_perms(auth.user, view.get_provider()))
38+
provider = view.get_provider()
39+
is_admin = provider.get_group('admin').user_set.filter(id=auth.user.id).exists()
40+
is_moderator = provider.get_group('moderator').user_set.filter(id=auth.user.id).exists()
41+
return is_moderator or is_admin

api/registrations/permissions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ class ContributorOrModerator(permissions.BasePermission):
77

88
def has_object_permission(self, request, view, obj):
99
auth = get_user_auth(request)
10+
is_admin = obj.provider.get_group('admin').user_set.filter(id=auth.user.id).exists()
11+
is_moderator = obj.provider.get_group('moderator').user_set.filter(id=auth.user.id).exists()
1012

11-
# If a user has perms on the provider, they must be a moderator or admin
12-
is_moderator = bool(get_perms(auth.user, obj.provider))
13-
return obj.is_admin_contributor(auth.user) or is_moderator
13+
return obj.is_admin_contributor(auth.user) or is_moderator or is_admin
1414

1515

1616
class ContributorOrModeratorOrPublic(permissions.BasePermission):

api_tests/nodes/views/test_node_list.py

Lines changed: 101 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,16 @@ def test_current_user_permissions(self, app, user, url, public_project, non_cont
259259
res = app.get(url_public, auth=superuser.auth)
260260
assert permissions.READ not in res.json['data'][0]['attributes']['current_user_permissions']
261261

262+
def test_legacy_host_for_htmls(self, app, url, public_project):
263+
settings.DOMAIN = 'https://staging3.osf.io'
264+
current_domain_response = app.get(url).json['data']
265+
assert current_domain_response[-1]['links']['html'].startswith(settings.DOMAIN)
266+
267+
# mock request from legacy OSF domain to staging3 backend
268+
# so that backend uses it to generate html links instead of current domain
269+
legacy_domain_response = app.get(url, headers={'Referer': 'http://legacy.osf.io'}).json['data']
270+
assert legacy_domain_response[-1]['links']['html'].startswith('http://legacy.osf.io')
271+
262272

263273
@pytest.mark.django_db
264274
@pytest.mark.enable_bookmark_creation
@@ -1467,25 +1477,6 @@ def test_create_from_template_errors(self, app, user_one, user_two, url):
14671477
expect_errors=True)
14681478
assert res.status_code == 404
14691479

1470-
# test_403_on_create_from_template_of_unauthorized_project
1471-
template_from = ProjectFactory(creator=user_two, is_public=True)
1472-
templated_project_data = {
1473-
'data': {
1474-
'type': 'nodes',
1475-
'attributes':
1476-
{
1477-
'title': 'No permission',
1478-
'category': 'project',
1479-
'template_from': template_from._id,
1480-
}
1481-
}
1482-
}
1483-
res = app.post_json_api(
1484-
url, templated_project_data,
1485-
auth=user_one.auth,
1486-
expect_errors=True)
1487-
assert res.status_code == 403
1488-
14891480
def test_creates_project_from_template(self, app, user_one, category, url):
14901481
template_from = ProjectFactory(creator=user_one, is_public=True)
14911482
template_component = ProjectFactory(
@@ -1517,6 +1508,97 @@ def test_creates_project_from_template(self, app, user_one, category, url):
15171508
assert len(new_project.nodes) == len(template_from.nodes)
15181509
assert new_project.nodes[0].title == template_component.title
15191510

1511+
def test_non_contributor_create_project_from_public_template_success(self, app, user_one, category, url):
1512+
template_from = ProjectFactory(creator=user_one, is_public=True)
1513+
user_without_permissions = AuthUserFactory()
1514+
templated_project_data = {
1515+
'data': {
1516+
'type': 'nodes',
1517+
'attributes':
1518+
{
1519+
'title': 'template from project',
1520+
'category': category,
1521+
'template_from': template_from._id,
1522+
}
1523+
}
1524+
}
1525+
res = app.post_json_api(
1526+
url, templated_project_data,
1527+
auth=user_without_permissions.auth
1528+
)
1529+
assert res.status_code == 201
1530+
1531+
def test_non_contributor_create_project_from_private_template_no_permission_fails(self, app, user_one, category, url):
1532+
template_from = ProjectFactory(creator=user_one, is_public=False)
1533+
user_without_permissions = AuthUserFactory()
1534+
templated_project_data = {
1535+
'data': {
1536+
'type': 'nodes',
1537+
'attributes':
1538+
{
1539+
'title': 'template from project',
1540+
'category': category,
1541+
'template_from': template_from._id,
1542+
}
1543+
}
1544+
}
1545+
res = app.post_json_api(
1546+
url, templated_project_data,
1547+
auth=user_without_permissions.auth,
1548+
expect_errors=True
1549+
)
1550+
assert res.status_code == 403
1551+
1552+
def test_contributor_create_project_from_private_template_with_permission_success(self, app, user_one, category, url):
1553+
template_from = ProjectFactory(creator=user_one, is_public=False)
1554+
user_without_permissions = AuthUserFactory()
1555+
template_from.add_contributor(user_without_permissions, permissions=permissions.READ, auth=Auth(user_one), save=True)
1556+
templated_project_data = {
1557+
'data': {
1558+
'type': 'nodes',
1559+
'attributes':
1560+
{
1561+
'title': 'template from project',
1562+
'category': category,
1563+
'template_from': template_from._id,
1564+
}
1565+
}
1566+
}
1567+
res = app.post_json_api(
1568+
url, templated_project_data,
1569+
auth=user_without_permissions.auth
1570+
)
1571+
assert res.status_code == 201
1572+
assert template_from.has_permission(user_without_permissions, permissions.READ)
1573+
1574+
template_from.update_contributor(
1575+
user_without_permissions,
1576+
permission=permissions.WRITE,
1577+
auth=Auth(user_one),
1578+
save=True,
1579+
visible=True
1580+
)
1581+
res = app.post_json_api(
1582+
url, templated_project_data,
1583+
auth=user_without_permissions.auth
1584+
)
1585+
assert res.status_code == 201
1586+
assert template_from.has_permission(user_without_permissions, permissions.WRITE)
1587+
1588+
template_from.update_contributor(
1589+
user_without_permissions,
1590+
permission=permissions.ADMIN,
1591+
auth=Auth(user_one),
1592+
save=True,
1593+
visible=True
1594+
)
1595+
res = app.post_json_api(
1596+
url, templated_project_data,
1597+
auth=user_without_permissions.auth
1598+
)
1599+
assert res.status_code == 201
1600+
assert template_from.has_permission(user_without_permissions, permissions.ADMIN)
1601+
15201602
def test_creates_project_creates_project_and_sanitizes_html(
15211603
self, app, user_one, category, url):
15221604
title = '<em>Cool</em> <strong>Project</strong>'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import pytest
2+
3+
from api.base.settings.defaults import API_BASE
4+
from api_tests.providers.mixins import OnlyModeratorOrAdminPermissionsMixin
5+
6+
from osf_tests.factories import CollectionProviderFactory
7+
8+
9+
@pytest.mark.django_db
10+
class TestOnlyModeratorOrAdmin(OnlyModeratorOrAdminPermissionsMixin):
11+
12+
@pytest.fixture()
13+
def urls(self, provider, moderator, admin):
14+
return [
15+
f'/{API_BASE}providers/collections/{provider._id}/moderators/',
16+
f'/{API_BASE}providers/collections/{provider._id}/moderators/{moderator._id}/',
17+
f'/{API_BASE}providers/collections/{provider._id}/moderators/{admin._id}/',
18+
]
19+
20+
@pytest.fixture()
21+
def provider(self):
22+
return CollectionProviderFactory()

api_tests/providers/mixins.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,3 +1038,42 @@ def test_provider_has_both_acceptable_and_default_licenses(self, app, provider,
10381038
assert license_one._id in license_ids
10391039
assert license_three._id in license_ids
10401040
assert license_two._id not in license_ids
1041+
1042+
1043+
@pytest.mark.django_db
1044+
class OnlyModeratorOrAdminPermissionsMixin:
1045+
1046+
@pytest.fixture()
1047+
def provider(self):
1048+
raise NotImplementedError
1049+
1050+
@pytest.fixture()
1051+
def user(self):
1052+
return AuthUserFactory()
1053+
1054+
@pytest.fixture()
1055+
def moderator(self, provider):
1056+
mod = AuthUserFactory()
1057+
provider.get_group('moderator').user_set.add(mod)
1058+
return mod
1059+
1060+
@pytest.fixture()
1061+
def admin(self, provider):
1062+
adm = AuthUserFactory()
1063+
provider.get_group('admin').user_set.add(adm)
1064+
return adm
1065+
1066+
@pytest.fixture()
1067+
def urls(self):
1068+
raise NotImplementedError
1069+
1070+
def test_moderator_or_admin_have_access_to_provider(self, app, provider, user, moderator, admin, urls):
1071+
for url in urls:
1072+
user_res = app.get(url, auth=user.auth, expect_errors=True)
1073+
assert user_res.status_code == 403
1074+
1075+
moderator_res = app.get(url, auth=moderator.auth)
1076+
assert moderator_res.status_code == 200
1077+
1078+
admin_res = app.get(url, auth=admin.auth)
1079+
assert admin_res.status_code == 200
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import pytest
2+
3+
from api.base.settings.defaults import API_BASE
4+
from api_tests.providers.mixins import OnlyModeratorOrAdminPermissionsMixin
5+
6+
from osf_tests.factories import PreprintProviderFactory
7+
8+
9+
@pytest.mark.django_db
10+
class TestOnlyModeratorOrAdmin(OnlyModeratorOrAdminPermissionsMixin):
11+
12+
@pytest.fixture()
13+
def urls(self, provider, moderator, admin):
14+
return [
15+
f'/{API_BASE}providers/preprints/{provider._id}/withdraw_requests/',
16+
f'/{API_BASE}providers/preprints/{provider._id}/moderators/',
17+
f'/{API_BASE}providers/preprints/{provider._id}/moderators/{moderator._id}/',
18+
f'/{API_BASE}providers/preprints/{provider._id}/moderators/{admin._id}/',
19+
]
20+
21+
@pytest.fixture()
22+
def provider(self):
23+
return PreprintProviderFactory()

0 commit comments

Comments
 (0)