Skip to content

Commit c9e1bcf

Browse files
authored
[Fixes #13581] Forbid API changing permissions for anonymous and registered members users if user is not allowed to (#13592)
* [Fixes #13581] added restriction in permission api if user dont have permission to update
1 parent 1553acc commit c9e1bcf

File tree

3 files changed

+292
-26
lines changed

3 files changed

+292
-26
lines changed

geonode/base/api/tests.py

Lines changed: 259 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,10 @@ def test_sort_resources(self):
10091009
reversed_resource_titles = sorted(resource_titles.copy())
10101010
self.assertNotEqual(resource_titles, reversed_resource_titles)
10111011

1012+
@override_settings(
1013+
EDITORS_CAN_MANAGE_ANONYMOUS_PERMISSIONS=True,
1014+
EDITORS_CAN_MANAGE_REGISTERED_MEMBERS_PERMISSIONS=True,
1015+
)
10121016
def test_perms_resources(self):
10131017
"""
10141018
Ensure we can Get & Set Permissions across the Resource Base list.
@@ -1164,7 +1168,9 @@ def test_perms_resources(self):
11641168
resource_perm_spec,
11651169
)
11661170

1167-
# Remove perms to Norman
1171+
# Remove perms to Norman by explicitly setting permissions to "none".
1172+
# Note: Omitting a user from the request preserves their existing permissions.
1173+
# To actually remove permissions, you must explicitly set them to "none".
11681174
resource_perm_spec = {
11691175
"uuid": resource.uuid,
11701176
"users": [
@@ -1178,6 +1184,16 @@ def test_perms_resources(self):
11781184
"is_staff": False,
11791185
"is_superuser": False,
11801186
},
1187+
{
1188+
"id": norman.id,
1189+
"username": norman.username,
1190+
"first_name": norman.first_name,
1191+
"last_name": norman.last_name,
1192+
"avatar": build_absolute_uri(avatar_url(bobby)),
1193+
"permissions": "none",
1194+
"is_staff": False,
1195+
"is_superuser": False,
1196+
},
11811197
{
11821198
"avatar": build_absolute_uri(avatar_url(bobby)),
11831199
"first_name": "admin",
@@ -2341,6 +2357,248 @@ def test_manager_can_edit_map(self):
23412357
resource_perm_spec,
23422358
)
23432359

2360+
@override_settings(
2361+
EDITORS_CAN_MANAGE_ANONYMOUS_PERMISSIONS=False,
2362+
EDITORS_CAN_MANAGE_REGISTERED_MEMBERS_PERMISSIONS=False,
2363+
)
2364+
def test_resource_service_permissions_with_restricted_settings(self):
2365+
"""
2366+
Test that when EDITORS_CAN_MANAGE_ANONYMOUS_PERMISSIONS and
2367+
EDITORS_CAN_MANAGE_REGISTERED_MEMBERS_PERMISSIONS are set to False,
2368+
only staff/admin users can modify anonymous and registered members permissions.
2369+
Non-staff editors should not be able to modify these groups' permissions.
2370+
"""
2371+
resource = Dataset.objects.filter(owner__username="admin").first()
2372+
bobby = get_user_model().objects.get(username="bobby")
2373+
2374+
# Give bobby edit permissions on the resource including permission to manage permissions
2375+
resource.set_permissions(
2376+
{
2377+
"users": {
2378+
bobby: [
2379+
"base.change_resourcebase",
2380+
"base.change_resourcebase_metadata",
2381+
"base.change_resourcebase_permissions",
2382+
]
2383+
}
2384+
}
2385+
)
2386+
2387+
# Login as bobby (non-staff editor)
2388+
self.assertTrue(self.client.login(username="bobby", password="bob"))
2389+
2390+
anonymous_group = Group.objects.get(name="anonymous")
2391+
registered_group = Group.objects.get(name="registered-members")
2392+
2393+
# Try to update permissions including anonymous and registered members groups
2394+
set_perms_url = urljoin(f"{reverse('base-resources-detail', kwargs={'pk': resource.pk})}/", "permissions")
2395+
perm_spec = {
2396+
"uuid": resource.uuid,
2397+
"groups": [
2398+
{
2399+
"id": anonymous_group.id,
2400+
"name": "anonymous",
2401+
"permissions": "view",
2402+
},
2403+
{
2404+
"id": registered_group.id,
2405+
"name": "registered-members",
2406+
"permissions": "download",
2407+
},
2408+
],
2409+
}
2410+
2411+
response = self.client.put(set_perms_url, data=perm_spec, format="json")
2412+
self.assertEqual(response.status_code, 200)
2413+
2414+
resp_js = json.loads(response.content.decode("utf-8"))
2415+
execution_id = resp_js.get("execution_id", "")
2416+
status_url = resp_js.get("status_url", None)
2417+
self.assertIsNotNone(status_url)
2418+
self.assertIsNotNone(execution_id)
2419+
2420+
for _cnt in range(0, 10):
2421+
response = self.client.get(f"{status_url}")
2422+
self.assertEqual(response.status_code, 200)
2423+
resp_js = json.loads(response.content.decode("utf-8"))
2424+
if resp_js.get("status", "") == "finished":
2425+
break
2426+
else:
2427+
resouce_service_dispatcher.apply((execution_id,))
2428+
sleep(3.0)
2429+
2430+
# Get the current permissions to verify anonymous and registered groups were excluded
2431+
get_perms_url = urljoin(f"{reverse('base-resources-detail', kwargs={'pk': resource.pk})}/", "permissions")
2432+
response = self.client.get(get_perms_url, format="json")
2433+
self.assertEqual(response.status_code, 200)
2434+
resource_perm_spec = response.data
2435+
2436+
groups_in_response = {g["name"]: g["permissions"] for g in resource_perm_spec.get("groups", [])}
2437+
2438+
# The groups should still have their original permissions (not the ones bobby tried to set)
2439+
# Bobby tried to set anonymous to "view" and registered-members to "download"
2440+
# But since he lacks permissions, these changes should NOT have been applied
2441+
# Verify the permissions remain unchanged from their original state
2442+
self.assertIn("anonymous", groups_in_response)
2443+
self.assertIn("registered-members", groups_in_response)
2444+
# The permissions should NOT be what bobby tried to set
2445+
self.assertNotEqual(
2446+
groups_in_response.get("anonymous"), "view", "Bobby should not have been able to set anonymous to view"
2447+
)
2448+
self.assertNotEqual(
2449+
groups_in_response.get("registered-members"),
2450+
"download",
2451+
"Bobby should not have been able to set registered-members to download",
2452+
)
2453+
2454+
# login as admin (staff user) and verify admin can modify these permissions
2455+
self.assertTrue(self.client.login(username="admin", password="admin"))
2456+
2457+
perm_spec_admin = {
2458+
"uuid": resource.uuid,
2459+
"groups": [
2460+
{
2461+
"id": anonymous_group.id,
2462+
"name": "anonymous",
2463+
"permissions": "view",
2464+
},
2465+
{
2466+
"id": registered_group.id,
2467+
"name": "registered-members",
2468+
"permissions": "view",
2469+
},
2470+
],
2471+
}
2472+
2473+
response = self.client.put(set_perms_url, data=perm_spec_admin, format="json")
2474+
self.assertEqual(response.status_code, 200)
2475+
2476+
# Wait for async task to complete
2477+
resp_js = json.loads(response.content.decode("utf-8"))
2478+
execution_id = resp_js.get("execution_id", "")
2479+
status_url = resp_js.get("status_url", None)
2480+
2481+
for _cnt in range(0, 10):
2482+
response = self.client.get(f"{status_url}")
2483+
self.assertEqual(response.status_code, 200)
2484+
resp_js = json.loads(response.content.decode("utf-8"))
2485+
if resp_js.get("status", "") == "finished":
2486+
break
2487+
else:
2488+
resouce_service_dispatcher.apply((execution_id,))
2489+
sleep(3.0)
2490+
2491+
# Verify admin successfully modified the permissions
2492+
response = self.client.get(get_perms_url, format="json")
2493+
self.assertEqual(response.status_code, 200)
2494+
resource_perm_spec = response.data
2495+
2496+
# Admin should have been able to set permissions for these groups
2497+
groups_in_response = {g["name"]: g["permissions"] for g in resource_perm_spec.get("groups", [])}
2498+
self.assertIn("anonymous", groups_in_response)
2499+
self.assertIn("registered-members", groups_in_response)
2500+
# Verify admin successfully changed the permissions to what was requested
2501+
self.assertEqual(
2502+
groups_in_response.get("anonymous"), "view", "Admin should have been able to set anonymous to view"
2503+
)
2504+
self.assertEqual(
2505+
groups_in_response.get("registered-members"),
2506+
"view",
2507+
"Admin should have been able to set registered-members to view",
2508+
)
2509+
2510+
@override_settings(
2511+
EDITORS_CAN_MANAGE_ANONYMOUS_PERMISSIONS=True,
2512+
EDITORS_CAN_MANAGE_REGISTERED_MEMBERS_PERMISSIONS=False,
2513+
)
2514+
def test_resource_service_permissions_with_partial_restriction(self):
2515+
"""
2516+
Test that when only EDITORS_CAN_MANAGE_REGISTERED_MEMBERS_PERMISSIONS is False,
2517+
editors can modify anonymous permissions but not registered members permissions.
2518+
"""
2519+
resource = Dataset.objects.filter(owner__username="admin").first()
2520+
bobby = get_user_model().objects.get(username="bobby")
2521+
2522+
# Give bobby edit permissions including permission to manage permissions
2523+
resource.set_permissions(
2524+
{
2525+
"users": {
2526+
bobby: [
2527+
"base.change_resourcebase",
2528+
"base.change_resourcebase_metadata",
2529+
"base.change_resourcebase_permissions",
2530+
]
2531+
}
2532+
}
2533+
)
2534+
2535+
# Login as bobby
2536+
self.assertTrue(self.client.login(username="bobby", password="bob"))
2537+
2538+
anonymous_group = Group.objects.get(name="anonymous")
2539+
registered_group = Group.objects.get(name="registered-members")
2540+
2541+
set_perms_url = urljoin(f"{reverse('base-resources-detail', kwargs={'pk': resource.pk})}/", "permissions")
2542+
perm_spec = {
2543+
"uuid": resource.uuid,
2544+
"groups": [
2545+
{
2546+
"id": anonymous_group.id,
2547+
"name": "anonymous",
2548+
"permissions": "view",
2549+
},
2550+
{
2551+
"id": registered_group.id,
2552+
"name": "registered-members",
2553+
"permissions": "download",
2554+
},
2555+
],
2556+
}
2557+
2558+
response = self.client.put(set_perms_url, data=perm_spec, format="json")
2559+
self.assertEqual(response.status_code, 200)
2560+
2561+
resp_js = json.loads(response.content.decode("utf-8"))
2562+
execution_id = resp_js.get("execution_id", "")
2563+
status_url = resp_js.get("status_url", None)
2564+
2565+
for _cnt in range(0, 10):
2566+
response = self.client.get(f"{status_url}")
2567+
self.assertEqual(response.status_code, 200)
2568+
resp_js = json.loads(response.content.decode("utf-8"))
2569+
if resp_js.get("status", "") == "finished":
2570+
break
2571+
else:
2572+
resouce_service_dispatcher.apply((execution_id,))
2573+
sleep(3.0)
2574+
2575+
# Verify bobby could modify anonymous but not registered members
2576+
# The registered-members group should have been filtered out from bobby's request
2577+
get_perms_url = urljoin(f"{reverse('base-resources-detail', kwargs={'pk': resource.pk})}/", "permissions")
2578+
response = self.client.get(get_perms_url, format="json")
2579+
self.assertEqual(response.status_code, 200)
2580+
resource_perm_spec = response.data
2581+
2582+
groups_in_response = {g["name"]: g["permissions"] for g in resource_perm_spec.get("groups", [])}
2583+
2584+
# Bobby tried to set anonymous to "view" and registered-members to "download"
2585+
# Since EDITORS_CAN_MANAGE_ANONYMOUS_PERMISSIONS=True, anonymous should be changed
2586+
# Since EDITORS_CAN_MANAGE_REGISTERED_MEMBERS_PERMISSIONS=False, registered-members should NOT be changed
2587+
self.assertIn("anonymous", groups_in_response)
2588+
self.assertIn("registered-members", groups_in_response)
2589+
2590+
# Verify anonymous was successfully changed by bobby
2591+
self.assertEqual(
2592+
groups_in_response.get("anonymous"), "view", "Bobby should have been able to set anonymous to view"
2593+
)
2594+
2595+
# Verify registered-members was NOT changed by bobby
2596+
self.assertNotEqual(
2597+
groups_in_response.get("registered-members"),
2598+
"download",
2599+
"Bobby should not have been able to set registered-members to download",
2600+
)
2601+
23442602
def test_resource_service_copy(self):
23452603
files = os.path.join(gisdata.GOOD_DATA, "vector/single_point.shp")
23462604
files_as_dict, _ = get_files(files)

geonode/base/api/views.py

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
FavoriteFilter,
6767
TKeywordsFilter,
6868
)
69-
from geonode.groups.models import GroupProfile
70-
from geonode.security.permissions import get_compact_perms_list, PermSpec, PermSpecCompact
69+
from geonode.groups.models import GroupProfile, Group
70+
from geonode.security.permissions import get_compact_perms_list, PermSpec
7171
from geonode.security.utils import (
7272
get_visible_resources,
7373
get_resources_with_perms,
@@ -109,6 +109,7 @@
109109
from geonode.assets.utils import create_asset_and_link, unlink_asset
110110
from geonode.assets.handlers import asset_handler_registry
111111
from geonode.utils import get_supported_datasets_file_types
112+
from geonode.base.utils import patch_perms
112113

113114
import logging
114115

@@ -615,29 +616,25 @@ def resource_service_permissions(self, request, pk, *args, **kwargs):
615616
action="permissions",
616617
input_params={"uuid": request_params.get("uuid", resource.uuid)},
617618
)
618-
elif request.method == "PUT":
619-
620-
perms_spec_compact = PermSpecCompact(request.data, resource)
621-
622-
if resource.dirty_state:
623-
raise Exception("Cannot update if the resource is in dirty state")
624-
resource.set_dirty_state()
625-
_exec_request = ExecutionRequest.objects.create(
626-
user=request.user,
627-
func_name="set_permissions",
628-
geonode_resource=resource,
629-
action="permissions",
630-
input_params={
631-
"uuid": request_params.get("uuid", resource.uuid),
632-
"permissions": perms_spec_compact.extended,
633-
"created": request_params.get("created", False),
634-
},
635-
)
636-
elif request.method == "PATCH":
637-
638-
perms_spec_compact_patch = PermSpecCompact(request.data, resource)
639-
perms_spec_compact_resource = PermSpecCompact(perms_spec.compact, resource)
640-
perms_spec_compact_resource.merge(perms_spec_compact_patch)
619+
elif request.method in ["PUT", "PATCH"]:
620+
user_perms = permissions_registry.get_perms(instance=resource, user=request.user)
621+
if request.data.get("groups"):
622+
excluded_ids = []
623+
if "can_manage_anonymous_permissions" not in user_perms:
624+
anonymous_group = Group.objects.get(name="anonymous")
625+
excluded_ids.append(anonymous_group.id)
626+
logger.info(
627+
f"User {request.user.username} cannot manage anonymous permissions on resource {resource.pk}"
628+
)
629+
if "can_manage_registered_member_permissions" not in user_perms:
630+
registered_group = Group.objects.get(name=groups_settings.REGISTERED_MEMBERS_GROUP_NAME)
631+
excluded_ids.append(registered_group.id)
632+
logger.info(
633+
f"User {request.user.username} cannot manage registered members permissions on resource {resource.pk}"
634+
)
635+
if excluded_ids:
636+
request.data["groups"] = [g for g in request.data["groups"] if g.get("id") not in excluded_ids]
637+
perms_spec_compact_resource = patch_perms(request.data, perms_spec.compact, resource)
641638

642639
if resource.dirty_state:
643640
raise Exception("Cannot update if the resource is in dirty state")

geonode/base/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from geonode.security.utils import AdvancedSecurityWorkflowManager
4040
from geonode.thumbs.utils import get_thumbs, remove_thumb
4141
from geonode.utils import get_legend_url
42+
from geonode.security.permissions import PermSpecCompact
4243

4344
logger = logging.getLogger("geonode.base.utils")
4445

@@ -213,3 +214,13 @@ def remove_country_from_languagecode(language: str):
213214

214215
lang, _, _ = language.lower().partition("-")
215216
return lang
217+
218+
219+
def patch_perms(updated_perms_compact, current_perms_compact, resource):
220+
"""
221+
Patch updated permission changes with current permissions.
222+
"""
223+
perms_spec_compact_patch = PermSpecCompact(updated_perms_compact, resource)
224+
perms_spec_compact_resource = PermSpecCompact(current_perms_compact, resource)
225+
perms_spec_compact_resource.merge(perms_spec_compact_patch)
226+
return perms_spec_compact_resource

0 commit comments

Comments
 (0)