Skip to content

Commit

Permalink
add SampleDataFileExistsAPIView access restricting (#2078)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkonie committed Feb 26, 2025
1 parent 4b35eac commit b6aec5c
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 22 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Added
- Support for newlines in altamISA error messages (#2033)
- Support for comment, performer and contact field values as list (#1789, #2033)
- Support for numeric field values as list (#1789, #2033)
- ``SHEETS_API_FILE_EXISTS_RESTRICT`` Django setting (#2078)
- **Taskflowbackend**
- ``TaskflowAPI.raise_submit_api_exception()`` helper (#1847)
- UTF-8 BOM header support for MD5 files (#1818)
Expand Down Expand Up @@ -81,6 +82,7 @@ Changed
- Return ``ProjectIrodsFileListAPIView`` results as list without ``irods_data`` object (#2040)
- Remove length limitation from ``Process.performer`` (#1789, #1942, #2033)
- Replace REST API ``SODARUserSerializer`` fields with UUID ``SlugRelatedField`` (#2057)
- Enable ``SampleDataFileExistsAPIView`` access restriction to guests and above (#2078)
- **Taskflowbackend**
- Refactor task tests (#2002)
- Unify user name parameter naming in flows (#1653)
Expand Down
7 changes: 4 additions & 3 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,6 @@ def set_logging(level=None):
SHEETS_ONTOLOGY_URL_SKIP = env.list(
'SHEETS_ONTOLOGY_URL_SKIP', default=['bioontology.org', 'hpo.jax.org']
)

# Labels and URL patterns for external link columns
# Provide custom labels via a JSON file via SHEETS_EXTERNAL_LINK_PATH.
# Each entry should have a "label" and an optional "url".
Expand All @@ -762,10 +761,8 @@ def set_logging(level=None):
'SHEETS_EXTERNAL_LINK_PATH',
os.path.join(ROOT_DIR, 'samplesheets/config/ext_links.json'),
)

# Remote sample sheet sync interval in minutes
SHEETS_SYNC_INTERVAL = env.int('SHEETS_SYNC_INTERVAL', 5)

# BAM/CRAM file path glob patterns to omit from study shortcuts and IGV sessions
SHEETS_IGV_OMIT_BAM = env.list(
'SHEETS_IGV_OMIT_BAM', default=['*dragen_evidence.bam']
Expand All @@ -775,6 +772,10 @@ def set_logging(level=None):
'SHEETS_IGV_OMIT_VCF',
default=['*cnv.vcf.gz', '*ploidy.vcf.gz', '*sv.vcf.gz'],
)
# Restrict SampleDataFileExistsAPIView access to users with project roles
SHEETS_API_FILE_EXISTS_RESTRICT = env.bool(
'SHEETS_API_FILE_EXISTS_RESTRICT', False
)

# Landingzones app settings
# Status query interval in seconds
Expand Down
1 change: 1 addition & 0 deletions config/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
)
SHEETS_IGV_OMIT_BAM = ['*dragen_evidence.bam']
SHEETS_IGV_OMIT_VCF = ['*cnv.vcf.gz', '*ploidy.vcf.gz', '*sv.vcf.gz']
SHEETS_API_FILE_EXISTS_RESTRICT = False

# Landingzones app settings
LANDINGZONES_TRIGGER_ENABLE = True
Expand Down
4 changes: 4 additions & 0 deletions docs_manual/source/admin_settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ Sample Sheets Settings
``SHEETS_IGV_OMIT_VCF``
VCF file name suffixes to omit from study shortcuts and IGV session
generation.
``SHEETS_API_FILE_EXISTS_RESTRICT``
Restrict access to ``SampleDataFileExistsAPIView`` to users with the role of
project guest or above in any category or project. Recommended for instances
deployed on the public internet with general OIDC SSO access (boolean).

Landing Zones Settings
----------------------
Expand Down
4 changes: 3 additions & 1 deletion docs_manual/source/sodar_release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Release for SODAR Core v1.0 upgrade, iRODS v4.3 upgrade and feature updates.
- Add support for comment, performer and contact field values as list
- Add support for numeric field values as list
- Add support for UTF-8 BOM header in MD5 checksum files
- Add optional SampleDataFileExistsAPIView access restricting for users with
project roles
- Update minimum supported iRODS version to v4.3.3
- Update REST API versioning
- Update REST API views for OpenAPI support
Expand All @@ -31,11 +33,11 @@ Release for SODAR Core v1.0 upgrade, iRODS v4.3 upgrade and feature updates.
or iRODS collections
- Update irodsinfo configuration download to return JSON without Zip archive if
client-side cert is not set
- Upgrade to Django v4.2
- Upgrade to Postgres v16
- Upgrade to python-irodsclient v2.2.0
- Upgrade to altamISA v0.3.0
- Upgrade to SODAR Core v1.0.5
- Upgrade to Django v4.2
- Remove Python v3.8 support
- Remove Postgres <12 support
- Remove iRODS <4.3 support
Expand Down
34 changes: 29 additions & 5 deletions samplesheets/tests/test_permissions_api_taskflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def setUp(self):
self.irods.data_objects.put(
IRODS_FILE_PATH, coll_path, **{REG_CHKSUM_KW: ''}
)
self.post_data = {'checksum': IRODS_FILE_MD5}
self.get_data = {'checksum': IRODS_FILE_MD5}
self.url = reverse('samplesheets:api_file_exists')

def test_get(self):
Expand All @@ -134,9 +134,9 @@ def test_get(self):
self.user_guest,
self.user_no_roles,
]
self.assert_response_api(self.url, good_users, 200, data=self.post_data)
self.assert_response_api(self.url, good_users, 200, data=self.get_data)
self.assert_response_api(
self.url, self.anonymous, 401, data=self.post_data
self.url, self.anonymous, 401, data=self.get_data
)

@override_settings(PROJECTROLES_ALLOW_ANONYMOUS=True)
Expand All @@ -160,9 +160,33 @@ def test_get_archive(self):
self.user_guest,
self.user_no_roles,
]
self.assert_response_api(self.url, good_users, 200, data=self.post_data)
self.assert_response_api(self.url, good_users, 200, data=self.get_data)
self.assert_response_api(
self.url, self.anonymous, 401, data=self.post_data
self.url, self.anonymous, 401, data=self.get_data
)

@override_settings(SHEETS_API_FILE_EXISTS_RESTRICT=True)
def test_get_restrict(self):
"""Test GET with file exists restriction enabled"""
good_users = [
self.superuser,
self.user_owner_cat,
self.user_delegate_cat,
self.user_contributor_cat,
self.user_guest_cat,
self.user_owner,
self.user_delegate,
self.user_contributor,
self.user_guest,
]
bad_users = [
self.user_finder_cat,
self.user_no_roles,
]
self.assert_response_api(self.url, good_users, 200, data=self.get_data)
self.assert_response_api(self.url, bad_users, 403, data=self.get_data)
self.assert_response_api(
self.url, self.anonymous, 401, data=self.get_data
)


Expand Down
39 changes: 27 additions & 12 deletions samplesheets/tests/test_views_api_taskflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1171,11 +1171,13 @@ class TestSampleDataFileExistsAPIView(SampleSheetAPITaskflowTestBase):
def setUp(self):
super().setUp()
self.make_irods_colls(self.investigation)
self.url = reverse('samplesheets:api_file_exists')

def test_get_no_file(self):
"""Test GET with no file uploaded"""
url = reverse('samplesheets:api_file_exists')
response = self.request_knox(url, data={'checksum': IRODS_FILE_MD5})
"""Test SampleDataFileExistsAPIView GET with no file uploaded"""
response = self.request_knox(
self.url, data={'checksum': IRODS_FILE_MD5}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['status'], False)

Expand All @@ -1185,35 +1187,48 @@ def test_get_file(self):
self.irods.data_objects.put(
IRODS_FILE_PATH, coll_path, **{REG_CHKSUM_KW: ''}
)
url = reverse('samplesheets:api_file_exists')
response = self.request_knox(url, data={'checksum': IRODS_FILE_MD5})
response = self.request_knox(
self.url, data={'checksum': IRODS_FILE_MD5}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['status'], True)

def test_get_file_sub_coll(self):
"""Test GET with file in a sub collection"""
"""Test GET with file in sub collection"""
coll_path = self.irods_backend.get_sample_path(self.project) + '/sub'
self.irods.collections.create(coll_path)
self.irods.data_objects.put(
IRODS_FILE_PATH, coll_path + '/', **{REG_CHKSUM_KW: ''}
)
url = reverse('samplesheets:api_file_exists')
response = self.request_knox(url, data={'checksum': IRODS_FILE_MD5})
response = self.request_knox(
self.url, data={'checksum': IRODS_FILE_MD5}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['status'], True)

def test_get_no_checksum(self):
"""Test GET with no checksum (should fail)"""
url = reverse('samplesheets:api_file_exists')
response = self.request_knox(url, data={'checksum': ''})
response = self.request_knox(self.url, data={'checksum': ''})
self.assertEqual(response.status_code, 400)

def test_get_invalid_checksum(self):
"""Test GET with invalid checksum (should fail)"""
url = reverse('samplesheets:api_file_exists')
response = self.request_knox(url, data={'checksum': 'Invalid MD5!'})
response = self.request_knox(
self.url, data={'checksum': 'Invalid MD5!'}
)
self.assertEqual(response.status_code, 400)

@override_settings(SHEETS_API_FILE_EXISTS_RESTRICT=True)
def test_get_restrict(self):
"""Test GET with file exists restriction enabled"""
user_no_roles = self.make_user('user_no_roles')
response = self.request_knox(
self.url,
data={'checksum': IRODS_FILE_MD5},
token=self.get_token(user_no_roles),
)
self.assertEqual(response.status_code, 403)


class TestProjectIrodsFileListAPIView(SampleSheetAPITaskflowTestBase):
"""Tests for ProjectIrodsFileListAPIView"""
Expand Down
28 changes: 27 additions & 1 deletion samplesheets/views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@

# Projectroles dependency
from projectroles.app_settings import AppSettingAPI
from projectroles.models import RemoteSite
from projectroles.models import (
RoleAssignment,
RemoteSite,
SODAR_CONSTANTS,
ROLE_RANKING,
)
from projectroles.plugins import get_backend_api
from projectroles.views_api import (
SODARAPIBaseMixin,
Expand Down Expand Up @@ -75,6 +80,9 @@
table_builder = SampleSheetTableBuilder()


# SODAR constants
PROJECT_ROLE_GUEST = SODAR_CONSTANTS['PROJECT_ROLE_GUEST']

# Local constants
SAMPLESHEETS_API_MEDIA_TYPE = 'application/vnd.bihealth.sodar.samplesheets+json'
SAMPLESHEETS_API_ALLOWED_VERSIONS = ['1.0']
Expand All @@ -85,6 +93,10 @@
IRODS_REQUEST_EX_MSG = 'iRODS data request failed'
IRODS_TICKET_EX_MSG = 'iRODS access ticket failed'
IRODS_TICKET_NO_UPDATE_FIELDS_MSG = 'No fields to update'
FILE_EXISTS_RESTRICT_MSG = (
'File exist query access restricted: user does not have guest access or '
'above in any project (SHEETS_API_FILE_EXISTS_RESTRICT=True)'
)


# Base Classes and Mixins ------------------------------------------------------
Expand Down Expand Up @@ -849,6 +861,10 @@ class SampleDataFileExistsAPIView(
Return status of data object existing in SODAR iRODS by MD5 checksum.
Includes all projects in search regardless of user permissions.
If ``SHEETS_API_FILE_EXISTS_RESTRICT`` is set True on the server, this view
is only accessible by users who have a guest role or above in at least one
category or project.
**URL:** ``/samplesheets/api/file/exists``
**Methods:** ``GET``
Expand All @@ -867,6 +883,16 @@ class SampleDataFileExistsAPIView(
permission_classes = (IsAuthenticated,)

def get(self, request, *args, **kwargs):
if (
settings.SHEETS_API_FILE_EXISTS_RESTRICT
and not request.user.is_superuser
):
roles = RoleAssignment.objects.filter(
user=request.user,
role__rank__lte=ROLE_RANKING[PROJECT_ROLE_GUEST],
)
if roles.count() == 0:
raise PermissionDenied(FILE_EXISTS_RESTRICT_MSG)
if not settings.ENABLE_IRODS:
raise APIException('iRODS not enabled')
irods_backend = get_backend_api('omics_irods')
Expand Down

0 comments on commit b6aec5c

Please sign in to comment.