Skip to content
8 changes: 8 additions & 0 deletions gateway/sds_gateway/api_methods/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,14 @@ def get_user_permission_level(
if permission:
return permission.permission_level

# if we get to here then check if the item is public
if item_type in item_models:
model_class = item_models[ItemType(item_type)]
if model_class.objects.filter(
uuid=item_uuid, is_public=True, is_deleted=False
).exists():
return PermissionLevel.VIEWER
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

return None

@classmethod
Expand Down
55 changes: 55 additions & 0 deletions gateway/sds_gateway/api_methods/tests/test_asset_access_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,61 @@ def test_get_accessible_files_queryset_via_dataset_m2m(self):
assert file1 not in other_files
assert file2 in other_files # File owner always has access

def test_public_m2m_assets_are_in_accessible_querysets(self):
"""Public M2M relationships grant access without an explicit share."""
# Generated test via Cursor
public_dataset = Dataset.objects.create(
name="Public Dataset",
owner=self.owner,
description="Public test dataset",
is_public=True,
)
public_capture = Capture.objects.create(
name="Public Capture",
owner=self.owner,
capture_type=CaptureType.RadioHound,
index_name="public-captures-rh",
top_level_dir="public-capture-dir",
is_public=True,
)
capture_in_public_dataset = Capture.objects.create(
name="Capture in Public Dataset",
owner=self.owner,
capture_type=CaptureType.RadioHound,
index_name="public-dataset-captures-rh",
top_level_dir="public-dataset-capture-dir",
)
capture_in_public_dataset.datasets.add(public_dataset)

file_in_public_capture = File.objects.create(
name="public-capture-file.h5",
owner=self.owner,
size=1000,
)
file_in_public_capture.captures.add(public_capture)

file_in_capture_in_public_dataset = File.objects.create(
name="public-dataset-capture-file.h5",
owner=self.owner,
size=1000,
)
file_in_capture_in_public_dataset.captures.add(capture_in_public_dataset)

file_in_public_dataset = File.objects.create(
name="public-dataset-file.h5",
owner=self.owner,
size=1000,
)
file_in_public_dataset.datasets.add(public_dataset)

accessible_files = get_accessible_files_queryset(self.other_user)
accessible_captures = get_accessible_captures_queryset(self.other_user)

assert file_in_public_capture in accessible_files
assert file_in_capture_in_public_dataset in accessible_files
assert file_in_public_dataset in accessible_files
assert capture_in_public_dataset in accessible_captures

# Queryset tests with both FK and M2M
def test_get_accessible_files_queryset_via_both_relationships(self):
"""Test get_accessible_files_queryset with both FK and M2M relationships."""
Expand Down
31 changes: 23 additions & 8 deletions gateway/sds_gateway/api_methods/utils/asset_access_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def user_has_access_to_capture(user, capture: Capture) -> bool:

# Check if any dataset is owned by user or in shared_datasets
user_has_access_to_dataset = any(
dataset.owner == user or dataset.uuid in shared_datasets
dataset.owner == user or dataset.uuid in shared_datasets or dataset.is_public
for dataset in capture_datasets
)

Expand Down Expand Up @@ -77,7 +77,7 @@ def user_has_access_to_file(user, file: File) -> bool:

# Check if file's captures are accessible (directly shared, owned by user)
user_has_access_to_capture = any(
capture.owner == user or capture.uuid in shared_captures
capture.owner == user or capture.uuid in shared_captures or capture.is_public
Comment thread
cursor[bot] marked this conversation as resolved.
for capture in file_captures
)

Expand All @@ -88,7 +88,9 @@ def user_has_access_to_file(user, file: File) -> bool:
capture, include_deleted=False
)
if any(
dataset.owner == user or dataset.uuid in shared_datasets
dataset.owner == user
or dataset.uuid in shared_datasets
or dataset.is_public
for dataset in capture_datasets
):
user_has_access_to_capture = True
Expand All @@ -97,22 +99,27 @@ def user_has_access_to_file(user, file: File) -> bool:
# Use centralized function to get datasets (handles both M2M and FK)
file_datasets = relationship_utils.get_file_datasets(file, include_deleted=False)
user_has_access_to_dataset = any(
dataset.owner == user or dataset.uuid in shared_datasets
dataset.owner == user or dataset.uuid in shared_datasets or dataset.is_public
Comment thread
cursor[bot] marked this conversation as resolved.
for dataset in file_datasets
)

return user_owns_file or user_has_access_to_capture or user_has_access_to_dataset
return (
user_owns_file
or user_has_access_to_capture
or user_has_access_to_dataset
or file.is_public
)


def get_accessible_files_queryset(user):
"""
Get a queryset of files that the user has access to.

A user has access to files if:
1. They own the file directly, OR
1. They own the file directly, OR is_public=True OR
2. The file is part of a capture that is shared with them, OR
3. The file is part of a dataset that is shared with them, OR
4. The file is part of a capture that is part of a shared dataset
4. The file is part of a capture that is part of a shared dataset, OR

Args:
user: The user to check access for
Expand Down Expand Up @@ -140,6 +147,8 @@ def get_accessible_files_queryset(user):
# test_file_owner_has_access
access_query |= Q(owner=user)

access_query |= Q(is_public=True)
Comment thread
cursor[bot] marked this conversation as resolved.

# 2. Files part of captures that are shared with the user
# EXPAND: Support both M2M (captures) and FK (capture) relationships
capture_shared_query = (
Expand All @@ -151,6 +160,7 @@ def get_accessible_files_queryset(user):
|
# Capture directly shared with user
Q(captures__uuid__in=captures_shared_with_user)
| Q(captures__is_public=True)
|
# Capture part of dataset that is shared with user (via M2M)
Q(
Expand All @@ -162,6 +172,7 @@ def get_accessible_files_queryset(user):
|
# Dataset directly shared with user
Q(captures__datasets__uuid__in=datasets_shared_with_user)
| Q(captures__datasets__is_public=True)
)
)
)
Expand Down Expand Up @@ -200,6 +211,7 @@ def get_accessible_files_queryset(user):
|
# Dataset directly shared with user
Q(datasets__uuid__in=datasets_shared_with_user)
| Q(datasets__is_public=True)
)
)
|
Expand All @@ -226,7 +238,7 @@ def get_accessible_captures_queryset(user):

A user has access to captures if:
1. They own the capture directly, OR
2. The capture is shared with them, OR
2. The capture is shared with them, OR is_public=True OR
3. The capture is part of a dataset that is shared with them

Args:
Expand Down Expand Up @@ -255,6 +267,8 @@ def get_accessible_captures_queryset(user):
# 2. Captures directly shared with the user
access_query |= Q(uuid__in=captures_shared_with_user)

access_query |= Q(is_public=True)

# 3. Captures part of datasets that are shared with the user
# EXPAND: Support both M2M (datasets) and FK (dataset) relationships
dataset_shared_query = (
Expand All @@ -266,6 +280,7 @@ def get_accessible_captures_queryset(user):
|
# Dataset directly shared with user
Q(datasets__uuid__in=datasets_shared_with_user)
| Q(datasets__is_public=True)
Comment thread
cursor[bot] marked this conversation as resolved.
)
)
|
Expand Down