diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6ed190bb..7b4f58ff 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,7 @@ Added - Assay app unit tests (#1980) - Missing assay plugin ``__init__.py`` files (#2014) - Study plugin override via ISA-Tab comments (#1885) + - Token auth support in study plugin IGV XML serving views (#1999, #2021) Changed ------- diff --git a/irodsbackend/views.py b/irodsbackend/views.py index 460e200c..e7fabe7f 100644 --- a/irodsbackend/views.py +++ b/irodsbackend/views.py @@ -233,7 +233,8 @@ def get(self, request, *args, **kwargs): @fallback_to_auth_basic class BasicAuthView(View): """ - View for verifying login credentials for local users in iRODS. + View for verifying login credentials for local users in iRODS. Allows using + Knox token in place of password. Should only be used in local development and testing situations or when an external LDAP/AD login is not available. diff --git a/samplesheets/studyapps/cancer/tests/test_permissions.py b/samplesheets/studyapps/cancer/tests/test_permissions.py new file mode 100644 index 00000000..ca731cac --- /dev/null +++ b/samplesheets/studyapps/cancer/tests/test_permissions.py @@ -0,0 +1,57 @@ +"""Permission tests for the cancer study app""" + +# NOTE: Basic auth tested in test_views + +import os + +from django.urls import reverse + +from samplesheets.models import GenericMaterial +from samplesheets.tests.test_io import SHEET_DIR +from samplesheets.tests.test_permissions import SamplesheetsPermissionTestBase + + +# Local constants +SHEET_PATH = os.path.join(SHEET_DIR, 'bih_cancer.zip') +SOURCE_ID = 'normal1' + + +class TestIGVSessionFileRenderView(SamplesheetsPermissionTestBase): + """Tests for IGVSessionFileRenderView permissions""" + + def setUp(self): + super().setUp() + self.investigation = self.import_isa_from_file(SHEET_PATH, self.project) + self.study = self.investigation.studies.first() + self.assay = self.study.assays.first() + self.source = GenericMaterial.objects.get( + study=self.study, name=SOURCE_ID + ) + self.url = reverse( + 'samplesheets.studyapps.cancer:igv', + kwargs={'genericmaterial': self.source.sodar_uuid}, + ) + + def test_get(self): + """Test IGVSessionFileRenderView GET""" + good_users = [ + self.superuser, + self.user_owner_cat, # Inherited + self.user_delegate_cat, # Inherited + self.user_contributor_cat, # Inherited + self.user_guest_cat, # Inherited + 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(self.url, good_users, 200) + self.assert_response(self.url, bad_users, 302) + self.assert_response(self.url, self.anonymous, 401) + # Test public project + self.project.set_public() + self.assert_response( + self.url, [self.user_finder_cat, self.user_no_roles], 200 + ) + self.assert_response(self.url, self.anonymous, 401) diff --git a/samplesheets/studyapps/cancer/tests/test_views.py b/samplesheets/studyapps/cancer/tests/test_views.py new file mode 100644 index 00000000..71706a75 --- /dev/null +++ b/samplesheets/studyapps/cancer/tests/test_views.py @@ -0,0 +1,103 @@ +"""View tests for the cancer study app""" + +# NOTE: We don't need to add files in iRODS to test this view + +import base64 +import os + +from django.urls import reverse + +# Projectroles dependency +from projectroles.tests.test_views_api import ( + SODARAPIViewTestMixin, + EMPTY_KNOX_TOKEN, +) + +from samplesheets.models import GenericMaterial +from samplesheets.tests.test_io import SHEET_DIR +from samplesheets.tests.test_models import SampleSheetModelMixin +from samplesheets.tests.test_views import SamplesheetsViewTestBase + + +# Local constants +SHEET_PATH = os.path.join(SHEET_DIR, 'bih_cancer.zip') +SOURCE_ID = 'normal1' + + +class TestIGVSessionFileRenderView( + SODARAPIViewTestMixin, + SampleSheetModelMixin, + SamplesheetsViewTestBase, +): + """Tests for cancer plugin IGVSessionFileRenderView""" + + @staticmethod + def _get_auth_header(username, password): + """Return basic auth header""" + credentials = base64.b64encode( + '{}:{}'.format(username, password).encode('utf-8') + ).strip() + return { + 'HTTP_AUTHORIZATION': 'Basic {}'.format(credentials.decode('utf-8')) + } + + def setUp(self): + super().setUp() + self.investigation = self.import_isa_from_file(SHEET_PATH, self.project) + self.study = self.investigation.studies.first() + self.assay = self.study.assays.first() + self.source = GenericMaterial.objects.get( + study=self.study, name=SOURCE_ID + ) + self.url = reverse( + 'samplesheets.studyapps.cancer:igv', + kwargs={'genericmaterial': self.source.sodar_uuid}, + ) + + def test_get(self): + """Test IGVSessionFileRenderView GET""" + with self.login(self.user_contributor): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.get('Content-Disposition'), + 'attachment; filename="{}.case.igv.xml"'.format(SOURCE_ID), + ) + # NOTE: XML forming tested in TestGetIGVXML + + def test_get_basic_auth(self): + """Test GET with basic auth""" + response = self.client.get( + self.url, + **self._get_auth_header(self.user_contributor.username, 'password') + ) + self.assertEqual(response.status_code, 200) + + def test_get_token(self): + """Test GET with Knox token""" + knox_token = self.get_token(self.user_contributor) + response = self.client.get( + self.url, + **self._get_auth_header(self.user_contributor.username, knox_token) + ) + self.assertEqual(response.status_code, 200) + + def test_get_token_invalid(self): + """Test GET with invalid Knox token""" + self.get_token(self.user_contributor) + response = self.client.get( + self.url, + **self._get_auth_header( + self.user_contributor.username, EMPTY_KNOX_TOKEN + ) + ) + self.assertEqual(response.status_code, 401) + + def test_get_token_wrong_user(self): + """Test GET with Knox token and wrong user""" + knox_token = self.get_token(self.user_contributor) + response = self.client.get( + self.url, + **self._get_auth_header(self.user_delegate.username, knox_token) + ) + self.assertEqual(response.status_code, 401) diff --git a/samplesheets/studyapps/germline/tests/test_permissions.py b/samplesheets/studyapps/germline/tests/test_permissions.py new file mode 100644 index 00000000..a9d46e98 --- /dev/null +++ b/samplesheets/studyapps/germline/tests/test_permissions.py @@ -0,0 +1,57 @@ +"""Permission tests for the germline study app""" + +# NOTE: Basic auth tested in test_views + +import os + +from django.urls import reverse + +from samplesheets.models import GenericMaterial +from samplesheets.tests.test_io import SHEET_DIR +from samplesheets.tests.test_permissions import SamplesheetsPermissionTestBase + + +# Local constants +SHEET_PATH = os.path.join(SHEET_DIR, 'bih_germline.zip') +SOURCE_ID = 'p1' + + +class TestIGVSessionFileRenderView(SamplesheetsPermissionTestBase): + """Tests for IGVSessionFileRenderView permissions""" + + def setUp(self): + super().setUp() + self.investigation = self.import_isa_from_file(SHEET_PATH, self.project) + self.study = self.investigation.studies.first() + self.assay = self.study.assays.first() + self.source = GenericMaterial.objects.get( + study=self.study, name=SOURCE_ID + ) + self.url = reverse( + 'samplesheets.studyapps.germline:igv', + kwargs={'genericmaterial': self.source.sodar_uuid}, + ) + + def test_get(self): + """Test IGVSessionFileRenderView GET""" + good_users = [ + self.superuser, + self.user_owner_cat, # Inherited + self.user_delegate_cat, # Inherited + self.user_contributor_cat, # Inherited + self.user_guest_cat, # Inherited + 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(self.url, good_users, 200) + self.assert_response(self.url, bad_users, 302) + self.assert_response(self.url, self.anonymous, 401) + # Test public project + self.project.set_public() + self.assert_response( + self.url, [self.user_finder_cat, self.user_no_roles], 200 + ) + self.assert_response(self.url, self.anonymous, 401) diff --git a/samplesheets/studyapps/germline/tests/test_views.py b/samplesheets/studyapps/germline/tests/test_views.py new file mode 100644 index 00000000..141aa877 --- /dev/null +++ b/samplesheets/studyapps/germline/tests/test_views.py @@ -0,0 +1,104 @@ +"""View tests for the germline study app""" + +# NOTE: We don't need to add files in iRODS to test this view + +import base64 +import os + +from django.urls import reverse + +# Projectroles dependency +from projectroles.tests.test_views_api import ( + SODARAPIViewTestMixin, + EMPTY_KNOX_TOKEN, +) + +from samplesheets.models import GenericMaterial +from samplesheets.tests.test_io import SHEET_DIR +from samplesheets.tests.test_models import SampleSheetModelMixin +from samplesheets.tests.test_views import SamplesheetsViewTestBase + + +# Local constants +SHEET_PATH = os.path.join(SHEET_DIR, 'bih_germline.zip') +SOURCE_ID = 'p1' +FAMILY_ID = 'FAM_p1' + + +class TestIGVSessionFileRenderView( + SODARAPIViewTestMixin, + SampleSheetModelMixin, + SamplesheetsViewTestBase, +): + """Tests for germline plugin IGVSessionFileRenderView""" + + @staticmethod + def _get_auth_header(username, password): + """Return basic auth header""" + credentials = base64.b64encode( + '{}:{}'.format(username, password).encode('utf-8') + ).strip() + return { + 'HTTP_AUTHORIZATION': 'Basic {}'.format(credentials.decode('utf-8')) + } + + def setUp(self): + super().setUp() + self.investigation = self.import_isa_from_file(SHEET_PATH, self.project) + self.study = self.investigation.studies.first() + self.assay = self.study.assays.first() + self.source = GenericMaterial.objects.get( + study=self.study, name=SOURCE_ID + ) + self.url = reverse( + 'samplesheets.studyapps.germline:igv', + kwargs={'genericmaterial': self.source.sodar_uuid}, + ) + + def test_get(self): + """Test IGVSessionFileRenderView GET""" + with self.login(self.user_contributor): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.get('Content-Disposition'), + 'attachment; filename="{}.pedigree.igv.xml"'.format(FAMILY_ID), + ) + # NOTE: XML forming tested in TestGetIGVXML + + def test_get_basic_auth(self): + """Test GET with basic auth""" + response = self.client.get( + self.url, + **self._get_auth_header(self.user_contributor.username, 'password') + ) + self.assertEqual(response.status_code, 200) + + def test_get_token(self): + """Test GET with Knox token""" + knox_token = self.get_token(self.user_contributor) + response = self.client.get( + self.url, + **self._get_auth_header(self.user_contributor.username, knox_token) + ) + self.assertEqual(response.status_code, 200) + + def test_get_token_invalid(self): + """Test GET with invalid Knox token""" + self.get_token(self.user_contributor) + response = self.client.get( + self.url, + **self._get_auth_header( + self.user_contributor.username, EMPTY_KNOX_TOKEN + ) + ) + self.assertEqual(response.status_code, 401) + + def test_get_token_wrong_user(self): + """Test GET with Knox token and wrong user""" + knox_token = self.get_token(self.user_contributor) + response = self.client.get( + self.url, + **self._get_auth_header(self.user_delegate.username, knox_token) + ) + self.assertEqual(response.status_code, 401)