From 3488eca5440f4900466baeabb53386110faef13e Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 2 Oct 2023 16:45:36 -0700 Subject: [PATCH 1/6] Remove previously used anvil_audit source file Forgot to delete it in the refactoring branch I think. --- anvil_consortium_manager/anvil_audit.py | 264 ------------------------ 1 file changed, 264 deletions(-) delete mode 100644 anvil_consortium_manager/anvil_audit.py diff --git a/anvil_consortium_manager/anvil_audit.py b/anvil_consortium_manager/anvil_audit.py deleted file mode 100644 index 1b1b25ad..00000000 --- a/anvil_consortium_manager/anvil_audit.py +++ /dev/null @@ -1,264 +0,0 @@ -"""Classes handling auditing of information in the Django database against AnVIL.""" - -from abc import ABC, abstractproperty - - -class AnVILAuditResults(ABC): - """Abstract base class to store audit results from AnVIL.""" - - @abstractproperty - def allowed_errors(self): - """List specifying the list of allowed errors for this audit result class.""" - ... - - def __init__(self): - self.verified = set() - self.errors = {} - self.not_in_app = set() - - def add_not_in_app(self, record): - """Add a record that is on ANVIL but is not in the app. - - Args: - record (str): An identifier for the record that is not in the app. - For example, for ManagedGroups, this will be the name of the group on AnVIL. - """ - self.not_in_app.add(record) - - def add_error(self, model_instance, error): - """Add an error for a Django model instance. - - Args: - model_instance (obj): The Django model instance that had a detected error. - error (str): The error that was detected. - - Raises: - ValueError: If the `error` is not in the `allowed_errors` attribute of the class. - """ - if error not in self.allowed_errors: - raise ValueError("'{}' is not an allowed error.".format(error)) - if model_instance in self.verified: - # Should this just remove it from verified and add the error instead? - raise ValueError( - "Cannot add error for model_instance {} that is already verified.".format( - model_instance - ) - ) - if model_instance in self.errors: - self.errors[model_instance].append(error) - else: - self.errors[model_instance] = [error] - - def add_verified(self, model_instance): - """Add a Django model instance that has been verified against AnVIL. - - Args: - model_instance (obj): The Django model instance that was verified. - - Raises: - ValueError: If the Django model instance being added has an error recorded in the `errors` attribute. - """ - if model_instance in self.errors: - raise ValueError("{} has reported errors.".format(model_instance)) - self.verified.add(model_instance) - - def get_verified(self): - """Return a set of the verified records. - - Returns: - set: The set of Django model instances that were verified against AnVIL. - """ - return self.verified - - def get_errors(self): - """Return the errors that were recorded in the audit. - - Returns: - dict: A dictionary of errors. - - The keys of the dictionary are the Django model instances that had errors. - The value for a given element is a list of the errors that were detected for that instance. - """ - return self.errors - - def get_not_in_app(self): - """Return records that are on AnVIL but not in the app. - - Returns: - set: The records that exist on AnVIL but not in the app. - """ - return self.not_in_app - - def ok(self): - """Check if the audit results are ok. - - Returns: - bool: An indicator of whether all audited records were successfully verified. - """ - return not self.errors and not self.not_in_app - - def export( - self, include_verified=True, include_errors=True, include_not_in_app=True - ): - """Return a dictionary representation of the audit results.""" - x = {} - if include_verified: - x["verified"] = [ - {"id": instance.pk, "instance": instance} - for instance in self.get_verified() - ] - if include_errors: - x["errors"] = [ - {"id": k.pk, "instance": k, "errors": v} - for k, v in self.get_errors().items() - ] - if include_not_in_app: - x["not_in_app"] = list(self.get_not_in_app()) - return x - - -class BillingProjectAuditResults(AnVILAuditResults): - """Class to hold audit results for :class:`~anvil_consortium_manager.models.BillingProject`. - - The elements of the set returned by ``get_verified()`` - and the keys of the dictionary returned by ``get_errors()`` - should all be :class:`~anvil_consortium_manager.models.BillingProject` model instances. - """ - - ERROR_NOT_IN_ANVIL = "Not in AnVIL" - """Error when a BillingProject in the app does not exist in AnVIL.""" - - # Set up allowed errors. - allowed_errors = ERROR_NOT_IN_ANVIL - - -class AccountAuditResults(AnVILAuditResults): - """Class to hold audit results for :class:`~anviL_consortium_manager.models.Account`. - - The elements of the set returned by ``get_verified()`` - and the keys of the dictionary returned by ``get_errors()`` - should all be :class:`~anvil_consortium_manager.models.Account` model instances. - """ - - ERROR_NOT_IN_ANVIL = "Not in AnVIL" - """Error when the Account does not exist in AnVIL.""" - - # Set up allowed errors. - allowed_errors = ERROR_NOT_IN_ANVIL - - -class ManagedGroupAuditResults(AnVILAuditResults): - """Class to hold audit results for :class:`~anviL_consortium_manager.models.ManagedGroup`s. - - The elements of the set returned by ``get_verified()`` - and the keys of the dictionary returned by ``get_errors()`` - should are be :class:`~anvil_consortium_manager.models.ManagedGroup` model instances. - """ - - ERROR_NOT_IN_ANVIL = "Not in AnVIL" - """Error when a ManagedGroup in the app does not exist in AnVIL.""" - - ERROR_DIFFERENT_ROLE = "App has a different role in this group" - """Error when the service account running the app has a different role on AnVIL.""" - - ERROR_GROUP_MEMBERSHIP = "Group membership does not match in AnVIL" - """Error when a ManagedGroup has a different record of membership in the app compared to on AnVIL.""" - - # Set up allowed errors. - allowed_errors = ( - ERROR_NOT_IN_ANVIL, - ERROR_DIFFERENT_ROLE, - ERROR_GROUP_MEMBERSHIP, - ) - - -class ManagedGroupMembershipAuditResults(AnVILAuditResults): - """Class to hold audit results for the membership of a model instance of - :class:`~anviL_consortium_manager.models.ManagedGroup`. - - The elements of the set returned by ``get_verified()`` - and the keys of the dictionary returned by ``get_errors()`` - should all be :class:`~anvil_consortium_manager.models.ManagedGroupMembership` model instances. - """ - - ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL = "Account not an admin in AnVIL" - """Error when an Account is an admin of a ManagedGroup on the app, but not in AnVIL.""" - - ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL = "Account not a member in AnVIL" - """Error when an Account is a member of a ManagedGroup on the app, but not in AnVIL.""" - - ERROR_GROUP_ADMIN_NOT_IN_ANVIL = "Group not an admin in AnVIL" - """Error when a ManagedGroup is an admin of another ManagedGroup on the app, but not in AnVIL.""" - - ERROR_GROUP_MEMBER_NOT_IN_ANVIL = "Group not a member in AnVIL" - """Error when an ManagedGroup is a member of another ManagedGroup on the app, but not in AnVIL.""" - - # Set up allowed errors. - allowed_errors = ( - ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL, - ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL, - ERROR_GROUP_ADMIN_NOT_IN_ANVIL, - ERROR_GROUP_MEMBER_NOT_IN_ANVIL, - ) - - -class WorkspaceAuditResults(AnVILAuditResults): - """Class to hold audit results for :class:`~anviL_consortium_manager.models.Workspace`. - - The elements of the set returned by ``get_verified()`` - and the keys of the dictionary returned by ``get_errors()`` - should all be :class:`~anvil_consortium_manager.models.Workspace` model instances. - """ - - ERROR_NOT_IN_ANVIL = "Not in AnVIL" - """Error when a Workspace in the app does not exist on AnVIL.""" - - ERROR_NOT_OWNER_ON_ANVIL = "Not an owner on AnVIL" - """Error when the service account running the app is not an owner of the Workspace on AnVIL.""" - - ERROR_DIFFERENT_AUTH_DOMAINS = "Has different auth domains on AnVIL" - """Error when the Workspace has different auth domains in the app and on AnVIL.""" - - ERROR_WORKSPACE_SHARING = "Workspace sharing does not match on AnVIL" - """Error when a Workspace is shared with different ManagedGroups in the app and on AnVIL.""" - - ERROR_DIFFERENT_LOCK = "Workspace lock status does not match on AnVIL" - """Error when the workspace.is_locked status does not match the lock status on AnVIL.""" - - # Set up allowed errors. - allowed_errors = ( - ERROR_NOT_IN_ANVIL, - ERROR_NOT_OWNER_ON_ANVIL, - ERROR_DIFFERENT_AUTH_DOMAINS, - ERROR_WORKSPACE_SHARING, - ERROR_DIFFERENT_LOCK, - ) - - -class WorkspaceGroupSharingAuditResults(AnVILAuditResults): - """Class to hold audit results for group sharing to :class:`~anviL_consortium_manager.models.Workspace`s. - - The elements of the set returned by ``get_verified()`` - and the keys of the dictionary returned by ``get_errors()`` - should all be :class:`~anvil_consortium_manager.models.WorkspaceGroupSharing` model instances. - """ - - ERROR_NOT_SHARED_IN_ANVIL = "Not shared in AnVIL" - """Error when a ManagedGroup has access to a workspace in the app but not on AnVIL.""" - - ERROR_DIFFERENT_ACCESS = "Different access level in AnVIL" - """Error when a ManagedGroup has a different access level for workspace in the app and on AnVIL.""" - - ERROR_DIFFERENT_CAN_SHARE = "can_share value does not match in AnVIL" - """Error when the can_share value for a ManagedGroup does not match what's on AnVIL.""" - - ERROR_DIFFERENT_CAN_COMPUTE = "can_compute value does not match in AnVIL" - """Error when the can_compute value for a ManagedGroup does not match what's on AnVIL.""" - - # Set up allowed errors. - allowed_errors = ( - ERROR_NOT_SHARED_IN_ANVIL, - ERROR_DIFFERENT_ACCESS, - ERROR_DIFFERENT_CAN_SHARE, - ERROR_DIFFERENT_CAN_COMPUTE, - ) From 75117b7b6f9618039df653858a76ce9df3cd5479 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 2 Oct 2023 16:47:15 -0700 Subject: [PATCH 2/6] Rename audit test source file to match directory name --- .../tests/{test_anvil_audit.py => test_audit.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename anvil_consortium_manager/tests/{test_anvil_audit.py => test_audit.py} (100%) diff --git a/anvil_consortium_manager/tests/test_anvil_audit.py b/anvil_consortium_manager/tests/test_audit.py similarity index 100% rename from anvil_consortium_manager/tests/test_anvil_audit.py rename to anvil_consortium_manager/tests/test_audit.py From ea278b961b379cbc0b5b7b98fc0db8916f45121b Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 2 Oct 2023 16:48:57 -0700 Subject: [PATCH 3/6] Bump version number to release candidate --- CHANGELOG.md | 2 +- anvil_consortium_manager/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab555e4d..a9fb7d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change log -## Devel +## 0.18 (in prep) * Include a workspace_data_object context variable for the `WorkspaceDetail` and `WorkspaceUpdate` views. * Refactor auditing classes. diff --git a/anvil_consortium_manager/__init__.py b/anvil_consortium_manager/__init__.py index 67ce87a1..3bda373d 100644 --- a/anvil_consortium_manager/__init__.py +++ b/anvil_consortium_manager/__init__.py @@ -1 +1 @@ -__version__ = "0.18dev3" +__version__ = "0.18rc1" From 6b345d7b8f05eccc946dd5cadd527b9f98365ae9 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 2 Oct 2023 16:49:40 -0700 Subject: [PATCH 4/6] Add __init__.py to audit directory This allows proper importing. --- anvil_consortium_manager/audit/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 anvil_consortium_manager/audit/__init__.py diff --git a/anvil_consortium_manager/audit/__init__.py b/anvil_consortium_manager/audit/__init__.py new file mode 100644 index 00000000..e69de29b From 29ceabc4e84f5cc3e642f600beb751b97c8dc4f4 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 2 Oct 2023 16:55:56 -0700 Subject: [PATCH 5/6] Add auth model for limited view permission Add (temporary?) tests for the test_func of this mixin until we actually start using it in the app. --- anvil_consortium_manager/auth.py | 17 +++++- anvil_consortium_manager/tests/test_auth.py | 64 +++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 anvil_consortium_manager/tests/test_auth.py diff --git a/anvil_consortium_manager/auth.py b/anvil_consortium_manager/auth.py index 2f1f94a0..0414710e 100644 --- a/anvil_consortium_manager/auth.py +++ b/anvil_consortium_manager/auth.py @@ -1,9 +1,24 @@ -from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin from django.contrib.contenttypes.models import ContentType from .models import AnVILProjectManagerAccess +class AnVILConsortiumManagerLimitedViewRequired(UserPassesTestMixin): + """AnVIL global app limited view permission required mixin. + + This mixin allows anyone with either LIMITED_VIEW or VIEW permission to access a view.""" + + def test_func(self): + apm_content_type = ContentType.objects.get_for_model(AnVILProjectManagerAccess) + perm_1 = f"{apm_content_type.app_label}.{AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME}" + perm_2 = f"{apm_content_type.app_label}.{AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME}" + has_perms = self.request.user.has_perms( + (perm_1,) + ) or self.request.user.has_perms((perm_2,)) + return has_perms + + class AnVILConsortiumManagerViewRequired(PermissionRequiredMixin): """AnVIL global app view permission required mixin""" diff --git a/anvil_consortium_manager/tests/test_auth.py b/anvil_consortium_manager/tests/test_auth.py new file mode 100644 index 00000000..e0ef3d22 --- /dev/null +++ b/anvil_consortium_manager/tests/test_auth.py @@ -0,0 +1,64 @@ +"""Tests for the auth.py source file classes that aren't tested elsewhere.""" +from django.contrib.auth.models import Permission, User +from django.test import RequestFactory, TestCase + +from .. import auth, models + + +class AnVILConsortiumManagerLimitedViewRequiredTest(TestCase): + """(Temporary) class to test the AnVILConsortiumManagerLimitedViewRequired mixin.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + self.user = User.objects.create_user(username="test", password="test") + + def get_view_class(self): + return auth.AnVILConsortiumManagerLimitedViewRequired + + def test_user_with_limited_view_perms(self): + """test_func returns True for a user with limited view permission.""" + self.user.user_permissions.add( + Permission.objects.get( + codename=models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME + ) + ) + inst = self.get_view_class()() + request = self.factory.get("") + request.user = self.user + inst.request = request + self.assertTrue(inst.test_func()) + + def test_user_with_view_perms(self): + """test_func returns True for a user with view permission.""" + self.user.user_permissions.add( + Permission.objects.get( + codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + inst = self.get_view_class()() + request = self.factory.get("") + request.user = self.user + inst.request = request + self.assertTrue(inst.test_func()) + + def test_user_with_edit_perms(self): + """test_func returns False for a user with edit permission.""" + self.user.user_permissions.add( + Permission.objects.get( + codename=models.AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + inst = self.get_view_class()() + request = self.factory.get("") + request.user = self.user + inst.request = request + self.assertFalse(inst.test_func()) + + def test_user_with_no_perms(self): + """test_func returns False for a user with no permissions.""" + inst = self.get_view_class()() + request = self.factory.get("") + request.user = self.user + inst.request = request + self.assertFalse(inst.test_func()) From ecf9266ef689b1262b6b55c069373f0ea2bd5683 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 3 Oct 2023 14:22:37 -0700 Subject: [PATCH 6/6] Bump version number and update CHANGELOG for release --- CHANGELOG.md | 2 +- anvil_consortium_manager/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9fb7d5b..ed6a6798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change log -## 0.18 (in prep) +## 0.18 (2023-10-03) * Include a workspace_data_object context variable for the `WorkspaceDetail` and `WorkspaceUpdate` views. * Refactor auditing classes. diff --git a/anvil_consortium_manager/__init__.py b/anvil_consortium_manager/__init__.py index 3bda373d..a506fc88 100644 --- a/anvil_consortium_manager/__init__.py +++ b/anvil_consortium_manager/__init__.py @@ -1 +1 @@ -__version__ = "0.18rc1" +__version__ = "0.18"