From 1f15ded42358690fcd2c6e895144e69d3d5d702a Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 1 Jul 2024 14:36:32 -0700 Subject: [PATCH 01/16] Remove debugging print statement in template --- primed/templates/users/user_detail.html | 1 - 1 file changed, 1 deletion(-) diff --git a/primed/templates/users/user_detail.html b/primed/templates/users/user_detail.html index 0261598e..75163ead 100644 --- a/primed/templates/users/user_detail.html +++ b/primed/templates/users/user_detail.html @@ -155,5 +155,4 @@

{% if object == request.user %}My{% el -

{{ signed_agreements }}

{% endblock content %} From 1d7dd859e845acb46c884c93415fdc016f5c0e79 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 14:56:44 -0700 Subject: [PATCH 02/16] Use temporary version of ACM in requirements file This is the version of ACM that has custom workspace adapter methods implemented, and I'd like to test it out before releasing the new version of the package. CI will fail because the version in the requirements.in file does not have the version in requirements.txt. This is good, because we need to update requirements.in with the official released version once it's ready. --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 57feb16a..ed9899d1 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -62,7 +62,7 @@ django==4.2.13 # django-tables2 django-allauth==0.54.0 # via -r requirements/requirements.in -django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.23 +django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@237e18bb998f258af6959245ea2c307d59cedbe2 # via -r requirements/requirements.in django-autocomplete-light==3.11.0 # via django-anvil-consortium-manager From 54c5ddba49d3cbc7f1cafb0ab11faa5b06e3fecb Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 15:15:15 -0700 Subject: [PATCH 03/16] Add a custom WorkspaceForm with auth domain disabled This will be used in the adapters where we are going to create our own auth domains. --- primed/primed_anvil/forms.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/primed/primed_anvil/forms.py b/primed/primed_anvil/forms.py index d7163fe0..91e1dfba 100644 --- a/primed/primed_anvil/forms.py +++ b/primed/primed_anvil/forms.py @@ -1,3 +1,4 @@ +from anvil_consortium_manager.forms import WorkspaceForm from django import forms @@ -5,3 +6,14 @@ class CustomDateInput(forms.widgets.DateInput): """Form widget to select a date with a calendar picker.""" input_type = "date" + + +class WorkspaceAuthDomainDisabledForm(WorkspaceForm): + """Form for creating a workspace with the authorization domains field disabled.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["authorization_domains"].disabled = True + self.fields["authorization_domains"].help_text = ( + "An authorization domain will be automatically created " "using the name of the workspace." + ) From bbfe2056e3bca6aa1c7c01096bbb8d2cdc29f1a6 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 15:16:14 -0700 Subject: [PATCH 04/16] Add skeleton methods to CDSAWorkspace adapter These will be filled in in future commits. --- primed/cdsa/adapters.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/primed/cdsa/adapters.py b/primed/cdsa/adapters.py index 78e6f169..3601757c 100644 --- a/primed/cdsa/adapters.py +++ b/primed/cdsa/adapters.py @@ -32,3 +32,17 @@ def get_extra_detail_context_data(self, workspace, request): extra_context["primary_cdsa"] = None return extra_context + + def before_workspace_create(self, workspace): + # Create the auth domain for the workspace. + # Add the ADMINs group as an admin of the auth domain. + pass + + def after_workspace_create(self, workspace): + # Add the ADMINs group as an owner of the workspace. + pass + + def after_workspace_import(self, workspace): + # Add the ADMINs group as an owner of the workspace, if it's not already. + # Make sure to test for the case where it is already an owner of the workspace. + pass From a8a09e63a857148e0d7d47c41a97641cbfbf121d Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 15:57:50 -0700 Subject: [PATCH 05/16] Automate some CDSA workspace creation tasks Create an auth domain based on the name of the workspace. Add the admins group as an admin of the auth domain. Share the workspace with the auth domain as an owner. --- primed/cdsa/adapters.py | 38 +++++++--- primed/cdsa/tests/test_views.py | 122 +++++++++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 11 deletions(-) diff --git a/primed/cdsa/adapters.py b/primed/cdsa/adapters.py index 3601757c..577d2b22 100644 --- a/primed/cdsa/adapters.py +++ b/primed/cdsa/adapters.py @@ -1,8 +1,9 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter -from anvil_consortium_manager.forms import WorkspaceForm -from anvil_consortium_manager.models import Workspace +from anvil_consortium_manager.models import GroupGroupMembership, ManagedGroup, Workspace, WorkspaceGroupSharing +from django.conf import settings from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable +from primed.primed_anvil.forms import WorkspaceAuthDomainDisabledForm from . import forms, models, tables @@ -15,7 +16,7 @@ class CDSAWorkspaceAdapter(BaseWorkspaceAdapter): description = "Workspaces containing data from the Consortium Data Sharing Agreement" list_table_class_staff_view = tables.CDSAWorkspaceStaffTable list_table_class_view = tables.CDSAWorkspaceUserTable - workspace_form_class = WorkspaceForm + workspace_form_class = WorkspaceAuthDomainDisabledForm workspace_data_model = models.CDSAWorkspace workspace_data_form_class = forms.CDSAWorkspaceForm workspace_detail_template_name = "cdsa/cdsaworkspace_detail.html" @@ -35,14 +36,31 @@ def get_extra_detail_context_data(self, workspace, request): def before_workspace_create(self, workspace): # Create the auth domain for the workspace. + """Add authorization domain to workspace.""" + auth_domain_name = "AUTH_" + workspace.name + auth_domain = ManagedGroup.objects.create( + name=auth_domain_name, + is_managed_by_app=True, + email=auth_domain_name + "@firecloud.org", + ) + workspace.authorization_domains.add(auth_domain) + auth_domain.anvil_create() # Add the ADMINs group as an admin of the auth domain. - pass + admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + membership = GroupGroupMembership.objects.create( + parent_group=auth_domain, + child_group=admins_group, + role=GroupGroupMembership.ADMIN, + ) + membership.anvil_create() def after_workspace_create(self, workspace): # Add the ADMINs group as an owner of the workspace. - pass - - def after_workspace_import(self, workspace): - # Add the ADMINs group as an owner of the workspace, if it's not already. - # Make sure to test for the case where it is already an owner of the workspace. - pass + admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + sharing = WorkspaceGroupSharing.objects.create( + workspace=workspace, + group=admins_group, + access=WorkspaceGroupSharing.OWNER, + can_compute=True, + ) + sharing.anvil_create_or_update() diff --git a/primed/cdsa/tests/test_views.py b/primed/cdsa/tests/test_views.py index 6015197d..2cb3efce 100644 --- a/primed/cdsa/tests/test_views.py +++ b/primed/cdsa/tests/test_views.py @@ -9,6 +9,7 @@ GroupGroupMembership, ManagedGroup, Workspace, + WorkspaceGroupSharing, ) from anvil_consortium_manager.tests.api_factories import ErrorResponseFactory from anvil_consortium_manager.tests.factories import ( @@ -10262,23 +10263,27 @@ def setUp(self): ) self.requester = UserFactory.create() self.workspace_type = "cdsa" + # Create the admins group. + ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) def get_url(self, *args): """Get the url for the view being tested.""" return reverse("anvil_consortium_manager:workspaces:new", args=args) - def test_creates_upload_workspace_without_duos(self): + def test_creates_workspace(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" study = factories.StudyFactory.create() duo_permission = DataUsePermissionFactory.create() # Create an extra that won't be specified. DataUseModifierFactory.create() billing_project = BillingProjectFactory.create(name="test-billing-project") + # API response for workspace creation. url = self.api_client.rawls_entry_point + "/api/workspaces" json_data = { "namespace": "test-billing-project", "name": "test-workspace", "attributes": {}, + "authorizationDomain": [{"membersGroupName": "AUTH_test-workspace"}], } self.anvil_response_mock.add( responses.POST, @@ -10286,6 +10291,37 @@ def test_creates_upload_workspace_without_duos(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_test-workspace", + status=self.api_success_code, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/AUTH_test-workspace/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -10315,6 +10351,24 @@ def test_creates_upload_workspace_without_duos(self): self.assertEqual(new_workspace_data.data_use_permission, duo_permission) self.assertEqual(new_workspace_data.acknowledgments, "test acknowledgments") self.assertEqual(new_workspace_data.requested_by, self.requester) + # Check that auth domain exists. + self.assertEqual(new_workspace.authorization_domains.count(), 1) + auth_domain = new_workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_test-workspace") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_test-workspace@firecloud.org") + # Check that auth domain admin is correct. + membership = GroupGroupMembership.objects.get( + parent_group=auth_domain, child_group__name="TEST_PRIMED_CC_ADMINS" + ) + self.assertEqual(membership.role, membership.ADMIN) + # Check that workspace sharing is correct. + sharing = WorkspaceGroupSharing.objects.get( + workspace=new_workspace, + group__name="TEST_PRIMED_CC_ADMINS", + ) + self.assertEqual(sharing.access, sharing.OWNER) + self.assertEqual(sharing.can_compute, True) def test_creates_upload_workspace_with_duo_modifiers(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" @@ -10325,11 +10379,13 @@ def test_creates_upload_workspace_with_duo_modifiers(self): # Create an extra that won't be specified. DataUseModifierFactory.create() billing_project = BillingProjectFactory.create(name="test-billing-project") + # API response for workspace creation. url = self.api_client.rawls_entry_point + "/api/workspaces" json_data = { "namespace": "test-billing-project", "name": "test-workspace", "attributes": {}, + "authorizationDomain": [{"membersGroupName": "AUTH_test-workspace"}], } self.anvil_response_mock.add( responses.POST, @@ -10337,6 +10393,37 @@ def test_creates_upload_workspace_with_duo_modifiers(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_test-workspace", + status=self.api_success_code, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/AUTH_test-workspace/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -10371,11 +10458,13 @@ def test_creates_upload_workspace_with_disease_term(self): data_use_permission = DataUsePermissionFactory.create(requires_disease_term=True) # Create an extra that won't be specified. billing_project = BillingProjectFactory.create(name="test-billing-project") + # API response for workspace creation. url = self.api_client.rawls_entry_point + "/api/workspaces" json_data = { "namespace": "test-billing-project", "name": "test-workspace", "attributes": {}, + "authorizationDomain": [{"membersGroupName": "AUTH_test-workspace"}], } self.anvil_response_mock.add( responses.POST, @@ -10383,6 +10472,37 @@ def test_creates_upload_workspace_with_disease_term(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_test-workspace", + status=self.api_success_code, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/AUTH_test-workspace/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), From fe364632aeb34901438506b03b6ea7df9911e679 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 16:42:36 -0700 Subject: [PATCH 06/16] Add tests specifically for the adapter methods Instead of just testing everything through the view, add specific tests for the adapter methods. --- primed/cdsa/adapters.py | 2 +- primed/cdsa/tests/test_adapters.py | 146 +++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 primed/cdsa/tests/test_adapters.py diff --git a/primed/cdsa/adapters.py b/primed/cdsa/adapters.py index 577d2b22..7e8ffb76 100644 --- a/primed/cdsa/adapters.py +++ b/primed/cdsa/adapters.py @@ -55,7 +55,7 @@ def before_workspace_create(self, workspace): membership.anvil_create() def after_workspace_create(self, workspace): - # Add the ADMINs group as an owner of the workspace. + # Share the workspace with the ADMINs group as an owner. admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) sharing = WorkspaceGroupSharing.objects.create( workspace=workspace, diff --git a/primed/cdsa/tests/test_adapters.py b/primed/cdsa/tests/test_adapters.py new file mode 100644 index 00000000..d564e62d --- /dev/null +++ b/primed/cdsa/tests/test_adapters.py @@ -0,0 +1,146 @@ +import responses +from anvil_consortium_manager.models import ( + GroupGroupMembership, + WorkspaceGroupSharing, +) +from anvil_consortium_manager.tests.factories import ( + ManagedGroupFactory, + WorkspaceFactory, +) +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin +from django.conf import settings +from django.test import TestCase, override_settings + +from .. import adapters +from . import factories + + +class CDSAWorkspaceAdapterTest(AnVILAPIMockTestMixin, TestCase): + """Tests for methods in the CDSAWorkspaceAdapter.""" + + def setUp(self): + super().setUp() + self.adapter = adapters.CDSAWorkspaceAdapter() + # Create the admins group. + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + + def test_before_anvil_create(self): + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # Run the adapter method. + self.adapter.before_workspace_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # Check for GroupGroupMembership. + self.assertEqual(GroupGroupMembership.objects.count(), 1) + membership = GroupGroupMembership.objects.first() + self.assertEqual(membership.parent_group, auth_domain) + self.assertEqual(membership.child_group, self.admins_group) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") + def test_before_anvil_create_different_cc_admins_name(self): + admins_group = ManagedGroupFactory.create(name="foobar") + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/foobar@firecloud.org", + status=204, + ) + # Run the adapter method. + self.adapter.before_workspace_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # Check for GroupGroupMembership. + self.assertEqual(GroupGroupMembership.objects.count(), 1) + membership = GroupGroupMembership.objects.first() + self.assertEqual(membership.parent_group, auth_domain) + self.assertEqual(membership.child_group, admins_group) + + def test_after_workspace_create(self): + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + cdsa_workspace = factories.CDSAWorkspaceFactory.create( + workspace__billing_project__name="bar", workspace__name="foo" + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Run the adapter method. + self.adapter.after_workspace_create(cdsa_workspace.workspace) + # Check for WorkspaceGroupSharing. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) + sharing = WorkspaceGroupSharing.objects.first() + self.assertEqual(sharing.workspace, cdsa_workspace.workspace) + self.assertEqual(sharing.group, self.admins_group) + self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) + self.assertTrue(sharing.can_compute) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") + def test_after_workspace_create_different_admins_group(self): + admins_group = ManagedGroupFactory.create(name="foobar") + cdsa_workspace = factories.CDSAWorkspaceFactory.create( + workspace__billing_project__name="bar", workspace__name="foo" + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "foobar@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Run the adapter method. + self.adapter.after_workspace_create(cdsa_workspace.workspace) + # Check for WorkspaceGroupSharing. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) + sharing = WorkspaceGroupSharing.objects.first() + self.assertEqual(sharing.workspace, cdsa_workspace.workspace) + self.assertEqual(sharing.group, admins_group) + self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) + self.assertTrue(sharing.can_compute) From ca3c00c532c5bbd3f2f0985796f562366c62372c Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 16:46:02 -0700 Subject: [PATCH 07/16] Update adapter method names based on new ACM changes The adapter method names chnaged from eg before_workspace_create to before_anvil_create. Update the CDSA code to use those names. --- primed/cdsa/adapters.py | 4 ++-- primed/cdsa/tests/test_adapters.py | 12 ++++++------ requirements/requirements.txt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/primed/cdsa/adapters.py b/primed/cdsa/adapters.py index 7e8ffb76..fe9f404e 100644 --- a/primed/cdsa/adapters.py +++ b/primed/cdsa/adapters.py @@ -34,7 +34,7 @@ def get_extra_detail_context_data(self, workspace, request): return extra_context - def before_workspace_create(self, workspace): + def before_anvil_create(self, workspace): # Create the auth domain for the workspace. """Add authorization domain to workspace.""" auth_domain_name = "AUTH_" + workspace.name @@ -54,7 +54,7 @@ def before_workspace_create(self, workspace): ) membership.anvil_create() - def after_workspace_create(self, workspace): + def after_anvil_create(self, workspace): # Share the workspace with the ADMINs group as an owner. admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) sharing = WorkspaceGroupSharing.objects.create( diff --git a/primed/cdsa/tests/test_adapters.py b/primed/cdsa/tests/test_adapters.py index d564e62d..6350864b 100644 --- a/primed/cdsa/tests/test_adapters.py +++ b/primed/cdsa/tests/test_adapters.py @@ -40,7 +40,7 @@ def test_before_anvil_create(self): status=204, ) # Run the adapter method. - self.adapter.before_workspace_create(workspace) + self.adapter.before_anvil_create(workspace) self.assertEqual(workspace.authorization_domains.count(), 1) auth_domain = workspace.authorization_domains.first() self.assertEqual(auth_domain.name, "AUTH_foo") @@ -70,7 +70,7 @@ def test_before_anvil_create_different_cc_admins_name(self): status=204, ) # Run the adapter method. - self.adapter.before_workspace_create(workspace) + self.adapter.before_anvil_create(workspace) self.assertEqual(workspace.authorization_domains.count(), 1) auth_domain = workspace.authorization_domains.first() self.assertEqual(auth_domain.name, "AUTH_foo") @@ -82,7 +82,7 @@ def test_before_anvil_create_different_cc_admins_name(self): self.assertEqual(membership.parent_group, auth_domain) self.assertEqual(membership.child_group, admins_group) - def test_after_workspace_create(self): + def test_after_anvil_create(self): # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. cdsa_workspace = factories.CDSAWorkspaceFactory.create( workspace__billing_project__name="bar", workspace__name="foo" @@ -104,7 +104,7 @@ def test_after_workspace_create(self): json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, ) # Run the adapter method. - self.adapter.after_workspace_create(cdsa_workspace.workspace) + self.adapter.after_anvil_create(cdsa_workspace.workspace) # Check for WorkspaceGroupSharing. self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) sharing = WorkspaceGroupSharing.objects.first() @@ -114,7 +114,7 @@ def test_after_workspace_create(self): self.assertTrue(sharing.can_compute) @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") - def test_after_workspace_create_different_admins_group(self): + def test_after_anvil_create_different_admins_group(self): admins_group = ManagedGroupFactory.create(name="foobar") cdsa_workspace = factories.CDSAWorkspaceFactory.create( workspace__billing_project__name="bar", workspace__name="foo" @@ -136,7 +136,7 @@ def test_after_workspace_create_different_admins_group(self): json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, ) # Run the adapter method. - self.adapter.after_workspace_create(cdsa_workspace.workspace) + self.adapter.after_anvil_create(cdsa_workspace.workspace) # Check for WorkspaceGroupSharing. self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) sharing = WorkspaceGroupSharing.objects.first() diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ed9899d1..1a27fac3 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -62,7 +62,7 @@ django==4.2.13 # django-tables2 django-allauth==0.54.0 # via -r requirements/requirements.in -django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@237e18bb998f258af6959245ea2c307d59cedbe2 +django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@cd6ea592f7b5b324fc3dad84b1dfb48eebb26456 # via -r requirements/requirements.in django-autocomplete-light==3.11.0 # via django-anvil-consortium-manager From b6bef3bcae9785c1e609b3f272af6fe8e7edfa40 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 16:57:07 -0700 Subject: [PATCH 08/16] Split adapter behavior into reusable mixins We'll want to use the methods that create the uath domain and that share a workspace with the admins elsewhere, so move the code into a set of resuable classes that can be inherited in the workspae adapters. --- primed/cdsa/adapters.py | 37 +++----------------------- primed/primed_anvil/adapters.py | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/primed/cdsa/adapters.py b/primed/cdsa/adapters.py index fe9f404e..18c20846 100644 --- a/primed/cdsa/adapters.py +++ b/primed/cdsa/adapters.py @@ -1,14 +1,14 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter -from anvil_consortium_manager.models import GroupGroupMembership, ManagedGroup, Workspace, WorkspaceGroupSharing -from django.conf import settings +from anvil_consortium_manager.models import Workspace from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable +from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter, WorkspaceAuthDomainAdapter from primed.primed_anvil.forms import WorkspaceAuthDomainDisabledForm from . import forms, models, tables -class CDSAWorkspaceAdapter(BaseWorkspaceAdapter): +class CDSAWorkspaceAdapter(WorkspaceAuthDomainAdapter, WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): """Adapter for CDSAWorkspaces.""" type = "cdsa" @@ -33,34 +33,3 @@ def get_extra_detail_context_data(self, workspace, request): extra_context["primary_cdsa"] = None return extra_context - - def before_anvil_create(self, workspace): - # Create the auth domain for the workspace. - """Add authorization domain to workspace.""" - auth_domain_name = "AUTH_" + workspace.name - auth_domain = ManagedGroup.objects.create( - name=auth_domain_name, - is_managed_by_app=True, - email=auth_domain_name + "@firecloud.org", - ) - workspace.authorization_domains.add(auth_domain) - auth_domain.anvil_create() - # Add the ADMINs group as an admin of the auth domain. - admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) - membership = GroupGroupMembership.objects.create( - parent_group=auth_domain, - child_group=admins_group, - role=GroupGroupMembership.ADMIN, - ) - membership.anvil_create() - - def after_anvil_create(self, workspace): - # Share the workspace with the ADMINs group as an owner. - admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) - sharing = WorkspaceGroupSharing.objects.create( - workspace=workspace, - group=admins_group, - access=WorkspaceGroupSharing.OWNER, - can_compute=True, - ) - sharing.anvil_create_or_update() diff --git a/primed/primed_anvil/adapters.py b/primed/primed_anvil/adapters.py index f62a4593..931f9b07 100644 --- a/primed/primed_anvil/adapters.py +++ b/primed/primed_anvil/adapters.py @@ -1,4 +1,10 @@ from anvil_consortium_manager.adapters.account import BaseAccountAdapter +from anvil_consortium_manager.models import ( + GroupGroupMembership, + ManagedGroup, + WorkspaceGroupSharing, +) +from django.conf import settings from django.db.models import Q from .filters import AccountListFilter @@ -24,3 +30,44 @@ def get_autocomplete_label(self, account): else: name = "---" return "{} ({})".format(name, account.email) + + +class WorkspaceAuthDomainAdapter: + """Helper class to add auth domains to workspaces.""" + + def before_anvil_create(self, workspace): + """Add authorization domain to workspace.""" + # Create the auth domain for the workspace. + super().before_anvil_create(workspace) + auth_domain_name = "AUTH_" + workspace.name + auth_domain = ManagedGroup.objects.create( + name=auth_domain_name, + is_managed_by_app=True, + email=auth_domain_name + "@firecloud.org", + ) + workspace.authorization_domains.add(auth_domain) + auth_domain.anvil_create() + # Add the ADMINs group as an admin of the auth domain. + admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + membership = GroupGroupMembership.objects.create( + parent_group=auth_domain, + child_group=admins_group, + role=GroupGroupMembership.ADMIN, + ) + membership.anvil_create() + + +class WorkspaceAdminSharingAdapter: + """Helper class to share workspaces with the PRIMED_CC_ADMINs group.""" + + def after_anvil_create(self, workspace): + super().after_anvil_create(workspace) + # Share the workspace with the ADMINs group as an owner. + admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + sharing = WorkspaceGroupSharing.objects.create( + workspace=workspace, + group=admins_group, + access=WorkspaceGroupSharing.OWNER, + can_compute=True, + ) + sharing.anvil_create_or_update() From 4f6faf97862d62e541d57dedf43245f3fc72c3e5 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 3 Jul 2024 08:53:10 -0700 Subject: [PATCH 09/16] Use new adapter behaviors for dbGaPWorkspaces Add the two new adapter mixins to the dbGaPWorkspaceAdapter. This means that a dbGaPWorkspace will get an auth domain automatically created, the admins group will be added to it, and the workspace will be shared with the admins group as an owner. --- primed/dbgap/adapters.py | 3 +- primed/dbgap/tests/test_adapters.py | 145 ++++++++++++++++++++++++++++ primed/dbgap/tests/test_views.py | 85 +++++++++++++++- 3 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 primed/dbgap/tests/test_adapters.py diff --git a/primed/dbgap/adapters.py b/primed/dbgap/adapters.py index d1eaf728..d42b7157 100644 --- a/primed/dbgap/adapters.py +++ b/primed/dbgap/adapters.py @@ -3,11 +3,12 @@ from anvil_consortium_manager.models import Workspace from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable +from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter, WorkspaceAuthDomainAdapter from . import forms, models, tables -class dbGaPWorkspaceAdapter(BaseWorkspaceAdapter): +class dbGaPWorkspaceAdapter(WorkspaceAuthDomainAdapter, WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): """Adapter for dbGaPWorkspaces.""" type = "dbgap" diff --git a/primed/dbgap/tests/test_adapters.py b/primed/dbgap/tests/test_adapters.py new file mode 100644 index 00000000..fd091bf5 --- /dev/null +++ b/primed/dbgap/tests/test_adapters.py @@ -0,0 +1,145 @@ +import responses +from anvil_consortium_manager.models import ( + GroupGroupMembership, + WorkspaceGroupSharing, +) +from anvil_consortium_manager.tests.factories import ( + ManagedGroupFactory, + WorkspaceFactory, +) +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin +from django.conf import settings +from django.test import TestCase, override_settings + +from .. import adapters +from . import factories + + +class dbGaPWorkspaceAdapterTest(AnVILAPIMockTestMixin, TestCase): + """Tests for methods in the dbGaPWorkspaceAdapter.""" + + def setUp(self): + super().setUp() + self.adapter = adapters.dbGaPWorkspaceAdapter() + # Create the admins group. + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + + def test_before_anvil_create(self): + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # Run the adapter method. + self.adapter.before_anvil_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # Check for GroupGroupMembership. + self.assertEqual(GroupGroupMembership.objects.count(), 1) + membership = GroupGroupMembership.objects.first() + self.assertEqual(membership.parent_group, auth_domain) + self.assertEqual(membership.child_group, self.admins_group) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") + def test_before_anvil_create_different_cc_admins_name(self): + admins_group = ManagedGroupFactory.create(name="foobar") + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/foobar@firecloud.org", + status=204, + ) + # Run the adapter method. + self.adapter.before_anvil_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # Check for GroupGroupMembership. + self.assertEqual(GroupGroupMembership.objects.count(), 1) + membership = GroupGroupMembership.objects.first() + self.assertEqual(membership.parent_group, auth_domain) + self.assertEqual(membership.child_group, admins_group) + + def test_after_anvil_create(self): + dbgap_workspace = factories.dbGaPWorkspaceFactory.create( + workspace__billing_project__name="bar", workspace__name="foo" + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Run the adapter method. + self.adapter.after_anvil_create(dbgap_workspace.workspace) + # Check for WorkspaceGroupSharing. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) + sharing = WorkspaceGroupSharing.objects.first() + self.assertEqual(sharing.workspace, dbgap_workspace.workspace) + self.assertEqual(sharing.group, self.admins_group) + self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) + self.assertTrue(sharing.can_compute) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") + def test_after_anvil_create_different_admins_group(self): + admins_group = ManagedGroupFactory.create(name="foobar") + dbgap_workspace = factories.dbGaPWorkspaceFactory.create( + workspace__billing_project__name="bar", workspace__name="foo" + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "foobar@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Run the adapter method. + self.adapter.after_anvil_create(dbgap_workspace.workspace) + # Check for WorkspaceGroupSharing. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) + sharing = WorkspaceGroupSharing.objects.first() + self.assertEqual(sharing.workspace, dbgap_workspace.workspace) + self.assertEqual(sharing.group, admins_group) + self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) + self.assertTrue(sharing.can_compute) diff --git a/primed/dbgap/tests/test_views.py b/primed/dbgap/tests/test_views.py index dad3f2f3..79e06122 100644 --- a/primed/dbgap/tests/test_views.py +++ b/primed/dbgap/tests/test_views.py @@ -11,6 +11,7 @@ GroupGroupMembership, ManagedGroup, Workspace, + WorkspaceGroupSharing, ) from anvil_consortium_manager.tests.api_factories import ErrorResponseFactory from anvil_consortium_manager.tests.factories import ( @@ -947,6 +948,8 @@ def setUp(self): ) self.requester = UserFactory.create() self.workspace_type = "dbgap" + # Create the admins group. + self.admins_group = ManagedGroupFactory.create(name="TEST_PRIMED_CC_ADMINS") def get_url(self, *args): """Get the url for the view being tested.""" @@ -956,17 +959,19 @@ def get_api_url(self, billing_project_name, workspace_name): """Return the Terra API url for a given billing project and workspace.""" return self.api_client.rawls_entry_point + "/api/workspaces/" + billing_project_name + "/" + workspace_name - def test_creates_upload_workspace_without_duos(self): + def test_creates_workspace_without_duos(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" dbgap_study_accession = factories.dbGaPStudyAccessionFactory.create() # Create an extra that won't be specified. DataUseModifierFactory.create() billing_project = BillingProjectFactory.create(name="test-billing-project") + # API response for workspace creation. url = self.api_client.rawls_entry_point + "/api/workspaces" json_data = { "namespace": "test-billing-project", "name": "test-workspace", "attributes": {}, + "authorizationDomain": [{"membersGroupName": "AUTH_test-workspace"}], } self.anvil_response_mock.add( responses.POST, @@ -974,6 +979,37 @@ def test_creates_upload_workspace_without_duos(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_test-workspace", + status=self.api_success_code, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/AUTH_test-workspace/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -1011,8 +1047,20 @@ def test_creates_upload_workspace_without_duos(self): self.assertEqual(new_workspace_data.data_use_limitations, "test limitations") self.assertEqual(new_workspace_data.acknowledgments, "test acknowledgments") self.assertEqual(new_workspace_data.requested_by, self.requester) + # Check that an auth domain was created. + self.assertEqual(new_workspace.authorization_domains.count(), 1) + self.assertEqual(new_workspace.authorization_domains.first().name, "AUTH_test-workspace") + # Check that the PRIMED_ADMINS group is an admin of the auth domain. + membership = GroupGroupMembership.objects.get( + parent_group=new_workspace.authorization_domains.first(), + child_group=self.admins_group, + ) + self.assertEqual(membership.role, membership.ADMIN) + # Check that the workspace was shared with the admins group. + sharing = WorkspaceGroupSharing.objects.get(workspace=new_workspace, group=self.admins_group) + self.assertEqual(sharing.access, sharing.OWNER) - def test_creates_upload_workspace_with_duos(self): + def test_creates_workspace_with_duos(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" dbgap_study_accession = factories.dbGaPStudyAccessionFactory.create() data_use_permission = DataUsePermissionFactory.create() @@ -1021,11 +1069,13 @@ def test_creates_upload_workspace_with_duos(self): # Create an extra that won't be specified. DataUseModifierFactory.create() billing_project = BillingProjectFactory.create(name="test-billing-project") + # API response for workspace creation. url = self.api_client.rawls_entry_point + "/api/workspaces" json_data = { "namespace": "test-billing-project", "name": "test-workspace", "attributes": {}, + "authorizationDomain": [{"membersGroupName": "AUTH_test-workspace"}], } self.anvil_response_mock.add( responses.POST, @@ -1033,6 +1083,37 @@ def test_creates_upload_workspace_with_duos(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_test-workspace", + status=self.api_success_code, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/AUTH_test-workspace/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), From e89f8dd6d46812fb5b2596f8deee4cbc8d68cdae Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 3 Jul 2024 09:03:47 -0700 Subject: [PATCH 10/16] Use the adapter behaviors for CollaborativeAnalysisWorkspaces Automatically create an auth domain, add the admins group to the auth domain, and share the workspace with the admins group. --- primed/collaborative_analysis/adapters.py | 8 +- .../tests/test_adapters.py | 146 ++++++++++++++++++ .../tests/test_views.py | 66 ++++++++ 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 primed/collaborative_analysis/tests/test_adapters.py diff --git a/primed/collaborative_analysis/adapters.py b/primed/collaborative_analysis/adapters.py index f070d926..7fa62cf8 100644 --- a/primed/collaborative_analysis/adapters.py +++ b/primed/collaborative_analysis/adapters.py @@ -1,10 +1,16 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter from anvil_consortium_manager.forms import WorkspaceForm +from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter, WorkspaceAuthDomainAdapter + from . import forms, models, tables -class CollaborativeAnalysisWorkspaceAdapter(BaseWorkspaceAdapter): +class CollaborativeAnalysisWorkspaceAdapter( + WorkspaceAuthDomainAdapter, + WorkspaceAdminSharingAdapter, + BaseWorkspaceAdapter, +): """Adapter for CollaborativeAnalysisWorkspace.""" type = "collab_analysis" diff --git a/primed/collaborative_analysis/tests/test_adapters.py b/primed/collaborative_analysis/tests/test_adapters.py new file mode 100644 index 00000000..540d7502 --- /dev/null +++ b/primed/collaborative_analysis/tests/test_adapters.py @@ -0,0 +1,146 @@ +import responses +from anvil_consortium_manager.models import ( + GroupGroupMembership, + WorkspaceGroupSharing, +) +from anvil_consortium_manager.tests.factories import ( + ManagedGroupFactory, + WorkspaceFactory, +) +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin +from django.conf import settings +from django.test import TestCase, override_settings + +from .. import adapters +from . import factories + + +class CollaborativeAnalysisWorkspaceAdapterTest(AnVILAPIMockTestMixin, TestCase): + """Tests for methods in the CollaborativeAnalysisWorkspaceAdapter.""" + + def setUp(self): + super().setUp() + self.adapter = adapters.CollaborativeAnalysisWorkspaceAdapter() + # Create the admins group. + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + + def test_before_anvil_create(self): + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # Run the adapter method. + self.adapter.before_anvil_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # Check for GroupGroupMembership. + self.assertEqual(GroupGroupMembership.objects.count(), 1) + membership = GroupGroupMembership.objects.first() + self.assertEqual(membership.parent_group, auth_domain) + self.assertEqual(membership.child_group, self.admins_group) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") + def test_before_anvil_create_different_cc_admins_name(self): + admins_group = ManagedGroupFactory.create(name="foobar") + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/foobar@firecloud.org", + status=204, + ) + # Run the adapter method. + self.adapter.before_anvil_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # Check for GroupGroupMembership. + self.assertEqual(GroupGroupMembership.objects.count(), 1) + membership = GroupGroupMembership.objects.first() + self.assertEqual(membership.parent_group, auth_domain) + self.assertEqual(membership.child_group, admins_group) + + def test_after_anvil_create(self): + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + collab_workspace = factories.CollaborativeAnalysisWorkspaceFactory.create( + workspace__billing_project__name="bar", workspace__name="foo" + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Run the adapter method. + self.adapter.after_anvil_create(collab_workspace.workspace) + # Check for WorkspaceGroupSharing. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) + sharing = WorkspaceGroupSharing.objects.first() + self.assertEqual(sharing.workspace, collab_workspace.workspace) + self.assertEqual(sharing.group, self.admins_group) + self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) + self.assertTrue(sharing.can_compute) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") + def test_after_anvil_create_different_admins_group(self): + admins_group = ManagedGroupFactory.create(name="foobar") + collab_workspace = factories.CollaborativeAnalysisWorkspaceFactory.create( + workspace__billing_project__name="bar", workspace__name="foo" + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "foobar@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Run the adapter method. + self.adapter.after_anvil_create(collab_workspace.workspace) + # Check for WorkspaceGroupSharing. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) + sharing = WorkspaceGroupSharing.objects.first() + self.assertEqual(sharing.workspace, collab_workspace.workspace) + self.assertEqual(sharing.group, admins_group) + self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) + self.assertTrue(sharing.can_compute) diff --git a/primed/collaborative_analysis/tests/test_views.py b/primed/collaborative_analysis/tests/test_views.py index 36327a31..e057e984 100644 --- a/primed/collaborative_analysis/tests/test_views.py +++ b/primed/collaborative_analysis/tests/test_views.py @@ -8,6 +8,7 @@ GroupAccountMembership, GroupGroupMembership, Workspace, + WorkspaceGroupSharing, ) from anvil_consortium_manager.tests.api_factories import ErrorResponseFactory from anvil_consortium_manager.tests.factories import ( @@ -186,6 +187,8 @@ def setUp(self): self.custodian = UserFactory.create() self.source_workspace = dbGaPWorkspaceFactory.create().workspace self.analyst_group = ManagedGroupFactory.create() + # Create the admins group. + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) def get_url(self, *args): """Get the url for the view being tested.""" @@ -194,11 +197,25 @@ def get_url(self, *args): def test_creates_workspace(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" billing_project = BillingProjectFactory.create(name="test-billing-project") + # url = self.api_client.rawls_entry_point + "/api/workspaces" + # json_data = { + # "namespace": "test-billing-project", + # "name": "test-workspace", + # "attributes": {}, + # } + # self.anvil_response_mock.add( + # responses.POST, + # url, + # status=self.api_success_code, + # match=[responses.matchers.json_params_matcher(json_data)], + # ) + # API response for workspace creation. url = self.api_client.rawls_entry_point + "/api/workspaces" json_data = { "namespace": "test-billing-project", "name": "test-workspace", "attributes": {}, + "authorizationDomain": [{"membersGroupName": "AUTH_test-workspace"}], } self.anvil_response_mock.add( responses.POST, @@ -206,6 +223,37 @@ def test_creates_workspace(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_test-workspace", + status=self.api_success_code, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/AUTH_test-workspace/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -230,6 +278,24 @@ def test_creates_workspace(self): self.assertEqual(models.CollaborativeAnalysisWorkspace.objects.count(), 1) new_workspace_data = models.CollaborativeAnalysisWorkspace.objects.latest("pk") self.assertEqual(new_workspace_data.workspace, new_workspace) + # Check that auth domain exists. + self.assertEqual(new_workspace.authorization_domains.count(), 1) + auth_domain = new_workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_test-workspace") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_test-workspace@firecloud.org") + # Check that auth domain admin is correct. + membership = GroupGroupMembership.objects.get( + parent_group=auth_domain, child_group__name="TEST_PRIMED_CC_ADMINS" + ) + self.assertEqual(membership.role, membership.ADMIN) + # Check that workspace sharing is correct. + sharing = WorkspaceGroupSharing.objects.get( + workspace=new_workspace, + group__name="TEST_PRIMED_CC_ADMINS", + ) + self.assertEqual(sharing.access, sharing.OWNER) + self.assertEqual(sharing.can_compute, True) class CollaborativeAnalysisWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): From d3474d992e4357802c245644af36ecadd0cdcab9 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 3 Jul 2024 09:05:54 -0700 Subject: [PATCH 11/16] Use the WorkspaceAuthDomainDisabled form for dbGaP and collab adapters This disables the auth domain field, which is a better user experience when the auth domain will be automatically created. --- primed/collaborative_analysis/adapters.py | 4 ++-- primed/dbgap/adapters.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/primed/collaborative_analysis/adapters.py b/primed/collaborative_analysis/adapters.py index 7fa62cf8..ceb0d9c1 100644 --- a/primed/collaborative_analysis/adapters.py +++ b/primed/collaborative_analysis/adapters.py @@ -1,7 +1,7 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter -from anvil_consortium_manager.forms import WorkspaceForm from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter, WorkspaceAuthDomainAdapter +from primed.primed_anvil.forms import WorkspaceAuthDomainDisabledForm from . import forms, models, tables @@ -18,7 +18,7 @@ class CollaborativeAnalysisWorkspaceAdapter( description = "Workspaces used for collaborative analyses" list_table_class_staff_view = tables.CollaborativeAnalysisWorkspaceStaffTable list_table_class_view = tables.CollaborativeAnalysisWorkspaceUserTable - workspace_form_class = WorkspaceForm + workspace_form_class = WorkspaceAuthDomainDisabledForm workspace_data_model = models.CollaborativeAnalysisWorkspace workspace_data_form_class = forms.CollaborativeAnalysisWorkspaceForm workspace_detail_template_name = "collaborative_analysis/collaborativeanalysisworkspace_detail.html" diff --git a/primed/dbgap/adapters.py b/primed/dbgap/adapters.py index d42b7157..10905c31 100644 --- a/primed/dbgap/adapters.py +++ b/primed/dbgap/adapters.py @@ -1,9 +1,9 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter -from anvil_consortium_manager.forms import WorkspaceForm from anvil_consortium_manager.models import Workspace from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter, WorkspaceAuthDomainAdapter +from primed.primed_anvil.forms import WorkspaceAuthDomainDisabledForm from . import forms, models, tables @@ -16,7 +16,7 @@ class dbGaPWorkspaceAdapter(WorkspaceAuthDomainAdapter, WorkspaceAdminSharingAda description = "Workspaces containing data from released dbGaP accessions" list_table_class_staff_view = tables.dbGaPWorkspaceStaffTable list_table_class_view = tables.dbGaPWorkspaceUserTable - workspace_form_class = WorkspaceForm + workspace_form_class = WorkspaceAuthDomainDisabledForm workspace_data_model = models.dbGaPWorkspace workspace_data_form_class = forms.dbGaPWorkspaceForm workspace_detail_template_name = "dbgap/dbgapworkspace_detail.html" From 77206272473199fee23fd9797a1a481bef5019ba Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 3 Jul 2024 09:12:42 -0700 Subject: [PATCH 12/16] Automatically share all other workspaces with admins group Add the WorkspaceAdminSharingAdapter mixin to all other workspace adapters, which will automatically share th workspace with the admins group as an owner after it's created. --- primed/miscellaneous_workspaces/adapters.py | 13 +- .../tests/test_views.py | 136 +++++++++++++++++- 2 files changed, 142 insertions(+), 7 deletions(-) diff --git a/primed/miscellaneous_workspaces/adapters.py b/primed/miscellaneous_workspaces/adapters.py index 42a2d72c..ef9cb591 100644 --- a/primed/miscellaneous_workspaces/adapters.py +++ b/primed/miscellaneous_workspaces/adapters.py @@ -3,6 +3,7 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter from anvil_consortium_manager.forms import WorkspaceForm +from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter from primed.primed_anvil.tables import ( DefaultWorkspaceStaffTable, DefaultWorkspaceUserTable, @@ -11,7 +12,7 @@ from . import forms, models, tables -class SimulatedDataWorkspaceAdapter(BaseWorkspaceAdapter): +class SimulatedDataWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): """Adapter for SimulatedDataWorkspaces.""" type = "simulated_data" @@ -25,7 +26,7 @@ class SimulatedDataWorkspaceAdapter(BaseWorkspaceAdapter): workspace_detail_template_name = "miscellaneous_workspaces/simulateddataworkspace_detail.html" -class ConsortiumDevelWorkspaceAdapter(BaseWorkspaceAdapter): +class ConsortiumDevelWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): """Adapter for ConsortiumDevelWorkspaces.""" type = "devel" @@ -39,7 +40,7 @@ class ConsortiumDevelWorkspaceAdapter(BaseWorkspaceAdapter): workspace_detail_template_name = "anvil_consortium_manager/workspace_detail.html" -class ResourceWorkspaceAdapter(BaseWorkspaceAdapter): +class ResourceWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): """Adapter for ResourceWorkspaces.""" type = "resource" @@ -53,7 +54,7 @@ class ResourceWorkspaceAdapter(BaseWorkspaceAdapter): workspace_detail_template_name = "anvil_consortium_manager/workspace_detail.html" -class TemplateWorkspaceAdapter(BaseWorkspaceAdapter): +class TemplateWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): """Adapter for TemplateWorkspaces.""" type = "template" @@ -67,7 +68,7 @@ class TemplateWorkspaceAdapter(BaseWorkspaceAdapter): workspace_detail_template_name = "anvil_consortium_manager/workspace_detail.html" -class OpenAccessWorkspaceAdapter(BaseWorkspaceAdapter): +class OpenAccessWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): """Adapter for TemplateWorkspaces.""" type = "open_access" @@ -81,7 +82,7 @@ class OpenAccessWorkspaceAdapter(BaseWorkspaceAdapter): workspace_detail_template_name = "miscellaneous_workspaces/openaccessworkspace_detail.html" -class DataPrepWorkspaceAdapter(BaseWorkspaceAdapter): +class DataPrepWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): """Adapter for DataPrepWorkspace.""" type = "data_prep" diff --git a/primed/miscellaneous_workspaces/tests/test_views.py b/primed/miscellaneous_workspaces/tests/test_views.py index ac351cea..34c53e4a 100644 --- a/primed/miscellaneous_workspaces/tests/test_views.py +++ b/primed/miscellaneous_workspaces/tests/test_views.py @@ -1,12 +1,14 @@ """Tests for views related to the `workspaces` app.""" import responses -from anvil_consortium_manager.models import AnVILProjectManagerAccess, Workspace +from anvil_consortium_manager.models import AnVILProjectManagerAccess, Workspace, WorkspaceGroupSharing from anvil_consortium_manager.tests.factories import ( BillingProjectFactory, + ManagedGroupFactory, WorkspaceFactory, ) from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.test import TestCase @@ -59,6 +61,7 @@ def setUp(self): ) self.requester = UserFactory.create() self.workspace_type = "simulated_data" + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) def get_url(self, *args): """Get the url for the view being tested.""" @@ -79,6 +82,24 @@ def test_creates_workspace(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -100,6 +121,9 @@ def test_creates_workspace(self): self.assertEqual(models.SimulatedDataWorkspace.objects.count(), 1) new_workspace_data = models.SimulatedDataWorkspace.objects.latest("pk") self.assertEqual(new_workspace_data.workspace, new_workspace) + # Check that the workspace was shared with the admins group. + sharing = WorkspaceGroupSharing.objects.get(workspace=new_workspace, group=self.admins_group) + self.assertEqual(sharing.access, sharing.OWNER) class SimulatedDataWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): @@ -250,6 +274,7 @@ def setUp(self): ) self.requester = UserFactory.create() self.workspace_type = "devel" + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) def get_url(self, *args): """Get the url for the view being tested.""" @@ -270,6 +295,24 @@ def test_creates_workspace(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -291,6 +334,9 @@ def test_creates_workspace(self): self.assertEqual(models.ConsortiumDevelWorkspace.objects.count(), 1) new_workspace_data = models.ConsortiumDevelWorkspace.objects.latest("pk") self.assertEqual(new_workspace_data.workspace, new_workspace) + # Check that the workspace was shared with the admins group. + sharing = WorkspaceGroupSharing.objects.get(workspace=new_workspace, group=self.admins_group) + self.assertEqual(sharing.access, sharing.OWNER) class ConsortiumDevelWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): @@ -441,6 +487,7 @@ def setUp(self): ) self.requester = UserFactory.create() self.workspace_type = "resource" + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) def get_url(self, *args): """Get the url for the view being tested.""" @@ -461,6 +508,24 @@ def test_creates_workspace(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -482,6 +547,9 @@ def test_creates_workspace(self): self.assertEqual(models.ResourceWorkspace.objects.count(), 1) new_workspace_data = models.ResourceWorkspace.objects.latest("pk") self.assertEqual(new_workspace_data.workspace, new_workspace) + # Check that the workspace was shared with the admins group. + sharing = WorkspaceGroupSharing.objects.get(workspace=new_workspace, group=self.admins_group) + self.assertEqual(sharing.access, sharing.OWNER) class ResourceWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): @@ -631,6 +699,7 @@ def setUp(self): Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME) ) self.workspace_type = "template" + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) def get_url(self, *args): """Get the url for the view being tested.""" @@ -651,6 +720,24 @@ def test_creates_workspace(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -673,6 +760,9 @@ def test_creates_workspace(self): new_workspace_data = models.TemplateWorkspace.objects.latest("pk") self.assertEqual(new_workspace_data.workspace, new_workspace) self.assertEqual(new_workspace_data.intended_usage, "Test usage") + # Check that the workspace was shared with the admins group. + sharing = WorkspaceGroupSharing.objects.get(workspace=new_workspace, group=self.admins_group) + self.assertEqual(sharing.access, sharing.OWNER) class TemplateWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): @@ -828,6 +918,7 @@ def setUp(self): self.workspace_type = "open_access" self.requester = UserFactory.create() self.study = StudyFactory.create() + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) def get_url(self, *args): """Get the url for the view being tested.""" @@ -848,6 +939,24 @@ def test_creates_workspace(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -875,6 +984,9 @@ def test_creates_workspace(self): self.assertEqual(new_workspace_data.studies.count(), 1) self.assertIn(self.study, new_workspace_data.studies.all()) self.assertEqual(new_workspace_data.data_source, "test source") + # Check that the workspace was shared with the admins group. + sharing = WorkspaceGroupSharing.objects.get(workspace=new_workspace, group=self.admins_group) + self.assertEqual(sharing.access, sharing.OWNER) class OpenAccessWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): @@ -1051,6 +1163,7 @@ def setUp(self): self.requester = UserFactory.create() self.target_workspace = WorkspaceFactory.create() self.workspace_type = "data_prep" + self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) def get_url(self, *args): """Get the url for the view being tested.""" @@ -1071,6 +1184,24 @@ def test_creates_workspace(self): status=self.api_success_code, match=[responses.matchers.json_params_matcher(json_data)], ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + + "/api/workspaces/test-billing-project/test-workspace/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Make the post request self.client.force_login(self.user) response = self.client.post( self.get_url(self.workspace_type), @@ -1093,6 +1224,9 @@ def test_creates_workspace(self): self.assertEqual(models.DataPrepWorkspace.objects.count(), 1) new_workspace_data = models.DataPrepWorkspace.objects.latest("pk") self.assertEqual(new_workspace_data.workspace, new_workspace) + # Check that the workspace was shared with the admins group. + sharing = WorkspaceGroupSharing.objects.get(workspace=new_workspace, group=self.admins_group) + self.assertEqual(sharing.access, sharing.OWNER) class DataPrepWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): From 8314a914c82397b99079f238606937fc79a51e57 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 3 Jul 2024 09:22:22 -0700 Subject: [PATCH 13/16] Rename adapter mixins to have Mixin in the class name --- primed/cdsa/adapters.py | 4 ++-- primed/collaborative_analysis/adapters.py | 6 +++--- primed/dbgap/adapters.py | 4 ++-- primed/miscellaneous_workspaces/adapters.py | 14 +++++++------- primed/primed_anvil/adapters.py | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/primed/cdsa/adapters.py b/primed/cdsa/adapters.py index 18c20846..58571ef2 100644 --- a/primed/cdsa/adapters.py +++ b/primed/cdsa/adapters.py @@ -2,13 +2,13 @@ from anvil_consortium_manager.models import Workspace from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable -from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter, WorkspaceAuthDomainAdapter +from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapterMixin, WorkspaceAuthDomainAdapterMixin from primed.primed_anvil.forms import WorkspaceAuthDomainDisabledForm from . import forms, models, tables -class CDSAWorkspaceAdapter(WorkspaceAuthDomainAdapter, WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): +class CDSAWorkspaceAdapter(WorkspaceAuthDomainAdapterMixin, WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter): """Adapter for CDSAWorkspaces.""" type = "cdsa" diff --git a/primed/collaborative_analysis/adapters.py b/primed/collaborative_analysis/adapters.py index ceb0d9c1..846c4e24 100644 --- a/primed/collaborative_analysis/adapters.py +++ b/primed/collaborative_analysis/adapters.py @@ -1,14 +1,14 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter -from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter, WorkspaceAuthDomainAdapter +from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapterMixin, WorkspaceAuthDomainAdapterMixin from primed.primed_anvil.forms import WorkspaceAuthDomainDisabledForm from . import forms, models, tables class CollaborativeAnalysisWorkspaceAdapter( - WorkspaceAuthDomainAdapter, - WorkspaceAdminSharingAdapter, + WorkspaceAuthDomainAdapterMixin, + WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter, ): """Adapter for CollaborativeAnalysisWorkspace.""" diff --git a/primed/dbgap/adapters.py b/primed/dbgap/adapters.py index 10905c31..ae54759d 100644 --- a/primed/dbgap/adapters.py +++ b/primed/dbgap/adapters.py @@ -2,13 +2,13 @@ from anvil_consortium_manager.models import Workspace from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable -from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter, WorkspaceAuthDomainAdapter +from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapterMixin, WorkspaceAuthDomainAdapterMixin from primed.primed_anvil.forms import WorkspaceAuthDomainDisabledForm from . import forms, models, tables -class dbGaPWorkspaceAdapter(WorkspaceAuthDomainAdapter, WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): +class dbGaPWorkspaceAdapter(WorkspaceAuthDomainAdapterMixin, WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter): """Adapter for dbGaPWorkspaces.""" type = "dbgap" diff --git a/primed/miscellaneous_workspaces/adapters.py b/primed/miscellaneous_workspaces/adapters.py index ef9cb591..ebcefdd2 100644 --- a/primed/miscellaneous_workspaces/adapters.py +++ b/primed/miscellaneous_workspaces/adapters.py @@ -3,7 +3,7 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter from anvil_consortium_manager.forms import WorkspaceForm -from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapter +from primed.primed_anvil.adapters import WorkspaceAdminSharingAdapterMixin from primed.primed_anvil.tables import ( DefaultWorkspaceStaffTable, DefaultWorkspaceUserTable, @@ -12,7 +12,7 @@ from . import forms, models, tables -class SimulatedDataWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): +class SimulatedDataWorkspaceAdapter(WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter): """Adapter for SimulatedDataWorkspaces.""" type = "simulated_data" @@ -26,7 +26,7 @@ class SimulatedDataWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceA workspace_detail_template_name = "miscellaneous_workspaces/simulateddataworkspace_detail.html" -class ConsortiumDevelWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): +class ConsortiumDevelWorkspaceAdapter(WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter): """Adapter for ConsortiumDevelWorkspaces.""" type = "devel" @@ -40,7 +40,7 @@ class ConsortiumDevelWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspac workspace_detail_template_name = "anvil_consortium_manager/workspace_detail.html" -class ResourceWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): +class ResourceWorkspaceAdapter(WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter): """Adapter for ResourceWorkspaces.""" type = "resource" @@ -54,7 +54,7 @@ class ResourceWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapte workspace_detail_template_name = "anvil_consortium_manager/workspace_detail.html" -class TemplateWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): +class TemplateWorkspaceAdapter(WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter): """Adapter for TemplateWorkspaces.""" type = "template" @@ -68,7 +68,7 @@ class TemplateWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapte workspace_detail_template_name = "anvil_consortium_manager/workspace_detail.html" -class OpenAccessWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): +class OpenAccessWorkspaceAdapter(WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter): """Adapter for TemplateWorkspaces.""" type = "open_access" @@ -82,7 +82,7 @@ class OpenAccessWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdap workspace_detail_template_name = "miscellaneous_workspaces/openaccessworkspace_detail.html" -class DataPrepWorkspaceAdapter(WorkspaceAdminSharingAdapter, BaseWorkspaceAdapter): +class DataPrepWorkspaceAdapter(WorkspaceAdminSharingAdapterMixin, BaseWorkspaceAdapter): """Adapter for DataPrepWorkspace.""" type = "data_prep" diff --git a/primed/primed_anvil/adapters.py b/primed/primed_anvil/adapters.py index 931f9b07..eee418a4 100644 --- a/primed/primed_anvil/adapters.py +++ b/primed/primed_anvil/adapters.py @@ -32,7 +32,7 @@ def get_autocomplete_label(self, account): return "{} ({})".format(name, account.email) -class WorkspaceAuthDomainAdapter: +class WorkspaceAuthDomainAdapterMixin: """Helper class to add auth domains to workspaces.""" def before_anvil_create(self, workspace): @@ -57,7 +57,7 @@ def before_anvil_create(self, workspace): membership.anvil_create() -class WorkspaceAdminSharingAdapter: +class WorkspaceAdminSharingAdapterMixin: """Helper class to share workspaces with the PRIMED_CC_ADMINs group.""" def after_anvil_create(self, workspace): From 0b743a3d0cb2fc0e0589053448486ba33cd28cbc Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 3 Jul 2024 09:35:02 -0700 Subject: [PATCH 14/16] Move adapter mixin tests into a general test file Instead of essentially repeating the same tests fora ll the adapters, just test the adapter mixins themselves directly once. The actual workspace adapters themselves are still tested via the create views. --- primed/cdsa/tests/test_adapters.py | 146 -------------- .../tests/test_adapters.py | 146 -------------- primed/dbgap/tests/test_adapters.py | 145 -------------- primed/primed_anvil/adapters.py | 10 +- primed/primed_anvil/tests/test_adapters.py | 178 +++++++++++++++++- 5 files changed, 183 insertions(+), 442 deletions(-) delete mode 100644 primed/cdsa/tests/test_adapters.py delete mode 100644 primed/collaborative_analysis/tests/test_adapters.py delete mode 100644 primed/dbgap/tests/test_adapters.py diff --git a/primed/cdsa/tests/test_adapters.py b/primed/cdsa/tests/test_adapters.py deleted file mode 100644 index 6350864b..00000000 --- a/primed/cdsa/tests/test_adapters.py +++ /dev/null @@ -1,146 +0,0 @@ -import responses -from anvil_consortium_manager.models import ( - GroupGroupMembership, - WorkspaceGroupSharing, -) -from anvil_consortium_manager.tests.factories import ( - ManagedGroupFactory, - WorkspaceFactory, -) -from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin -from django.conf import settings -from django.test import TestCase, override_settings - -from .. import adapters -from . import factories - - -class CDSAWorkspaceAdapterTest(AnVILAPIMockTestMixin, TestCase): - """Tests for methods in the CDSAWorkspaceAdapter.""" - - def setUp(self): - super().setUp() - self.adapter = adapters.CDSAWorkspaceAdapter() - # Create the admins group. - self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) - - def test_before_anvil_create(self): - # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. - workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) - # API response for auth domain ManagedGroup creation. - self.anvil_response_mock.add( - responses.POST, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", - status=201, - ) - # API response for auth domain PRIMED_ADMINS membership. - self.anvil_response_mock.add( - responses.PUT, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", - status=204, - ) - # Run the adapter method. - self.adapter.before_anvil_create(workspace) - self.assertEqual(workspace.authorization_domains.count(), 1) - auth_domain = workspace.authorization_domains.first() - self.assertEqual(auth_domain.name, "AUTH_foo") - self.assertTrue(auth_domain.is_managed_by_app) - self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") - # Check for GroupGroupMembership. - self.assertEqual(GroupGroupMembership.objects.count(), 1) - membership = GroupGroupMembership.objects.first() - self.assertEqual(membership.parent_group, auth_domain) - self.assertEqual(membership.child_group, self.admins_group) - - @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") - def test_before_anvil_create_different_cc_admins_name(self): - admins_group = ManagedGroupFactory.create(name="foobar") - # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. - workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) - # API response for auth domain ManagedGroup creation. - self.anvil_response_mock.add( - responses.POST, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", - status=201, - ) - # API response for auth domain PRIMED_ADMINS membership. - self.anvil_response_mock.add( - responses.PUT, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/foobar@firecloud.org", - status=204, - ) - # Run the adapter method. - self.adapter.before_anvil_create(workspace) - self.assertEqual(workspace.authorization_domains.count(), 1) - auth_domain = workspace.authorization_domains.first() - self.assertEqual(auth_domain.name, "AUTH_foo") - self.assertTrue(auth_domain.is_managed_by_app) - self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") - # Check for GroupGroupMembership. - self.assertEqual(GroupGroupMembership.objects.count(), 1) - membership = GroupGroupMembership.objects.first() - self.assertEqual(membership.parent_group, auth_domain) - self.assertEqual(membership.child_group, admins_group) - - def test_after_anvil_create(self): - # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. - cdsa_workspace = factories.CDSAWorkspaceFactory.create( - workspace__billing_project__name="bar", workspace__name="foo" - ) - # API response for PRIMED_ADMINS workspace owner. - acls = [ - { - "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", - "accessLevel": "OWNER", - "canShare": False, - "canCompute": True, - } - ] - self.anvil_response_mock.add( - responses.PATCH, - self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", - status=200, - match=[responses.matchers.json_params_matcher(acls)], - json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, - ) - # Run the adapter method. - self.adapter.after_anvil_create(cdsa_workspace.workspace) - # Check for WorkspaceGroupSharing. - self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) - sharing = WorkspaceGroupSharing.objects.first() - self.assertEqual(sharing.workspace, cdsa_workspace.workspace) - self.assertEqual(sharing.group, self.admins_group) - self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) - self.assertTrue(sharing.can_compute) - - @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") - def test_after_anvil_create_different_admins_group(self): - admins_group = ManagedGroupFactory.create(name="foobar") - cdsa_workspace = factories.CDSAWorkspaceFactory.create( - workspace__billing_project__name="bar", workspace__name="foo" - ) - # API response for PRIMED_ADMINS workspace owner. - acls = [ - { - "email": "foobar@firecloud.org", - "accessLevel": "OWNER", - "canShare": False, - "canCompute": True, - } - ] - self.anvil_response_mock.add( - responses.PATCH, - self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", - status=200, - match=[responses.matchers.json_params_matcher(acls)], - json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, - ) - # Run the adapter method. - self.adapter.after_anvil_create(cdsa_workspace.workspace) - # Check for WorkspaceGroupSharing. - self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) - sharing = WorkspaceGroupSharing.objects.first() - self.assertEqual(sharing.workspace, cdsa_workspace.workspace) - self.assertEqual(sharing.group, admins_group) - self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) - self.assertTrue(sharing.can_compute) diff --git a/primed/collaborative_analysis/tests/test_adapters.py b/primed/collaborative_analysis/tests/test_adapters.py deleted file mode 100644 index 540d7502..00000000 --- a/primed/collaborative_analysis/tests/test_adapters.py +++ /dev/null @@ -1,146 +0,0 @@ -import responses -from anvil_consortium_manager.models import ( - GroupGroupMembership, - WorkspaceGroupSharing, -) -from anvil_consortium_manager.tests.factories import ( - ManagedGroupFactory, - WorkspaceFactory, -) -from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin -from django.conf import settings -from django.test import TestCase, override_settings - -from .. import adapters -from . import factories - - -class CollaborativeAnalysisWorkspaceAdapterTest(AnVILAPIMockTestMixin, TestCase): - """Tests for methods in the CollaborativeAnalysisWorkspaceAdapter.""" - - def setUp(self): - super().setUp() - self.adapter = adapters.CollaborativeAnalysisWorkspaceAdapter() - # Create the admins group. - self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) - - def test_before_anvil_create(self): - # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. - workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) - # API response for auth domain ManagedGroup creation. - self.anvil_response_mock.add( - responses.POST, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", - status=201, - ) - # API response for auth domain PRIMED_ADMINS membership. - self.anvil_response_mock.add( - responses.PUT, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", - status=204, - ) - # Run the adapter method. - self.adapter.before_anvil_create(workspace) - self.assertEqual(workspace.authorization_domains.count(), 1) - auth_domain = workspace.authorization_domains.first() - self.assertEqual(auth_domain.name, "AUTH_foo") - self.assertTrue(auth_domain.is_managed_by_app) - self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") - # Check for GroupGroupMembership. - self.assertEqual(GroupGroupMembership.objects.count(), 1) - membership = GroupGroupMembership.objects.first() - self.assertEqual(membership.parent_group, auth_domain) - self.assertEqual(membership.child_group, self.admins_group) - - @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") - def test_before_anvil_create_different_cc_admins_name(self): - admins_group = ManagedGroupFactory.create(name="foobar") - # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. - workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) - # API response for auth domain ManagedGroup creation. - self.anvil_response_mock.add( - responses.POST, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", - status=201, - ) - # API response for auth domain PRIMED_ADMINS membership. - self.anvil_response_mock.add( - responses.PUT, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/foobar@firecloud.org", - status=204, - ) - # Run the adapter method. - self.adapter.before_anvil_create(workspace) - self.assertEqual(workspace.authorization_domains.count(), 1) - auth_domain = workspace.authorization_domains.first() - self.assertEqual(auth_domain.name, "AUTH_foo") - self.assertTrue(auth_domain.is_managed_by_app) - self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") - # Check for GroupGroupMembership. - self.assertEqual(GroupGroupMembership.objects.count(), 1) - membership = GroupGroupMembership.objects.first() - self.assertEqual(membership.parent_group, auth_domain) - self.assertEqual(membership.child_group, admins_group) - - def test_after_anvil_create(self): - # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. - collab_workspace = factories.CollaborativeAnalysisWorkspaceFactory.create( - workspace__billing_project__name="bar", workspace__name="foo" - ) - # API response for PRIMED_ADMINS workspace owner. - acls = [ - { - "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", - "accessLevel": "OWNER", - "canShare": False, - "canCompute": True, - } - ] - self.anvil_response_mock.add( - responses.PATCH, - self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", - status=200, - match=[responses.matchers.json_params_matcher(acls)], - json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, - ) - # Run the adapter method. - self.adapter.after_anvil_create(collab_workspace.workspace) - # Check for WorkspaceGroupSharing. - self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) - sharing = WorkspaceGroupSharing.objects.first() - self.assertEqual(sharing.workspace, collab_workspace.workspace) - self.assertEqual(sharing.group, self.admins_group) - self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) - self.assertTrue(sharing.can_compute) - - @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") - def test_after_anvil_create_different_admins_group(self): - admins_group = ManagedGroupFactory.create(name="foobar") - collab_workspace = factories.CollaborativeAnalysisWorkspaceFactory.create( - workspace__billing_project__name="bar", workspace__name="foo" - ) - # API response for PRIMED_ADMINS workspace owner. - acls = [ - { - "email": "foobar@firecloud.org", - "accessLevel": "OWNER", - "canShare": False, - "canCompute": True, - } - ] - self.anvil_response_mock.add( - responses.PATCH, - self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", - status=200, - match=[responses.matchers.json_params_matcher(acls)], - json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, - ) - # Run the adapter method. - self.adapter.after_anvil_create(collab_workspace.workspace) - # Check for WorkspaceGroupSharing. - self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) - sharing = WorkspaceGroupSharing.objects.first() - self.assertEqual(sharing.workspace, collab_workspace.workspace) - self.assertEqual(sharing.group, admins_group) - self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) - self.assertTrue(sharing.can_compute) diff --git a/primed/dbgap/tests/test_adapters.py b/primed/dbgap/tests/test_adapters.py deleted file mode 100644 index fd091bf5..00000000 --- a/primed/dbgap/tests/test_adapters.py +++ /dev/null @@ -1,145 +0,0 @@ -import responses -from anvil_consortium_manager.models import ( - GroupGroupMembership, - WorkspaceGroupSharing, -) -from anvil_consortium_manager.tests.factories import ( - ManagedGroupFactory, - WorkspaceFactory, -) -from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin -from django.conf import settings -from django.test import TestCase, override_settings - -from .. import adapters -from . import factories - - -class dbGaPWorkspaceAdapterTest(AnVILAPIMockTestMixin, TestCase): - """Tests for methods in the dbGaPWorkspaceAdapter.""" - - def setUp(self): - super().setUp() - self.adapter = adapters.dbGaPWorkspaceAdapter() - # Create the admins group. - self.admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) - - def test_before_anvil_create(self): - # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. - workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) - # API response for auth domain ManagedGroup creation. - self.anvil_response_mock.add( - responses.POST, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", - status=201, - ) - # API response for auth domain PRIMED_ADMINS membership. - self.anvil_response_mock.add( - responses.PUT, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", - status=204, - ) - # Run the adapter method. - self.adapter.before_anvil_create(workspace) - self.assertEqual(workspace.authorization_domains.count(), 1) - auth_domain = workspace.authorization_domains.first() - self.assertEqual(auth_domain.name, "AUTH_foo") - self.assertTrue(auth_domain.is_managed_by_app) - self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") - # Check for GroupGroupMembership. - self.assertEqual(GroupGroupMembership.objects.count(), 1) - membership = GroupGroupMembership.objects.first() - self.assertEqual(membership.parent_group, auth_domain) - self.assertEqual(membership.child_group, self.admins_group) - - @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") - def test_before_anvil_create_different_cc_admins_name(self): - admins_group = ManagedGroupFactory.create(name="foobar") - # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. - workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) - # API response for auth domain ManagedGroup creation. - self.anvil_response_mock.add( - responses.POST, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", - status=201, - ) - # API response for auth domain PRIMED_ADMINS membership. - self.anvil_response_mock.add( - responses.PUT, - self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/foobar@firecloud.org", - status=204, - ) - # Run the adapter method. - self.adapter.before_anvil_create(workspace) - self.assertEqual(workspace.authorization_domains.count(), 1) - auth_domain = workspace.authorization_domains.first() - self.assertEqual(auth_domain.name, "AUTH_foo") - self.assertTrue(auth_domain.is_managed_by_app) - self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") - # Check for GroupGroupMembership. - self.assertEqual(GroupGroupMembership.objects.count(), 1) - membership = GroupGroupMembership.objects.first() - self.assertEqual(membership.parent_group, auth_domain) - self.assertEqual(membership.child_group, admins_group) - - def test_after_anvil_create(self): - dbgap_workspace = factories.dbGaPWorkspaceFactory.create( - workspace__billing_project__name="bar", workspace__name="foo" - ) - # API response for PRIMED_ADMINS workspace owner. - acls = [ - { - "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", - "accessLevel": "OWNER", - "canShare": False, - "canCompute": True, - } - ] - self.anvil_response_mock.add( - responses.PATCH, - self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", - status=200, - match=[responses.matchers.json_params_matcher(acls)], - json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, - ) - # Run the adapter method. - self.adapter.after_anvil_create(dbgap_workspace.workspace) - # Check for WorkspaceGroupSharing. - self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) - sharing = WorkspaceGroupSharing.objects.first() - self.assertEqual(sharing.workspace, dbgap_workspace.workspace) - self.assertEqual(sharing.group, self.admins_group) - self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) - self.assertTrue(sharing.can_compute) - - @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") - def test_after_anvil_create_different_admins_group(self): - admins_group = ManagedGroupFactory.create(name="foobar") - dbgap_workspace = factories.dbGaPWorkspaceFactory.create( - workspace__billing_project__name="bar", workspace__name="foo" - ) - # API response for PRIMED_ADMINS workspace owner. - acls = [ - { - "email": "foobar@firecloud.org", - "accessLevel": "OWNER", - "canShare": False, - "canCompute": True, - } - ] - self.anvil_response_mock.add( - responses.PATCH, - self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", - status=200, - match=[responses.matchers.json_params_matcher(acls)], - json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, - ) - # Run the adapter method. - self.adapter.after_anvil_create(dbgap_workspace.workspace) - # Check for WorkspaceGroupSharing. - self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) - sharing = WorkspaceGroupSharing.objects.first() - self.assertEqual(sharing.workspace, dbgap_workspace.workspace) - self.assertEqual(sharing.group, admins_group) - self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) - self.assertTrue(sharing.can_compute) diff --git a/primed/primed_anvil/adapters.py b/primed/primed_anvil/adapters.py index eee418a4..547c0ea4 100644 --- a/primed/primed_anvil/adapters.py +++ b/primed/primed_anvil/adapters.py @@ -48,7 +48,10 @@ def before_anvil_create(self, workspace): workspace.authorization_domains.add(auth_domain) auth_domain.anvil_create() # Add the ADMINs group as an admin of the auth domain. - admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + try: + admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + except ManagedGroup.DoesNotExist: + return membership = GroupGroupMembership.objects.create( parent_group=auth_domain, child_group=admins_group, @@ -63,7 +66,10 @@ class WorkspaceAdminSharingAdapterMixin: def after_anvil_create(self, workspace): super().after_anvil_create(workspace) # Share the workspace with the ADMINs group as an owner. - admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + try: + admins_group = ManagedGroup.objects.get(name=settings.ANVIL_CC_ADMINS_GROUP_NAME) + except ManagedGroup.DoesNotExist: + return sharing = WorkspaceGroupSharing.objects.create( workspace=workspace, group=admins_group, diff --git a/primed/primed_anvil/tests/test_adapters.py b/primed/primed_anvil/tests/test_adapters.py index 515b4b10..55dffb8b 100644 --- a/primed/primed_anvil/tests/test_adapters.py +++ b/primed/primed_anvil/tests/test_adapters.py @@ -1,6 +1,9 @@ -from anvil_consortium_manager.models import Account -from anvil_consortium_manager.tests.factories import AccountFactory -from django.test import TestCase +import responses +from anvil_consortium_manager.adapters.default import DefaultWorkspaceAdapter +from anvil_consortium_manager.models import Account, GroupGroupMembership, WorkspaceGroupSharing +from anvil_consortium_manager.tests.factories import AccountFactory, ManagedGroupFactory, WorkspaceFactory +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin +from django.test import TestCase, override_settings from primed.users.tests.factories import UserFactory @@ -57,3 +60,172 @@ def test_autocomplete_queryset_no_linked_user(self): self.assertEqual(len(queryset), 1) self.assertIn(account_1, queryset) self.assertNotIn(account_2, queryset) + + +class WorkspaceAuthDomainAdapterMixinTest(AnVILAPIMockTestMixin, TestCase): + def setUp(self): + super().setUp() + + class TestAdapter(adapters.WorkspaceAuthDomainAdapterMixin, DefaultWorkspaceAdapter): + pass + + self.adapter = TestAdapter() + + def test_before_anvil_create(self): + admins_group = ManagedGroupFactory.create(name="TEST_PRIMED_CC_ADMINS") + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + # Run the adapter method. + self.adapter.before_anvil_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # Check for GroupGroupMembership. + self.assertEqual(GroupGroupMembership.objects.count(), 1) + membership = GroupGroupMembership.objects.first() + self.assertEqual(membership.parent_group, auth_domain) + self.assertEqual(membership.child_group, admins_group) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") + def test_before_anvil_create_different_cc_admins_name(self): + admins_group = ManagedGroupFactory.create(name="foobar") + # Create a Workspace instead of CDSAWorkspace to skip factory auth domain behavior. + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # API response for auth domain PRIMED_ADMINS membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo/admin/foobar@firecloud.org", + status=204, + ) + # Run the adapter method. + self.adapter.before_anvil_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # Check for GroupGroupMembership. + self.assertEqual(GroupGroupMembership.objects.count(), 1) + membership = GroupGroupMembership.objects.first() + self.assertEqual(membership.parent_group, auth_domain) + self.assertEqual(membership.child_group, admins_group) + + def test_before_anvil_create_admins_group_does_not_exist(self): + """If the admins group does not exist, the workspace is not shared.""" + workspace = WorkspaceFactory.create(name="foo", workspace_type=self.adapter.get_type()) + # API response for auth domain ManagedGroup creation. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + "/api/groups/v1/AUTH_foo", + status=201, + ) + # Run the adapter method. + self.adapter.before_anvil_create(workspace) + self.assertEqual(workspace.authorization_domains.count(), 1) + auth_domain = workspace.authorization_domains.first() + self.assertEqual(auth_domain.name, "AUTH_foo") + self.assertTrue(auth_domain.is_managed_by_app) + self.assertEqual(auth_domain.email, "AUTH_foo@firecloud.org") + # No GroupGroupMembership objects were created. + self.assertEqual(GroupGroupMembership.objects.count(), 0) + + +class WorkspaceAdminSharingAdapterMixin(AnVILAPIMockTestMixin, TestCase): + def setUp(self): + super().setUp() + + class TestAdapter(adapters.WorkspaceAdminSharingAdapterMixin, DefaultWorkspaceAdapter): + pass + + self.adapter = TestAdapter() + + def test_after_anvil_create(self): + admins_group = ManagedGroupFactory.create(name="TEST_PRIMED_CC_ADMINS") + workspace = WorkspaceFactory.create( + billing_project__name="bar", name="foo", workspace_type=self.adapter.get_type() + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "TEST_PRIMED_CC_ADMINS@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Run the adapter method. + self.adapter.after_anvil_create(workspace) + # Check for WorkspaceGroupSharing. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) + sharing = WorkspaceGroupSharing.objects.first() + self.assertEqual(sharing.workspace, workspace) + self.assertEqual(sharing.group, admins_group) + self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) + self.assertTrue(sharing.can_compute) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foobar") + def test_after_anvil_create_different_admins_group(self): + admins_group = ManagedGroupFactory.create(name="foobar") + workspace = WorkspaceFactory.create( + billing_project__name="bar", name="foo", workspace_type=self.adapter.get_type() + ) + # API response for PRIMED_ADMINS workspace owner. + acls = [ + { + "email": "foobar@firecloud.org", + "accessLevel": "OWNER", + "canShare": False, + "canCompute": True, + } + ] + self.anvil_response_mock.add( + responses.PATCH, + self.api_client.rawls_entry_point + "/api/workspaces/bar/foo/acl?inviteUsersNotFound=false", + status=200, + match=[responses.matchers.json_params_matcher(acls)], + json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls}, + ) + # Run the adapter method. + self.adapter.after_anvil_create(workspace) + # Check for WorkspaceGroupSharing. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 1) + sharing = WorkspaceGroupSharing.objects.first() + self.assertEqual(sharing.workspace, workspace) + self.assertEqual(sharing.group, admins_group) + self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER) + self.assertTrue(sharing.can_compute) + + def test_after_anvil_create_no_admins_group(self): + workspace = WorkspaceFactory.create( + billing_project__name="bar", name="foo", workspace_type=self.adapter.get_type() + ) + # Run the adapter method. + self.adapter.after_anvil_create(workspace) + # No WorkspaceGroupSharing objects were created. + self.assertEqual(WorkspaceGroupSharing.objects.count(), 0) From 1ab110576fac7cdff86efd5a2ff7d7b9acf35c1a Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 3 Jul 2024 13:42:01 -0700 Subject: [PATCH 15/16] Update requirements.in file with new ACM version requirements.txt will be updated by CI. --- requirements/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index 5775f119..5f17a1f3 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -33,7 +33,7 @@ django-dbbackup # https://github.com/jazzband/django-dbbackup django-extensions # https://github.com/django-extensions/django-extensions # anvil_consortium_manager -django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.23 +django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.24 # Simple history - model history tracking django-simple-history From 2c521edcf5476e689cad45a4c7b3725f8685709d Mon Sep 17 00:00:00 2001 From: amstilp <3944584+amstilp@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:44:29 +0000 Subject: [PATCH 16/16] Compile requirements files --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 1a27fac3..028705a0 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -62,7 +62,7 @@ django==4.2.13 # django-tables2 django-allauth==0.54.0 # via -r requirements/requirements.in -django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@cd6ea592f7b5b324fc3dad84b1dfb48eebb26456 +django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.24 # via -r requirements/requirements.in django-autocomplete-light==3.11.0 # via django-anvil-consortium-manager