diff --git a/gateway/sds_gateway/api_methods/models.py b/gateway/sds_gateway/api_methods/models.py index 398629f6..fd37bb58 100644 --- a/gateway/sds_gateway/api_methods/models.py +++ b/gateway/sds_gateway/api_methods/models.py @@ -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 + return None @classmethod diff --git a/gateway/sds_gateway/api_methods/tests/test_asset_access_control.py b/gateway/sds_gateway/api_methods/tests/test_asset_access_control.py index a5c7f67f..4257aac9 100644 --- a/gateway/sds_gateway/api_methods/tests/test_asset_access_control.py +++ b/gateway/sds_gateway/api_methods/tests/test_asset_access_control.py @@ -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.""" diff --git a/gateway/sds_gateway/api_methods/utils/asset_access_control.py b/gateway/sds_gateway/api_methods/utils/asset_access_control.py index 4b48c2bd..f2c989c9 100644 --- a/gateway/sds_gateway/api_methods/utils/asset_access_control.py +++ b/gateway/sds_gateway/api_methods/utils/asset_access_control.py @@ -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 ) @@ -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 for capture in file_captures ) @@ -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 @@ -97,11 +99,16 @@ 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 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): @@ -109,10 +116,10 @@ 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 @@ -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) + # 2. Files part of captures that are shared with the user # EXPAND: Support both M2M (captures) and FK (capture) relationships capture_shared_query = ( @@ -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( @@ -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) ) ) ) @@ -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) ) ) | @@ -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: @@ -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 = ( @@ -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) ) ) |