From 44074897e2d920bfc567460b01a52b64e1ab7e5b Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Fri, 30 Aug 2024 13:34:55 -0700
Subject: [PATCH 01/97] Fix template for UploadWorkspaceAuthDomainAudit view
This view was using the template for the upload workspace sharing
audit instead of the auth domain audit. The behavior of the view is
fine since the templates are very similar; it's just that the
explanation and title are incorrect.
---
gregor_django/gregor_anvil/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index 8b43b389..cdf8c701 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -394,7 +394,7 @@ def form_valid(self, form):
class UploadWorkspaceAuthDomainAudit(AnVILConsortiumManagerStaffViewRequired, TemplateView):
"""View to audit UploadWorkspace auth domain membership for all UploadWorkspaces."""
- template_name = "gregor_anvil/upload_workspace_sharing_audit.html"
+ template_name = "gregor_anvil/upload_workspace_auth_domain_audit.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
From c4a9481cffadd43fb37fc465bbd00f51ad1e95cc Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 2 Sep 2024 16:25:10 +0000
Subject: [PATCH 02/97] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.3)
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 06d31928..ae91c05a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -12,7 +12,7 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
- rev: v0.6.2
+ rev: v0.6.3
hooks:
# Run the linter.
- id: ruff
From 7d48b0ca52588bd4de5f92dacaa558dcdca892c7 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 3 Sep 2024 10:39:21 -0700
Subject: [PATCH 03/97] Add text to title when auditing all upload workspaces
Instead of having the page title just have a colon followed by
nothing when auditing all upload workspaces, instead check if there
is an object and if not, display "all upload workspaces". Otherwise,
display the object str as before.
---
.../gregor_anvil/tests/test_views.py | 46 +++++++++++++++++++
.../upload_workspace_auth_domain_audit.html | 2 +-
.../upload_workspace_sharing_audit.html | 2 +-
3 files changed, 48 insertions(+), 2 deletions(-)
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index fa752a8a..32806ab0 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -2693,6 +2693,12 @@ def test_context_error_table_stop_sharing(self):
)
self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+ def test_title(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url())
+ # self.assertContains(response, str(self.upload_workspace))
+ self.assertIn("all upload workspaces", response.content.decode().lower())
+
class UploadWorkspaceSharingAuditByWorkspaceTest(AnVILAPIMockTestMixin, TestCase):
"""Tests for the UploadWorkspaceSharingAuditByWorkspace view."""
@@ -3059,6 +3065,17 @@ def test_context_error_table_stop_sharing(self):
)
self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+ def test_title(self):
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertContains(response, str(self.upload_workspace))
+ # self.assertNotIn("all upload workspaces", response.content.decode().lower())
+
class UploadWorkspaceSharingAuditByUploadCycleTest(AnVILAPIMockTestMixin, TestCase):
"""Tests for the UploadWorkspaceSharingAuditByUploadCycle view."""
@@ -3393,6 +3410,12 @@ def test_context_error_table_stop_sharing(self):
)
self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+ def test_title(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(self.upload_cycle.cycle))
+ self.assertContains(response, str(self.upload_cycle))
+ self.assertNotIn("all upload workspaces", response.content.decode().lower())
+
class UploadWorkspaceSharingAuditResolveTest(AnVILAPIMockTestMixin, TestCase):
def setUp(self):
@@ -4847,6 +4870,12 @@ def test_context_needs_action_table_change_to_admin(self):
)
self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+ def test_title(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url())
+ # self.assertContains(response, str(self.upload_workspace))
+ self.assertIn("all upload workspaces", response.content.decode().lower())
+
class UploadWorkspaceAuthDomainAuditByWorkspaceTest(AnVILAPIMockTestMixin, TestCase):
"""Tests for the UploadWorkspaceAuthDomainAuditByWorkspace view."""
@@ -5225,6 +5254,17 @@ def test_context_needs_action_table_change_to_admin(self):
)
self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+ def test_title(self):
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertContains(response, str(self.upload_workspace))
+ self.assertNotIn("all upload workspaces", response.content.decode().lower())
+
class UploadWorkspaceAuthDomainAuditByUploadCycleTest(AnVILAPIMockTestMixin, TestCase):
"""Tests for the UploadWorkspaceSharingAuditByUploadCycle view."""
@@ -5566,6 +5606,12 @@ def test_context_needs_action_table_change_to_admin(self):
)
self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+ def test_title(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(self.upload_cycle.cycle))
+ self.assertContains(response, str(self.upload_cycle))
+ self.assertNotIn("all upload workspaces", response.content.decode().lower())
+
class UploadWorkspaceAuthDomainAuditResolveTest(AnVILAPIMockTestMixin, TestCase):
"""Tests for the UploadWorkspaceAuthDomainAuditResolve view."""
diff --git a/gregor_django/templates/gregor_anvil/upload_workspace_auth_domain_audit.html b/gregor_django/templates/gregor_anvil/upload_workspace_auth_domain_audit.html
index 1588d613..bfc33c32 100644
--- a/gregor_django/templates/gregor_anvil/upload_workspace_auth_domain_audit.html
+++ b/gregor_django/templates/gregor_anvil/upload_workspace_auth_domain_audit.html
@@ -6,7 +6,7 @@
{% block content %}
-Upload workspace auth domain audit: {{ object }}
+Upload workspace auth domain audit: {% if object %} {{ object }} {% else %} all Upload Workspaces{% endif %}
diff --git a/gregor_django/templates/gregor_anvil/upload_workspace_sharing_audit.html b/gregor_django/templates/gregor_anvil/upload_workspace_sharing_audit.html
index dc4b1a0c..80c50a37 100644
--- a/gregor_django/templates/gregor_anvil/upload_workspace_sharing_audit.html
+++ b/gregor_django/templates/gregor_anvil/upload_workspace_sharing_audit.html
@@ -6,7 +6,7 @@
{% block content %}
-
Upload workspace sharing audit: {{ object }}
+
Upload workspace sharing audit: {% if object %}{{ object }}{% else %} all Upload Workspaces{% endif %}
From 96f73ea77b904736d28d8ea3fc9ef3f13076d523 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 3 Sep 2024 10:51:19 -0700
Subject: [PATCH 04/97] Color can_compute=False black in audit table
This makes more sense; red typically indicates an error while black
indicates "no but not a problem".
---
.../gregor_anvil/audit/upload_workspace_sharing_audit.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py b/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
index 249c7a24..3ef33806 100644
--- a/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
+++ b/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
@@ -135,7 +135,7 @@ class UploadWorkspaceSharingAuditTable(tables.Table):
managed_group = tables.Column(linkify=True)
# is_shared = tables.Column()
access = tables.Column(verbose_name="Current access")
- can_compute = BooleanIconColumn(show_false_icon=True, null=True)
+ can_compute = BooleanIconColumn(show_false_icon=True, null=True, true_color="green", false_color="black")
note = tables.Column()
# action = tables.Column()
action = tables.TemplateColumn(
From 13134c15d1579b28f9085c4375687b37eca7ac84 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 3 Sep 2024 11:03:09 -0700
Subject: [PATCH 05/97] Add (basic) tests for audit tables
---
.../gregor_anvil/tests/test_audit.py | 123 ++++++++++++++++++
1 file changed, 123 insertions(+)
diff --git a/gregor_django/gregor_anvil/tests/test_audit.py b/gregor_django/gregor_anvil/tests/test_audit.py
index 83dc8ff5..ac30b6ac 100644
--- a/gregor_django/gregor_anvil/tests/test_audit.py
+++ b/gregor_django/gregor_anvil/tests/test_audit.py
@@ -162,6 +162,67 @@ def test_get_errors_table(self):
self.assertEqual(table.rows[0].get_cell("value"), "c")
+class UploadWorkspaceSharingAuditTableTest(TestCase):
+ """General tests of the UploadWorkspaceSharingAuditTable class."""
+
+ def test_no_rows(self):
+ """Table works with no rows."""
+ table = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditTable([])
+ self.assertIsInstance(table, upload_workspace_sharing_audit.UploadWorkspaceSharingAuditTable)
+ self.assertEqual(len(table.rows), 0)
+
+ def test_one_row(self):
+ """Table works with one row."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = ManagedGroupFactory.create()
+ WorkspaceGroupSharingFactory.create(
+ workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.READER
+ )
+ data = [
+ {
+ "workspace": upload_workspace,
+ "managed_group": group,
+ "access": WorkspaceGroupSharing.READER,
+ "can_compute": None,
+ "note": "a note",
+ "action": "",
+ },
+ ]
+ table = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditTable(data)
+ self.assertIsInstance(table, upload_workspace_sharing_audit.UploadWorkspaceSharingAuditTable)
+ self.assertEqual(len(table.rows), 1)
+
+ def test_two_rows(self):
+ """Table works with two rows."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group_1 = ManagedGroupFactory.create()
+ group_2 = ManagedGroupFactory.create()
+ WorkspaceGroupSharingFactory.create(
+ workspace=upload_workspace.workspace, group=group_1, access=WorkspaceGroupSharing.READER
+ )
+ data = [
+ {
+ "workspace": upload_workspace,
+ "managed_group": group_1,
+ "access": WorkspaceGroupSharing.READER,
+ "can_compute": None,
+ "note": "a note",
+ "action": "",
+ },
+ {
+ "workspace": upload_workspace,
+ "managed_group": group_2,
+ "access": None,
+ "can_compute": None,
+ "note": "a note",
+ "action": "",
+ },
+ ]
+ table = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditTable(data)
+ self.assertIsInstance(table, upload_workspace_sharing_audit.UploadWorkspaceSharingAuditTable)
+ self.assertEqual(len(table.rows), 2)
+
+
class UploadWorkspaceSharingAuditTest(TestCase):
"""General tests of the `UploadWorkspaceSharingAudit` class."""
@@ -3860,6 +3921,68 @@ def test_other_group_shared_as_owner(self):
self.assertEqual(record.note, upload_workspace_sharing_audit.UploadWorkspaceSharingAudit.OTHER_GROUP_NO_ACCESS)
+class UploadWorkspaceAuthDomainAuditTableTest(TestCase):
+ """General tests of the UploadWorkspaceAuthDomainAuditTable class."""
+
+ def test_no_rows(self):
+ """Table works with no rows."""
+ table = upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable([])
+ self.assertIsInstance(table, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable)
+ self.assertEqual(len(table.rows), 0)
+
+ def test_one_row(self):
+ """Table works with one row."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = ManagedGroupFactory.create()
+ GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.MEMBER,
+ )
+ data = [
+ {
+ "workspace": upload_workspace,
+ "managed_group": group,
+ "role": GroupGroupMembership.MEMBER,
+ "note": "a note",
+ "action": "",
+ },
+ ]
+ table = upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable(data)
+ self.assertIsInstance(table, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable)
+ self.assertEqual(len(table.rows), 1)
+
+ def test_two_rows(self):
+ """Table works with two rows."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group_1 = ManagedGroupFactory.create()
+ group_2 = ManagedGroupFactory.create()
+ GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group_1,
+ role=GroupGroupMembership.MEMBER,
+ )
+ data = [
+ {
+ "workspace": upload_workspace,
+ "managed_group": group_1,
+ "role": GroupGroupMembership.MEMBER,
+ "note": "a note",
+ "action": "",
+ },
+ {
+ "workspace": upload_workspace,
+ "managed_group": group_2,
+ "role": None,
+ "note": "a note",
+ "action": "",
+ },
+ ]
+ table = upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable(data)
+ self.assertIsInstance(table, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable)
+ self.assertEqual(len(table.rows), 2)
+
+
class UploadWorkspaceAuthDomainAuditTest(TestCase):
"""General tests of the `UploadWorkspaceAuthDomainAudit` class."""
From ac6c5193e7d17581defd2b409c1a65a2a7288f13 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 3 Sep 2024 11:18:08 -0700
Subject: [PATCH 06/97] Only show can_compute icon for owners and writers
Do not show can_compute for readers. Also, add tests for the
audit result classes so that we can test the get_table_dictionary
method. In this case, there is no need to test the individual
classes, since they are all just inheriting from the main class
without much different behavior.
---
.../audit/upload_workspace_sharing_audit.py | 5 +-
.../gregor_anvil/tests/test_audit.py | 131 ++++++++++++++++++
2 files changed, 135 insertions(+), 1 deletion(-)
diff --git a/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py b/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
index 3ef33806..af7befe6 100644
--- a/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
+++ b/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
@@ -34,11 +34,14 @@ def get_action_url(self):
def get_table_dictionary(self):
"""Return a dictionary that can be used to populate an instance of `dbGaPDataSharingSnapshotAuditTable`."""
+ can_compute = None
+ if self.current_sharing_instance and self.current_sharing_instance.access != WorkspaceGroupSharing.READER:
+ can_compute = self.current_sharing_instance.can_compute
row = {
"workspace": self.workspace,
"managed_group": self.managed_group,
"access": self.current_sharing_instance.access if self.current_sharing_instance else None,
- "can_compute": self.current_sharing_instance.can_compute if self.current_sharing_instance else None,
+ "can_compute": can_compute,
"note": self.note,
"action": self.action,
"action_url": self.get_action_url(),
diff --git a/gregor_django/gregor_anvil/tests/test_audit.py b/gregor_django/gregor_anvil/tests/test_audit.py
index ac30b6ac..83f4975e 100644
--- a/gregor_django/gregor_anvil/tests/test_audit.py
+++ b/gregor_django/gregor_anvil/tests/test_audit.py
@@ -162,6 +162,87 @@ def test_get_errors_table(self):
self.assertEqual(table.rows[0].get_cell("value"), "c")
+class UploadWorkspaceSharingAuditResultTest(TestCase):
+ """General tests of the UploadWorkspaceSharingAuditResult dataclasses."""
+
+ def test_shared_as_owner(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.OWNER, can_compute=True
+ )
+ instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ workspace=upload_workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertEqual(table_dictionary["access"], sharing.OWNER)
+ self.assertEqual(table_dictionary["can_compute"], True)
+
+ def test_shared_as_writer_with_compute(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.WRITER, can_compute=True
+ )
+ instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ workspace=upload_workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertEqual(table_dictionary["access"], sharing.WRITER)
+ self.assertEqual(table_dictionary["can_compute"], True)
+
+ def test_shared_as_writer_without_compute(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.WRITER, can_compute=False
+ )
+ instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ workspace=upload_workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertEqual(table_dictionary["access"], sharing.WRITER)
+ self.assertEqual(table_dictionary["can_compute"], False)
+
+ def test_shared_as_reader(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.READER
+ )
+ instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ workspace=upload_workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertEqual(table_dictionary["access"], sharing.READER)
+ self.assertIsNone(table_dictionary["can_compute"])
+
+ def test_not_shared(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ workspace=upload_workspace,
+ managed_group=group,
+ current_sharing_instance=None,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertIsNone(table_dictionary["access"])
+ self.assertIsNone(table_dictionary["can_compute"])
+
+
class UploadWorkspaceSharingAuditTableTest(TestCase):
"""General tests of the UploadWorkspaceSharingAuditTable class."""
@@ -3921,6 +4002,56 @@ def test_other_group_shared_as_owner(self):
self.assertEqual(record.note, upload_workspace_sharing_audit.UploadWorkspaceSharingAudit.OTHER_GROUP_NO_ACCESS)
+class UploadWorkspaceAuthDomainAuditResultTest(TestCase):
+ """General tests of the UploadWorkspaceAuthDomainAuditResult dataclasses."""
+
+ def test_member_as_admin(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.ADMIN,
+ )
+ instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ workspace=upload_workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertEqual(table_dictionary["role"], membership.ADMIN)
+
+ def test_member_as_member(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.MEMBER,
+ )
+ instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ workspace=upload_workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertEqual(table_dictionary["role"], membership.MEMBER)
+
+ def test_not_member(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ workspace=upload_workspace,
+ managed_group=group,
+ current_membership_instance=None,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertIsNone(table_dictionary["role"])
+
+
class UploadWorkspaceAuthDomainAuditTableTest(TestCase):
"""General tests of the UploadWorkspaceAuthDomainAuditTable class."""
From a2b0ee07fc903a9e65001a11b982846b462eb429 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 3 Sep 2024 11:22:08 -0700
Subject: [PATCH 07/97] Remove unused code from sharing audit
---
.../audit/upload_workspace_sharing_audit.py | 24 -------------------
1 file changed, 24 deletions(-)
diff --git a/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py b/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
index af7befe6..45d640aa 100644
--- a/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
+++ b/gregor_django/gregor_anvil/audit/upload_workspace_sharing_audit.py
@@ -20,18 +20,6 @@ class UploadWorkspaceSharingAuditResult(GREGoRAuditResult):
action: str = None
current_sharing_instance: WorkspaceGroupSharing = None
- def get_action_url(self):
- """The URL that handles the action needed."""
- # return reverse(
- # "gregor_anvil:audit:upload_workspaces:sharing:resolve",
- # args=[
- # self.dbgap_application.dbgap_project_id,
- # self.workspace.workspace.billing_project.name,
- # self.workspace.workspace.name,
- # ],
- # )
- return ""
-
def get_table_dictionary(self):
"""Return a dictionary that can be used to populate an instance of `dbGaPDataSharingSnapshotAuditTable`."""
can_compute = None
@@ -44,7 +32,6 @@ def get_table_dictionary(self):
"can_compute": can_compute,
"note": self.note,
"action": self.action,
- "action_url": self.get_action_url(),
}
return row
@@ -53,8 +40,6 @@ def get_table_dictionary(self):
class VerifiedShared(UploadWorkspaceSharingAuditResult):
"""Audit results class for when Sharing has been verified."""
- is_shared: bool = True
-
def __str__(self):
return f"Verified sharing: {self.note}"
@@ -63,8 +48,6 @@ def __str__(self):
class VerifiedNotShared(UploadWorkspaceSharingAuditResult):
"""Audit results class for when no Sharing has been verified."""
- is_shared: bool = False
-
def __str__(self):
return f"Verified not shared: {self.note}"
@@ -73,7 +56,6 @@ def __str__(self):
class ShareAsReader(UploadWorkspaceSharingAuditResult):
"""Audit results class for when Sharing should be granted as a reader."""
- is_shared: bool = False
action: str = "Share as reader"
def __str__(self):
@@ -84,7 +66,6 @@ def __str__(self):
class ShareAsWriter(UploadWorkspaceSharingAuditResult):
"""Audit results class for when Sharing should be granted as a writer."""
- is_shared: bool = False
action: str = "Share as writer"
def __str__(self):
@@ -95,7 +76,6 @@ def __str__(self):
class ShareAsOwner(UploadWorkspaceSharingAuditResult):
"""Audit results class for when Sharing should be granted as an owner."""
- is_shared: bool = False
action: str = "Share as owner"
def __str__(self):
@@ -106,7 +86,6 @@ def __str__(self):
class ShareWithCompute(UploadWorkspaceSharingAuditResult):
"""Audit results class for when Sharing should be granted with compute access."""
- is_shared: bool = False
action: str = "Share with compute"
def __str__(self):
@@ -117,7 +96,6 @@ def __str__(self):
class StopSharing(UploadWorkspaceSharingAuditResult):
"""Audit results class for when Sharing should be removed for a known reason."""
- is_shared: bool = True
action: str = "Stop sharing"
def __str__(self):
@@ -136,11 +114,9 @@ class UploadWorkspaceSharingAuditTable(tables.Table):
workspace = tables.Column(linkify=True)
managed_group = tables.Column(linkify=True)
- # is_shared = tables.Column()
access = tables.Column(verbose_name="Current access")
can_compute = BooleanIconColumn(show_false_icon=True, null=True, true_color="green", false_color="black")
note = tables.Column()
- # action = tables.Column()
action = tables.TemplateColumn(
template_name="gregor_anvil/snippets/upload_workspace_sharing_audit_action_button.html"
)
From e3fa75157aa5351b81f5b34e65ef0f2b6ba305c9 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 3 Sep 2024 12:02:59 -0700
Subject: [PATCH 08/97] Fix tests for auth domain audit classes
---
gregor_django/gregor_anvil/tests/test_audit.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/gregor_django/gregor_anvil/tests/test_audit.py b/gregor_django/gregor_anvil/tests/test_audit.py
index 83f4975e..7c0b4b10 100644
--- a/gregor_django/gregor_anvil/tests/test_audit.py
+++ b/gregor_django/gregor_anvil/tests/test_audit.py
@@ -4013,7 +4013,7 @@ def test_member_as_admin(self):
child_group=group,
role=GroupGroupMembership.ADMIN,
)
- instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ instance = upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditResult(
workspace=upload_workspace,
managed_group=group,
current_membership_instance=membership,
@@ -4030,7 +4030,7 @@ def test_member_as_member(self):
child_group=group,
role=GroupGroupMembership.MEMBER,
)
- instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ instance = upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditResult(
workspace=upload_workspace,
managed_group=group,
current_membership_instance=membership,
@@ -4042,7 +4042,7 @@ def test_member_as_member(self):
def test_not_member(self):
upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
group = ManagedGroupFactory.create()
- instance = upload_workspace_sharing_audit.UploadWorkspaceSharingAuditResult(
+ instance = upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditResult(
workspace=upload_workspace,
managed_group=group,
current_membership_instance=None,
From 1cc3f8783693cc9891b1f19df3ff892f631c9d38 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 3 Sep 2024 12:04:39 -0700
Subject: [PATCH 09/97] Shorten error explanation for sharing audit
---
.../snippets/upload_workspace_sharing_audit_explanation.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_sharing_audit_explanation.html b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_sharing_audit_explanation.html
index af07d58f..cdf2a379 100644
--- a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_sharing_audit_explanation.html
+++ b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_sharing_audit_explanation.html
@@ -36,7 +36,7 @@ Errors
- - The workspace has been shared with an unexpected group as an "OWNER".
+ - The workspace has been shared with an unexpected group.
From f4552b1d8b895148f179d43fc440951352daeb4f Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 3 Sep 2024 12:07:02 -0700
Subject: [PATCH 10/97] Fix auth domain audit explanation for final changes
The auth domain audit explanation still had the previous behavior
(before Cat's review and subsequent changes). Update the explanation
to reflect the actual behavior.
---
...ad_workspace_auth_domain_audit_explanation.html | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
index edd9af6c..8eca84f7 100644
--- a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
+++ b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
@@ -12,6 +12,12 @@
From d3ff69d25da8684e0ad28c5ac3b0d23008eed84f Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 16:27:06 +0000
Subject: [PATCH 39/97] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.6.5 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.5...v0.6.9)
- [github.com/gitleaks/gitleaks: v8.19.2 → v8.20.0](https://github.com/gitleaks/gitleaks/compare/v8.19.2...v8.20.0)
---
.pre-commit-config.yaml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 500af5b1..c976519c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,10 +1,10 @@
exclude: 'docs|node_modules|migrations|.git|.tox'
-default_stages: [commit]
+default_stages: [Nonepre-commitNone]
fail_fast: true
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -12,7 +12,7 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
- rev: v0.6.5
+ rev: v0.6.9
hooks:
# Run the linter.
- id: ruff
@@ -21,7 +21,7 @@ repos:
- id: ruff-format
- repo: https://github.com/gitleaks/gitleaks
- rev: v8.19.2
+ rev: v8.20.0
hooks:
- id: gitleaks
From cfeedbf60853d4882aeddcb5ba485c0517be8022 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp <3944584+amstilp@users.noreply.github.com>
Date: Mon, 7 Oct 2024 10:35:05 -0700
Subject: [PATCH 40/97] Fix default_stages incorrect update
The default_stages value was incorrectly updated by the pre-commit update bot, which broke pre-commit. Fix it to be "pre-commit" instead of "commit" or "Nonepre-commitNone".
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c976519c..808e8b7b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
exclude: 'docs|node_modules|migrations|.git|.tox'
-default_stages: [Nonepre-commitNone]
+default_stages: [pre-commit]
fail_fast: true
repos:
From 343c0211c5700544c8dfd489fa25426b00ee5ec6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 17:44:48 +0000
Subject: [PATCH 41/97] Bump codecov/codecov-action from 4.5.0 to 4.6.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0)
---
updated-dependencies:
- dependency-name: codecov/codecov-action
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4e094143..00051e4b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -140,6 +140,6 @@ jobs:
python -m coverage report
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
From 46126e6a19249b08883583264815c0739ccbb498 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 17:44:57 +0000
Subject: [PATCH 42/97] Bump actions/checkout from 4.1.7 to 4.2.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.1.7...v4.2.1)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 4 ++--
.github/workflows/gitleaks.yml | 2 +-
.github/workflows/pip-compile.yml | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4e094143..dfb8827d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,7 +54,7 @@ jobs:
steps:
- name: Checkout Code Repository
- uses: actions/checkout@v4.1.7
+ uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5.2.0
@@ -111,7 +111,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
- uses: actions/checkout@v4.1.7
+ uses: actions/checkout@v4.2.1
- name: Set up Python
uses: actions/setup-python@v5.2.0
diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml
index f6cca493..b898bbac 100644
--- a/.github/workflows/gitleaks.yml
+++ b/.github/workflows/gitleaks.yml
@@ -10,7 +10,7 @@ jobs:
name: gitleaks
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4.1.7
+ - uses: actions/checkout@v4.2.1
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2.3.6
diff --git a/.github/workflows/pip-compile.yml b/.github/workflows/pip-compile.yml
index 93c60d63..9f906bd1 100644
--- a/.github/workflows/pip-compile.yml
+++ b/.github/workflows/pip-compile.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout Code Repository
- uses: actions/checkout@v4.1.7
+ uses: actions/checkout@v4.2.1
with:
ref: ${{ github.head_ref }}
From 5b75f78b3b9fc16a6e3d0aebfe87d786f4a29fc2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 17:45:16 +0000
Subject: [PATCH 43/97] Bump ruff from 0.6.5 to 0.6.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.5 to 0.6.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.5...0.6.9)
---
updated-dependencies:
- dependency-name: ruff
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 668dfdde..67d8a4b7 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -150,7 +150,7 @@ requests==2.32.3
# -c requirements/requirements.txt
# -c requirements/test-requirements.txt
# sphinx
-ruff==0.6.5
+ruff==0.6.9
# via -r requirements/dev-requirements.in
six==1.16.0
# via
From aa6e25b5428d8fe20e6814404c5aa4689c988199 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 17:45:32 +0000
Subject: [PATCH 44/97] Bump sphinx-autobuild from 2024.9.17 to 2024.10.3
Bumps [sphinx-autobuild](https://github.com/sphinx-doc/sphinx-autobuild) from 2024.9.17 to 2024.10.3.
- [Release notes](https://github.com/sphinx-doc/sphinx-autobuild/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx-autobuild/blob/main/NEWS.rst)
- [Commits](https://github.com/sphinx-doc/sphinx-autobuild/compare/2024.09.17...2024.10.03)
---
updated-dependencies:
- dependency-name: sphinx-autobuild
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 668dfdde..28bd67d1 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -165,7 +165,7 @@ sphinx==8.0.2
# via
# -r requirements/dev-requirements.in
# sphinx-autobuild
-sphinx-autobuild==2024.9.17
+sphinx-autobuild==2024.10.3
# via -r requirements/dev-requirements.in
sphinxcontrib-applehelp==1.0.4
# via sphinx
From 66cd1168460d09c724d8c72ea92a8b2018422bab Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 17:45:50 +0000
Subject: [PATCH 45/97] Bump django-constance from 4.1.1 to 4.1.2
Bumps [django-constance](https://github.com/jazzband/django-constance) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/jazzband/django-constance/releases)
- [Changelog](https://github.com/jazzband/django-constance/blob/master/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-constance/compare/4.1.1...4.1.2)
---
updated-dependencies:
- dependency-name: django-constance
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index a314a609..6e83e7b4 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -57,7 +57,7 @@ django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-con
# via -r requirements/requirements.in
django-autocomplete-light==3.11.0
# via django-anvil-consortium-manager
-django-constance==4.1.1
+django-constance==4.1.2
# via -r requirements/requirements.in
django-crispy-forms==2.3
# via
From e25bd064533e80946fced92604d352f4c346a87f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 19:18:21 +0000
Subject: [PATCH 46/97] Bump crispy-bootstrap5 from 2024.2 to 2024.10
Bumps [crispy-bootstrap5](https://github.com/django-crispy-forms/crispy-bootstrap5) from 2024.2 to 2024.10.
- [Release notes](https://github.com/django-crispy-forms/crispy-bootstrap5/releases)
- [Changelog](https://github.com/django-crispy-forms/crispy-bootstrap5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/django-crispy-forms/crispy-bootstrap5/compare/2024.2...2024.10)
---
updated-dependencies:
- dependency-name: crispy-bootstrap5
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 6e83e7b4..fb4316be 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -27,7 +27,7 @@ charset-normalizer==3.3.2
# via requests
click==8.1.7
# via pip-tools
-crispy-bootstrap5==2024.2
+crispy-bootstrap5==2024.10
# via
# -r requirements/requirements.in
# django-anvil-consortium-manager
From 5475f65f76d52f7ebd38489b87618a503669c4e3 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Mon, 7 Oct 2024 12:57:08 -0700
Subject: [PATCH 47/97] Add a view to audit auth domain membership for a single
combined workspace
---
.../gregor_anvil/tests/test_views.py | 303 ++++++++++++++++++
gregor_django/gregor_anvil/urls.py | 10 +-
gregor_django/gregor_anvil/views.py | 36 ++-
3 files changed, 342 insertions(+), 7 deletions(-)
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index 2cb6dabd..305d6c57 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -9215,3 +9215,306 @@ def test_title(self):
response = self.client.get(self.get_url())
# self.assertContains(response, str(self.workspace))
self.assertIn("all combined workspaces", response.content.decode().lower())
+
+
+class CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspaceTest(AnVILAPIMockTestMixin, TestCase):
+ """Tests for the CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace view."""
+
+ def setUp(self):
+ """Set up test class."""
+ super().setUp()
+ self.factory = RequestFactory()
+ # Create a user with both view and edit permission.
+ self.user = User.objects.create_user(username="test", password="test")
+ self.user.user_permissions.add(
+ Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME)
+ )
+ self.workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+
+ def get_url(self, *args):
+ """Get the url for the view being tested."""
+ return reverse(
+ "gregor_anvil:audit:combined_workspaces:auth_domains:by_workspace",
+ args=args,
+ )
+
+ def get_view(self):
+ """Return the view being tested."""
+ return views.CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace.as_view()
+
+ def test_view_redirect_not_logged_in(self):
+ "View redirects to login view when user is not logged in."
+ # Need a client for redirects.
+ response = self.client.get(
+ self.get_url(self.workspace.workspace.billing_project.name, self.workspace.workspace.name)
+ )
+ self.assertRedirects(
+ response,
+ resolve_url(settings.LOGIN_URL)
+ + "?next="
+ + self.get_url(self.workspace.workspace.billing_project.name, self.workspace.workspace.name),
+ )
+
+ def test_status_code_with_user_permission_view(self):
+ """Returns successful response code if the user has view permission."""
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(self.workspace.workspace.billing_project.name, self.workspace.workspace.name)
+ )
+ self.assertEqual(response.status_code, 200)
+
+ def test_access_without_user_permission(self):
+ """Raises permission denied if user has no permissions."""
+ user_no_perms = User.objects.create_user(username="test-none", password="test-none")
+ request = self.factory.get(self.get_url("foo", "bar"))
+ request.user = user_no_perms
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
+ def test_invalid_billing_project_name(self):
+ """Raises a 404 error with an invalid object billing project."""
+ request = self.factory.get(self.get_url("foo", self.workspace.workspace.name))
+ request.user = self.user
+ with self.assertRaises(Http404):
+ self.get_view()(
+ request,
+ billing_project_slug="foo",
+ workspace_slug=self.workspace.workspace.name,
+ )
+
+ def test_invalid_workspace_name(self):
+ """Raises a 404 error with an invalid workspace name."""
+ request = self.factory.get(self.get_url(self.workspace.workspace.billing_project.name, "foo"))
+ request.user = self.user
+ with self.assertRaises(Http404):
+ self.get_view()(
+ request,
+ billing_project_slug=self.workspace.workspace.billing_project.name,
+ workspace_slug="foo",
+ )
+
+ def test_context_audit_results(self):
+ """The audit_results exists in the context."""
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ self.assertIn("audit_results", response.context_data)
+ audit_results = response.context_data["audit_results"]
+ self.assertIsInstance(
+ audit_results,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit,
+ )
+ self.assertTrue(audit_results.completed)
+ self.assertEqual(audit_results.queryset.count(), 1)
+ self.assertIn(self.workspace, audit_results.queryset)
+
+ def test_context_audit_results_does_not_include_other_workspaces(self):
+ """The audit_results does not include other workspaces."""
+ other_workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ audit_results = response.context_data["audit_results"]
+ self.assertEqual(audit_results.queryset.count(), 1)
+ self.assertNotIn(other_workspace, audit_results.queryset)
+
+ def test_context_verified_table_access(self):
+ """verified_table shows a record when audit has verified access."""
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ self.assertIn("verified_table", response.context_data)
+ table = response.context_data["verified_table"]
+ self.assertIsInstance(
+ table,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.workspace.workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.ADMIN)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.DCC_ADMIN_AS_ADMIN,
+ )
+ self.assertEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_needs_action_table_add_member(self):
+ """needs_action_table shows a record when audit finds that access needs to be granted."""
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ self.assertIn("needs_action_table", response.context_data)
+ table = response.context_data["needs_action_table"]
+ self.assertIsInstance(
+ table,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.workspace.workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertIsNone(table.rows[0].get_cell_value("role"))
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.GREGOR_ALL_AS_MEMBER,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_needs_action_table_add_admin(self):
+ """needs_action_table shows a record when audit finds that access needs to be granted."""
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ self.assertIn("needs_action_table", response.context_data)
+ table = response.context_data["needs_action_table"]
+ self.assertIsInstance(
+ table,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.workspace.workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertIsNone(table.rows[0].get_cell_value("role"))
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.DCC_ADMIN_AS_ADMIN,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_error_table_remove(self):
+ """error table shows a record when audit finds that access needs to be removed."""
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ self.assertIn("errors_table", response.context_data)
+ table = response.context_data["errors_table"]
+ self.assertIsInstance(
+ table,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.workspace.workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.MEMBER)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.OTHER_GROUP,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_errors_table_change_to_member(self):
+ """errors table shows a record when audit finds that access needs to be removed."""
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ self.assertIn("errors_table", response.context_data)
+ table = response.context_data["errors_table"]
+ self.assertIsInstance(
+ table,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.workspace.workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.ADMIN)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.GREGOR_ALL_AS_MEMBER,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_needs_action_table_change_to_admin(self):
+ """error table shows a record when audit finds that access needs to be removed."""
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ self.assertIn("needs_action_table", response.context_data)
+ table = response.context_data["needs_action_table"]
+ self.assertIsInstance(
+ table,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.workspace.workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.MEMBER)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.DCC_ADMIN_AS_ADMIN,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_title(self):
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.workspace.workspace.billing_project.name,
+ self.workspace.workspace.name,
+ )
+ )
+ self.assertContains(response, str(self.workspace))
+ # self.assertIn("all combined workspaces", response.content.decode().lower())
diff --git a/gregor_django/gregor_anvil/urls.py b/gregor_django/gregor_anvil/urls.py
index d12b8476..1a996136 100644
--- a/gregor_django/gregor_anvil/urls.py
+++ b/gregor_django/gregor_anvil/urls.py
@@ -123,11 +123,11 @@
# views.UploadWorkspaceAuthDomainAuditResolve.as_view(),
# name="resolve",
# ),
- # path(
- # "//",
- # views.CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace.as_view(),
- # name="by_workspace",
- # ),
+ path(
+ "//",
+ views.CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace.as_view(),
+ name="by_workspace",
+ ),
],
"auth_domains",
)
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index e55a82f7..c41f4e39 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -785,10 +785,42 @@ def get_context_data(self, **kwargs):
return context
-class CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace(AnVILConsortiumManagerStaffEditRequired, DetailView):
+class CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace(AnVILConsortiumManagerStaffViewRequired, DetailView):
"""View to audit auth domain membership for a specific CombinedConsortiumDataWorkspace."""
- pass
+ template_name = "gregor_anvil/combinedconsortiumdataworkspace_auth_domain_audit.html"
+ model = models.CombinedConsortiumDataWorkspace
+
+ def get_object(self, queryset=None):
+ """Look up the CombinedConsortiumDataWorkspace by billing project and name."""
+ # Filter the queryset based on kwargs.
+ billing_project_slug = self.kwargs.get("billing_project_slug", None)
+ workspace_slug = self.kwargs.get("workspace_slug", None)
+ queryset = models.CombinedConsortiumDataWorkspace.objects.filter(
+ workspace__billing_project__name=billing_project_slug,
+ workspace__name=workspace_slug,
+ )
+ try:
+ # Get the single item from the filtered queryset
+ obj = queryset.get()
+ except queryset.model.DoesNotExist:
+ raise Http404(
+ _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name}
+ )
+ return obj
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ # Run the audit.
+ audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit(
+ queryset=self.model.objects.filter(pk=self.object.pk)
+ )
+ audit.run_audit()
+ context["verified_table"] = audit.get_verified_table()
+ context["errors_table"] = audit.get_errors_table()
+ context["needs_action_table"] = audit.get_needs_action_table()
+ context["audit_results"] = audit
+ return context
class CombinedConsortiumDataWorkspaceAuthDomainAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
From 19bc38da1c4f607a64021210b540fd4e73c0f6b0 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Mon, 7 Oct 2024 14:49:37 -0700
Subject: [PATCH 48/97] Change audit class when DCC admins is a member
It should be ChangeToAdmin, not AddMember. Fix the audit and tests.
---
gregor_django/gregor_anvil/audit/combined_workspace_audit.py | 2 ++
gregor_django/gregor_anvil/tests/test_audit.py | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/gregor_django/gregor_anvil/audit/combined_workspace_audit.py b/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
index 1a91d937..cf35bec2 100644
--- a/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
+++ b/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
@@ -96,6 +96,8 @@ def _audit_workspace_and_dcc_admin_group(self, combined_workspace, managed_group
if current_membership and current_membership.role == GroupGroupMembership.ADMIN:
self.verified.append(workspace_auth_domain_audit_results.VerifiedAdmin(**audit_result_args))
+ elif current_membership and current_membership.role == GroupGroupMembership.MEMBER:
+ self.needs_action.append(workspace_auth_domain_audit_results.ChangeToAdmin(**audit_result_args))
else:
self.needs_action.append(workspace_auth_domain_audit_results.AddAdmin(**audit_result_args))
diff --git a/gregor_django/gregor_anvil/tests/test_audit.py b/gregor_django/gregor_anvil/tests/test_audit.py
index baaaae43..8f8125eb 100644
--- a/gregor_django/gregor_anvil/tests/test_audit.py
+++ b/gregor_django/gregor_anvil/tests/test_audit.py
@@ -7609,7 +7609,7 @@ def test_dcc_admin_as_member(self):
self.assertEqual(len(audit.needs_action), 1)
self.assertEqual(len(audit.errors), 0)
record = audit.needs_action[0]
- self.assertIsInstance(record, workspace_auth_domain_audit_results.AddAdmin)
+ self.assertIsInstance(record, workspace_auth_domain_audit_results.ChangeToAdmin)
self.assertEqual(record.workspace, self.combined_workspace.workspace)
self.assertEqual(record.managed_group, self.dcc_admin_group)
self.assertEqual(record.current_membership_instance, membership)
@@ -7874,7 +7874,7 @@ def test_dcc_admin_as_member(self):
self.assertEqual(len(audit.needs_action), 1)
self.assertEqual(len(audit.errors), 0)
record = audit.needs_action[0]
- self.assertIsInstance(record, workspace_auth_domain_audit_results.AddAdmin)
+ self.assertIsInstance(record, workspace_auth_domain_audit_results.ChangeToAdmin)
self.assertEqual(record.workspace, self.combined_workspace.workspace)
self.assertEqual(record.managed_group, self.dcc_admin_group)
self.assertEqual(record.current_membership_instance, membership)
From 6808d64ecf8441b7c709f86b31581234b91a68cf Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Mon, 7 Oct 2024 15:21:08 -0700
Subject: [PATCH 49/97] Add view to resolve auth domain audits for combined
workspaces
---
.../gregor_anvil/tests/test_views.py | 1307 +++++++++++++++++
gregor_django/gregor_anvil/urls.py | 10 +-
gregor_django/gregor_anvil/views.py | 112 +-
3 files changed, 1422 insertions(+), 7 deletions(-)
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index 305d6c57..e49f67f8 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -9518,3 +9518,1310 @@ def test_title(self):
)
self.assertContains(response, str(self.workspace))
# self.assertIn("all combined workspaces", response.content.decode().lower())
+
+
+class CombinedConsortiumDataWorkspaceAuthDomainAuditResolveTest(AnVILAPIMockTestMixin, TestCase):
+ def setUp(self):
+ """Set up test class."""
+ super().setUp()
+ self.factory = RequestFactory()
+ # Create a user with both view and edit permission.
+ self.user = User.objects.create_user(username="test", password="test")
+ self.user.user_permissions.add(
+ Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME)
+ )
+ self.user.user_permissions.add(
+ Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME)
+ )
+
+ def get_url(self, *args):
+ """Get the url for the view being tested."""
+ return reverse(
+ "gregor_anvil:audit:combined_workspaces:auth_domains:resolve",
+ args=args,
+ )
+
+ def get_view(self):
+ """Return the view being tested."""
+ return views.CombinedConsortiumDataWorkspaceAuthDomainAuditResolve.as_view()
+
+ def test_view_redirect_not_logged_in(self):
+ "View redirects to login view when user is not logged in."
+ # Need a client for redirects.
+ response = self.client.get(self.get_url("foo", "bar", "foobar"))
+ self.assertRedirects(
+ response,
+ resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar", "foobar"),
+ )
+
+ def test_status_code_with_user_permission_staff_edit(self):
+ """Returns successful response code if the user has staff edit permission."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+
+ def test_status_code_with_user_permission_staff_view(self):
+ """Returns 403 response code if the user has staff view permission."""
+ user_view = User.objects.create_user(username="test-view", password="test-view")
+ user_view.user_permissions.add(
+ Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME)
+ )
+ self.client.force_login(self.user)
+ request = self.factory.get(self.get_url("foo", "bar", "foobar"))
+ request.user = user_view
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
+ def test_status_code_with_user_permission_view(self):
+ """Returns forbidden response code if the user has view permission."""
+ user = User.objects.create_user(username="test-none", password="test-none")
+ user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME))
+ request = self.factory.get(self.get_url("foo", "bar", "foobar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
+ def test_access_without_user_permission(self):
+ """Raises permission denied if user has no permissions."""
+ user_no_perms = User.objects.create_user(username="test-none", password="test-none")
+ request = self.factory.get(self.get_url("foo", "bar", "foobar"))
+ request.user = user_no_perms
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
+ def test_get_billing_project_does_not_exist(self):
+ """Raises a 404 error with an invalid billing project."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url("foo", workspace.workspace.name, group.name))
+ self.assertEqual(response.status_code, 404)
+
+ def test_get_workspace_name_does_not_exist(self):
+ """Raises a 404 error with an invalid billing project."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(workspace.workspace.billing_project.name, "foo", group.name))
+ self.assertEqual(response.status_code, 404)
+
+ def test_get_group_does_not_exist(self):
+ """get request raises a 404 error with an non-existent email."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ "foo",
+ )
+ )
+ self.assertEqual(response.status_code, 404)
+
+ def test_get_context_audit_result(self):
+ """The audit_results exists in the context."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ self.assertIsInstance(
+ response.context_data["audit_result"],
+ workspace_auth_domain_audit_results.WorkspaceAuthDomainAuditResult,
+ )
+
+ def test_get_verified_admin(self):
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ group.name,
+ )
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, workspace_auth_domain_audit_results.VerifiedAdmin)
+ self.assertEqual(audit_result.workspace, workspace.workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.DCC_ADMIN_AS_ADMIN,
+ )
+
+ def test_get_verified_member(self):
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ group.name,
+ )
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, workspace_auth_domain_audit_results.VerifiedMember)
+ self.assertEqual(audit_result.workspace, workspace.workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.GREGOR_ALL_AS_MEMBER,
+ )
+
+ def test_get_verified_not_member(self):
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ group.name,
+ )
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, workspace_auth_domain_audit_results.VerifiedNotMember)
+ self.assertEqual(audit_result.workspace, workspace.workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.OTHER_GROUP,
+ )
+
+ def test_get_add_member(self):
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ group.name,
+ )
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, workspace_auth_domain_audit_results.AddMember)
+ self.assertEqual(audit_result.workspace, workspace.workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.GREGOR_ALL_AS_MEMBER,
+ )
+
+ def test_get_add_admin(self):
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ group.name,
+ )
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, workspace_auth_domain_audit_results.AddAdmin)
+ self.assertEqual(audit_result.workspace, workspace.workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.DCC_ADMIN_AS_ADMIN,
+ )
+
+ def test_get_change_to_member(self):
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ group.name,
+ )
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, workspace_auth_domain_audit_results.ChangeToMember)
+ self.assertEqual(audit_result.workspace, workspace.workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.GREGOR_ALL_AS_MEMBER,
+ )
+
+ def test_get_change_to_admin(self):
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ group.name,
+ )
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, workspace_auth_domain_audit_results.ChangeToAdmin)
+ self.assertEqual(audit_result.workspace, workspace.workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.DCC_ADMIN_AS_ADMIN,
+ )
+
+ def test_get_remove(self):
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ group.name,
+ )
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, workspace_auth_domain_audit_results.Remove)
+ self.assertEqual(audit_result.workspace, workspace.workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note,
+ combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit.OTHER_GROUP,
+ )
+
+ def test_post_billing_project_does_not_exist(self):
+ """Raises a 404 error with an invalid billing project."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(self.get_url("foo", workspace.workspace.name, group.name))
+ self.assertEqual(response.status_code, 404)
+
+ def test_post_workspace_name_does_not_exist(self):
+ """Raises a 404 error with an invalid billing project."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(self.get_url(workspace.workspace.billing_project.name, "foo", group.name))
+ self.assertEqual(response.status_code, 404)
+
+ def test_post_group_does_not_exist(self):
+ """post request raises a 404 error with an non-existent email."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(
+ workspace.workspace.billing_project.name,
+ workspace.workspace.name,
+ "foo",
+ )
+ )
+ self.assertEqual(response.status_code, 404)
+
+ def test_post_verified_member(self):
+ """Get request with VerifiedMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_post_verified_admin(self):
+ """Get request with VerifiedAdmin result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_post_verified_not_member(self):
+ """Get request with VerifiedNotMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_add_member(self):
+ """Get request with AddMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership = acm_models.GroupGroupMembership.objects.get(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+
+ def test_post_add_admin(self):
+ """Get request with AddAdmin result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership = acm_models.GroupGroupMembership.objects.get(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+
+ def test_post_change_to_member(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertGreater(membership.modified, membership.created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+
+ def test_post_change_to_admin(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertGreater(membership.modified, membership.created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+
+ def test_post_remove_admin(self):
+ """Post request with Remove result for an admin membership."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_remove_member(self):
+ """Post request with Remove result for a member membership."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_verified_member_htmx(self):
+ """Get request with VerifiedMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_post_verified_admin_htmx(self):
+ """Get request with VerifiedAdmin result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_post_verified_not_member_htmx(self):
+ """Get request with VerifiedNotMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_add_member_htmx(self):
+ """Get request with AddMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership = acm_models.GroupGroupMembership.objects.get(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+
+ def test_post_add_admin_htmx(self):
+ """Get request with AddAdmin result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership = acm_models.GroupGroupMembership.objects.get(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+
+ def test_post_change_to_member_htmx(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertGreater(membership.modified, membership.created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+
+ def test_post_change_to_admin_htmx(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertGreater(membership.modified, membership.created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+
+ def test_post_remove_admin_htmx(self):
+ """Post request with Remove result for an admin membership."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_remove_member_htmx(self):
+ """Post request with Remove result for a member membership."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_api_error_add_member(self):
+ """Get request with AddMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # No memberships were created.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_add_admin(self):
+ """Get request with AddAdmin result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # No memberships were created.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_change_to_member_error_on_put_call(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_change_to_member_error_on_delete_call(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_change_to_admin_error_on_delete_call(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_change_to_admin_error_on_put_call(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_remove_admin(self):
+ """Post request with Remove result for an admin membership."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_remove_member(self):
+ """Post request with Remove result for a member membership."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_htmx_add_member(self):
+ """Get request with AddMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ # No memberships were created.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+ # No message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_api_error_htmx_add_admin(self):
+ """Get request with AddAdmin result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # No memberships were created.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+ # No message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_api_error_htmx_change_to_member_error_on_put_call(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # No message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_api_error_htmx_change_to_member_error_on_delete_call(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # No message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_api_error_htmx_change_to_admin_error_on_delete_call(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # No message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_api_error_htmx_change_to_admin_error_on_put_call(self):
+ """Get request with ChangeToMember result."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # No message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_api_error_htmx_remove_admin(self):
+ """Post request with Remove result for an admin membership."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # No message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_api_error_htmx_remove_member(self):
+ """Post request with Remove result for a member membership."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(workspace__name="test-ws")
+ group = acm_factories.ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(workspace.workspace.billing_project.name, workspace.workspace.name, group.name), **header
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # No message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
diff --git a/gregor_django/gregor_anvil/urls.py b/gregor_django/gregor_anvil/urls.py
index 1a996136..6e7e7939 100644
--- a/gregor_django/gregor_anvil/urls.py
+++ b/gregor_django/gregor_anvil/urls.py
@@ -118,11 +118,11 @@
combined_workspace_auth_domain_audit_patterns = (
[
path("all/", views.CombinedConsortiumDataWorkspaceAuthDomainAudit.as_view(), name="all"),
- # path(
- # "resolve////",
- # views.UploadWorkspaceAuthDomainAuditResolve.as_view(),
- # name="resolve",
- # ),
+ path(
+ "resolve////",
+ views.CombinedConsortiumDataWorkspaceAuthDomainAuditResolve.as_view(),
+ name="resolve",
+ ),
path(
"//",
views.CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace.as_view(),
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index c41f4e39..b1e90636 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -824,6 +824,114 @@ def get_context_data(self, **kwargs):
class CombinedConsortiumDataWorkspaceAuthDomainAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
- """View to resolve CombinedConsortiumDataWorkspace auth domain audit results."""
+ """View to resolve UploadWorkspace auth domain audit results."""
+
+ form_class = Form
+ template_name = "gregor_anvil/combinedconsortiumdataworkspace_auth_domain_audit_resolve.html"
+ htmx_success = """ Handled!"""
+ htmx_error = """ Error!"""
+
+ def get_workspace(self):
+ """Look up the CombinedConsortiumDataWorkspace by billing project and name."""
+ # Filter the queryset based on kwargs.
+ billing_project_slug = self.kwargs.get("billing_project_slug", None)
+ workspace_slug = self.kwargs.get("workspace_slug", None)
+ queryset = models.CombinedConsortiumDataWorkspace.objects.filter(
+ workspace__billing_project__name=billing_project_slug,
+ workspace__name=workspace_slug,
+ )
+ try:
+ # Get the single item from the filtered queryset
+ obj = queryset.get()
+ except queryset.model.DoesNotExist:
+ raise Http404(
+ _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name}
+ )
+ return obj
+
+ def get_managed_group(self, queryset=None):
+ """Look up the ManagedGroup by name."""
+ try:
+ obj = ManagedGroup.objects.get(name=self.kwargs.get("managed_group_slug", None))
+ except ManagedGroup.DoesNotExist:
+ raise Http404("No ManagedGroups found matching the query")
+ return obj
+
+ def get_audit_result(self):
+ audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit()
+ # No way to set the group queryset, since it is dynamically determined by the workspace.
+ audit.audit_workspace_and_group(self.workspace, self.managed_group)
+ # Set to completed, because we are just running this one specific check.
+ audit.completed = True
+ return audit.get_all_results()[0]
+
+ def get(self, request, *args, **kwargs):
+ self.workspace = self.get_workspace()
+ self.managed_group = self.get_managed_group()
+ self.audit_result = self.get_audit_result()
+ return super().get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ self.workspace = self.get_workspace()
+ self.managed_group = self.get_managed_group()
+ self.audit_result = self.get_audit_result()
+ return super().post(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["workspace"] = self.workspace
+ context["managed_group"] = self.managed_group
+ context["audit_result"] = self.audit_result
+ return context
+
+ def get_success_url(self):
+ return self.workspace.get_absolute_url()
- pass
+ def form_valid(self, form):
+ # Handle the result.
+ try:
+ with transaction.atomic():
+ # Set up the membership instance.
+ if self.audit_result.current_membership_instance:
+ membership = self.audit_result.current_membership_instance
+ else:
+ membership = GroupGroupMembership(
+ parent_group=self.workspace.workspace.authorization_domains.first(),
+ child_group=self.managed_group,
+ )
+ # Now process the result.
+ if isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedMember):
+ pass
+ elif isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedAdmin):
+ pass
+ elif isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedNotMember):
+ pass
+ elif isinstance(self.audit_result, workspace_auth_domain_audit_results.Remove):
+ membership.anvil_delete()
+ membership.delete()
+ else:
+ if isinstance(self.audit_result, workspace_auth_domain_audit_results.ChangeToMember):
+ membership.anvil_delete()
+ membership.role = GroupGroupMembership.MEMBER
+ elif isinstance(self.audit_result, workspace_auth_domain_audit_results.ChangeToAdmin):
+ membership.anvil_delete()
+ membership.role = GroupGroupMembership.ADMIN
+ else:
+ if isinstance(self.audit_result, workspace_auth_domain_audit_results.AddMember):
+ membership.role = GroupGroupMembership.MEMBER
+ elif isinstance(self.audit_result, workspace_auth_domain_audit_results.AddAdmin):
+ membership.role = GroupGroupMembership.ADMIN
+ membership.full_clean()
+ membership.save()
+ membership.anvil_create()
+ except (AnVILAPIError, AnVILGroupNotFound) as e:
+ if self.request.htmx:
+ return HttpResponse(self.htmx_error)
+ else:
+ messages.error(self.request, "AnVIL API Error: " + str(e))
+ return super().form_invalid(form)
+ # Otherwise, the audit resolution succeeded.
+ if self.request.htmx:
+ return HttpResponse(self.htmx_success)
+ else:
+ return super().form_valid(form)
From de6f45c9c0f6a70228eebb2be48343684f810f60 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Mon, 7 Oct 2024 15:24:47 -0700
Subject: [PATCH 50/97] Remove accidentally-added test
I had added this test to a different TestCase thinking it had been
missed earlier, but I was incorrect - the test in question is not
needed in the TestCase, and already exists where it is needed.
---
gregor_django/gregor_anvil/tests/test_views.py | 7 -------
1 file changed, 7 deletions(-)
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index e49f67f8..1f475108 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -4578,13 +4578,6 @@ def test_access_without_user_permission(self):
with self.assertRaises(PermissionDenied):
self.get_view()(request)
- def test_invalid_upload_cycle(self):
- """Raises a 404 error with an invalid upload cycle."""
- request = self.factory.get(self.get_url())
- request.user = self.user
- with self.assertRaises(Http404):
- self.get_view()(request)
-
def test_context_audit_results_no_upload_workspaces(self):
"""The audit_results exists in the context."""
self.client.force_login(self.user)
From 445a39d2d30a7accf9a6215d9c4b473b2bc3a6dc Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 8 Oct 2024 08:32:39 -0700
Subject: [PATCH 51/97] Start updating test data script for combined workspace
auditing
Also rename the file to have a more descriptive name - it's not
just upload workspaces anymore.
---
....py => add_upload_cycle_audit_test_data.py | 21 ++++++++++---------
1 file changed, 11 insertions(+), 10 deletions(-)
rename add_upload_workspace_audit_test_data.py => add_upload_cycle_audit_test_data.py (98%)
diff --git a/add_upload_workspace_audit_test_data.py b/add_upload_cycle_audit_test_data.py
similarity index 98%
rename from add_upload_workspace_audit_test_data.py
rename to add_upload_cycle_audit_test_data.py
index 704c19de..3758244b 100644
--- a/add_upload_workspace_audit_test_data.py
+++ b/add_upload_cycle_audit_test_data.py
@@ -28,7 +28,7 @@
)
-# Create a future upload cycle.
+## Future upload cycle.
upload_cycle = factories.UploadCycleFactory.create(
cycle=1,
is_future=True,
@@ -39,7 +39,7 @@
workspace__name="TEST_U01_RC1",
)
-# Create a current upload cycle before compute.
+## Current upload cycle before compute.
upload_cycle = factories.UploadCycleFactory.create(
cycle=2,
is_current=True,
@@ -111,7 +111,7 @@
)
-# Create a current upload cycle after compute.
+## Current upload cycle after compute.
upload_cycle = factories.UploadCycleFactory.create(
cycle=3,
is_current=True,
@@ -256,7 +256,7 @@
role=GroupGroupMembership.ADMIN,
)
-# Create a past upload cycle after QC is completed.
+## Past upload cycle after QC is completed.
upload_cycle = factories.UploadCycleFactory.create(
cycle=5,
is_past=True,
@@ -321,7 +321,7 @@
role=GroupGroupMembership.ADMIN,
)
-# Create a past upload cycle with a combined workspace.
+## Past upload cycle with a combined workspace.
upload_cycle = factories.UploadCycleFactory.create(
cycle=6,
is_past=True,
@@ -332,11 +332,6 @@
workspace__name="TEST_U06_RC1",
date_qc_completed=timezone.now(),
)
-factories.CombinedConsortiumDataWorkspaceFactory.create(
- upload_cycle=upload_cycle,
- date_completed=timezone.now(),
- workspace__name="TEST_U06_COMBINED",
-)
# Create records as appropriate for the previous point in the cycle - past cycle before QC complete.
# Auth domain.
WorkspaceGroupSharingFactory.create(
@@ -383,3 +378,9 @@
child_group=dcc_admin_group,
role=GroupGroupMembership.ADMIN,
)
+# Create the combined workspace and its records.
+factories.CombinedConsortiumDataWorkspaceFactory.create(
+ upload_cycle=upload_cycle,
+ date_completed=timezone.now(),
+ workspace__name="TEST_U06_COMBINED",
+)
From d23137ebec96a6e5cf46e228ccdbd1b4717babe3 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 8 Oct 2024 10:53:44 -0700
Subject: [PATCH 52/97] Update test data for combined workspace auditing
---
add_upload_cycle_audit_test_data.py | 49 +++++++++++++++++++++++++++--
1 file changed, 47 insertions(+), 2 deletions(-)
diff --git a/add_upload_cycle_audit_test_data.py b/add_upload_cycle_audit_test_data.py
index 3758244b..ded2a130 100644
--- a/add_upload_cycle_audit_test_data.py
+++ b/add_upload_cycle_audit_test_data.py
@@ -17,6 +17,7 @@
rc_1_uploader_group = ManagedGroupFactory(name="DEMO_RC1_UPLOADERS")
rc_1_nonmember_group = ManagedGroupFactory(name="DEMO_RC1_NONMEMBERS")
gregor_all_group = ManagedGroupFactory(name="GREGOR_ALL")
+combined_auth_domain = ManagedGroupFactory(name="AUTH_GREGOR_COMBINED")
# Create an RC
rc = factories.ResearchCenterFactory.create(
@@ -27,6 +28,18 @@
non_member_group=rc_1_nonmember_group,
)
+# Add GREGOR_ALL and DCC_ADMINS to the combined auth domain.
+GroupGroupMembershipFactory.create(
+ parent_group=combined_auth_domain,
+ child_group=gregor_all_group,
+ role=GroupGroupMembership.MEMBER,
+)
+GroupGroupMembershipFactory.create(
+ parent_group=combined_auth_domain,
+ child_group=dcc_admin_group,
+ role=GroupGroupMembership.ADMIN,
+)
+
## Future upload cycle.
upload_cycle = factories.UploadCycleFactory.create(
@@ -256,7 +269,7 @@
role=GroupGroupMembership.ADMIN,
)
-## Past upload cycle after QC is completed.
+## Past upload cycle after QC is completed; combined workspace is not complete.
upload_cycle = factories.UploadCycleFactory.create(
cycle=5,
is_past=True,
@@ -320,6 +333,16 @@
child_group=dcc_admin_group,
role=GroupGroupMembership.ADMIN,
)
+# Create the combined workspace and its records.
+combined_workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(
+ upload_cycle=upload_cycle,
+ workspace__name="TEST_U05_COMBINED",
+)
+# Delete the auth domain created by the factory and add the shared auth domain.
+combined_workspace.workspace.authorization_domains.clear()
+combined_workspace.workspace.authorization_domains.add(combined_auth_domain)
+# No sharing records yet.
+
## Past upload cycle with a combined workspace.
upload_cycle = factories.UploadCycleFactory.create(
@@ -379,8 +402,30 @@
role=GroupGroupMembership.ADMIN,
)
# Create the combined workspace and its records.
-factories.CombinedConsortiumDataWorkspaceFactory.create(
+combined_workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(
upload_cycle=upload_cycle,
date_completed=timezone.now(),
workspace__name="TEST_U06_COMBINED",
)
+# Delete the auth domain created by the factory and add the shared auth domain.
+combined_workspace.workspace.authorization_domains.clear()
+combined_workspace.workspace.authorization_domains.add(combined_auth_domain)
+# Add sharing records from previous step - DCC admins, writers, and members.
+WorkspaceGroupSharingFactory.create(
+ workspace=combined_workspace.workspace,
+ group=dcc_admin_group,
+ access=WorkspaceGroupSharing.OWNER,
+ can_compute=True,
+)
+WorkspaceGroupSharingFactory.create(
+ workspace=combined_workspace.workspace,
+ group=dcc_writer_group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=True,
+)
+WorkspaceGroupSharingFactory.create(
+ workspace=combined_workspace.workspace,
+ group=dcc_member_group,
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+)
From 7274f5428b695dbc19ddc7932fe5ce1695935d6c Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 8 Oct 2024 10:56:08 -0700
Subject: [PATCH 53/97] Make can_compute icons black in tables
For both the Combined workspace sharing audit and the upload workspace
sharing audit. Green/black or green/red imply errors vs correctness,
but black does not.
---
gregor_django/gregor_anvil/audit/combined_workspace_audit.py | 2 +-
gregor_django/gregor_anvil/audit/upload_workspace_audit.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/gregor_django/gregor_anvil/audit/combined_workspace_audit.py b/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
index cf35bec2..0885a4c6 100644
--- a/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
+++ b/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
@@ -190,7 +190,7 @@ class CombinedConsortiumDataWorkspaceSharingAuditTable(tables.Table):
workspace = tables.Column(linkify=True)
managed_group = tables.Column(linkify=True)
access = tables.Column(verbose_name="Current access")
- can_compute = BooleanIconColumn(show_false_icon=True, null=True, true_color="green", false_color="black")
+ can_compute = BooleanIconColumn(show_false_icon=True, null=True, true_color="black", false_color="black")
note = tables.Column()
action = tables.TemplateColumn(
template_name="gregor_anvil/snippets/combinedconsortiumdataworkspace_sharing_audit_action_button.html"
diff --git a/gregor_django/gregor_anvil/audit/upload_workspace_audit.py b/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
index 0fa69467..bde9201d 100644
--- a/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
+++ b/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
@@ -357,7 +357,7 @@ class UploadWorkspaceSharingAuditTable(tables.Table):
workspace = tables.Column(linkify=True)
managed_group = tables.Column(linkify=True)
access = tables.Column(verbose_name="Current access")
- can_compute = BooleanIconColumn(show_false_icon=True, null=True, true_color="green", false_color="black")
+ can_compute = BooleanIconColumn(show_false_icon=True, null=True, true_color="black", false_color="black")
note = tables.Column()
action = tables.TemplateColumn(
template_name="gregor_anvil/snippets/upload_workspace_sharing_audit_action_button.html"
From f488b1569efe9103b61e0062499f0470a809e635 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 8 Oct 2024 11:21:55 -0700
Subject: [PATCH 54/97] Add links to audit views on combined workspace detail
page
---
.../gregor_anvil/tests/test_views.py | 24 +++++++++++++++++++
...ombinedconsortiumdataworkspace_detail.html | 16 +++++++++++++
2 files changed, 40 insertions(+)
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index 1f475108..fc714590 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -1911,6 +1911,30 @@ def test_contains_upload_workspaces_from_previous_cycles(self):
self.assertIn(upload_workspace_1, response.context_data["upload_workspace_table"].data)
self.assertIn(upload_workspace_2, response.context_data["upload_workspace_table"].data)
+ def test_contains_sharing_audit_button(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(self.object.workspace.billing_project.name, self.object.workspace.name))
+ url = reverse(
+ "gregor_anvil:audit:combined_workspaces:sharing:by_workspace",
+ args=[
+ self.object.workspace.billing_project.name,
+ self.object.workspace.name,
+ ],
+ )
+ self.assertContains(response, url)
+
+ def test_contains_auth_domain_audit_button(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(self.object.workspace.billing_project.name, self.object.workspace.name))
+ url = reverse(
+ "gregor_anvil:audit:combined_workspaces:auth_domains:by_workspace",
+ args=[
+ self.object.workspace.billing_project.name,
+ self.object.workspace.name,
+ ],
+ )
+ self.assertContains(response, url)
+
def test_includes_date_completed(self):
self.object.date_completed = "2022-01-01"
self.object.save()
diff --git a/gregor_django/templates/gregor_anvil/combinedconsortiumdataworkspace_detail.html b/gregor_django/templates/gregor_anvil/combinedconsortiumdataworkspace_detail.html
index c3ee4efa..6727465a 100644
--- a/gregor_django/templates/gregor_anvil/combinedconsortiumdataworkspace_detail.html
+++ b/gregor_django/templates/gregor_anvil/combinedconsortiumdataworkspace_detail.html
@@ -7,3 +7,19 @@
Date completed {{ workspace_data_object.date_completed }}
{% endblock workspace_data %}
+
+
+{% block action_buttons %}
+{% if show_edit_links %}
+
+
+ Audit consortium sharing
+
+
+ Audit auth domain membership
+
+
+{% endif %}
+
+{{ block.super }}
+{% endblock action_buttons %}
From fe2bc687271b035f92d46362acd2eaf4eb583d44 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 8 Oct 2024 11:23:36 -0700
Subject: [PATCH 55/97] Update wording on upload cycle detail audit buttons
Make it clear that the buttons are for auditing upload workspaces,
not the combined workspace.
---
gregor_django/templates/gregor_anvil/uploadcycle_detail.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/gregor_django/templates/gregor_anvil/uploadcycle_detail.html b/gregor_django/templates/gregor_anvil/uploadcycle_detail.html
index b8683f76..8a96d6ad 100644
--- a/gregor_django/templates/gregor_anvil/uploadcycle_detail.html
+++ b/gregor_django/templates/gregor_anvil/uploadcycle_detail.html
@@ -70,10 +70,10 @@ Partner upload workspaces
{% endif %}
- Audit consortium sharing
+ Audit upload workspace sharing
- Audit auth domain membership
+ Audit upload workspace auth domains
{% endblock action_buttons %}
From bd47e7a87e31c647b3300139f5f1b0ffe12319eb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 8 Oct 2024 19:05:30 +0000
Subject: [PATCH 56/97] Bump pre-commit from 3.8.0 to 4.0.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.8.0 to 4.0.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.8.0...v4.0.1)
---
updated-dependencies:
- dependency-name: pre-commit
dependency-type: direct:development
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 132223a6..553a3ef9 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -121,7 +121,7 @@ platformdirs==4.2.0
# via
# pylint
# virtualenv
-pre-commit==3.8.0
+pre-commit==4.0.1
# via -r requirements/dev-requirements.in
prompt-toolkit==3.0.43
# via ipython
From 0e7a60454c8fb57cfb7e985cc35eb1c0da58cb6d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 8 Oct 2024 19:06:31 +0000
Subject: [PATCH 57/97] Bump tablib from 3.6.1 to 3.7.0
Bumps [tablib](https://github.com/jazzband/tablib) from 3.6.1 to 3.7.0.
- [Release notes](https://github.com/jazzband/tablib/releases)
- [Changelog](https://github.com/jazzband/tablib/blob/master/HISTORY.md)
- [Commits](https://github.com/jazzband/tablib/compare/v3.6.1...v3.7.0)
---
updated-dependencies:
- dependency-name: tablib
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 6e83e7b4..f96bd07d 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -152,7 +152,7 @@ six==1.16.0
# via google-auth
sqlparse==0.5.1
# via django
-tablib==3.6.1
+tablib==3.7.0
# via -r requirements/requirements.in
tenacity==8.2.3
# via
From 9626fb05036a56d9b6d8e828a140f34ae0a5f178 Mon Sep 17 00:00:00 2001
From: Jonas Carson
Date: Wed, 9 Oct 2024 11:17:46 -0700
Subject: [PATCH 58/97] Updates to our drupal oauth provider and adapter to be
compatible with updated django-allauth
---
config/settings/base.py | 1 +
.../drupal_oauth_provider/provider.py | 10 ++
gregor_django/drupal_oauth_provider/tests.py | 110 +++++++++++++--
gregor_django/drupal_oauth_provider/views.py | 4 +-
.../socialaccount/authentication_error.html | 2 +-
gregor_django/users/adapters.py | 21 ++-
gregor_django/users/tests/test_adapters.py | 131 ++++++++++++------
requirements/requirements.in | 2 +-
requirements/requirements.txt | 2 +-
9 files changed, 222 insertions(+), 61 deletions(-)
diff --git a/config/settings/base.py b/config/settings/base.py
index 5539f254..f33bb9ec 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -155,6 +155,7 @@
"maintenance_mode.middleware.MaintenanceModeMiddleware",
"simple_history.middleware.HistoryRequestMiddleware",
"django_htmx.middleware.HtmxMiddleware",
+ "allauth.account.middleware.AccountMiddleware",
]
# STATIC
diff --git a/gregor_django/drupal_oauth_provider/provider.py b/gregor_django/drupal_oauth_provider/provider.py
index ef2d9c93..c7c36f60 100644
--- a/gregor_django/drupal_oauth_provider/provider.py
+++ b/gregor_django/drupal_oauth_provider/provider.py
@@ -2,11 +2,14 @@
from allauth.account.models import EmailAddress
from allauth.socialaccount import app_settings, providers
+from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
+from .views import CustomAdapter
+
logger = logging.getLogger(__name__)
DRUPAL_PROVIDER_ID = "drupal_oauth_provider"
@@ -27,6 +30,13 @@ class CustomProvider(OAuth2Provider):
id = DRUPAL_PROVIDER_ID
name = OVERRIDE_NAME
account_class = CustomAccount
+ oauth2_adapter_class = CustomAdapter
+ supports_token_authentication = True
+
+ def __init__(self, request, app=None):
+ if app is None:
+ app = get_adapter().get_app(request, self.id)
+ super().__init__(request, app=app)
def extract_uid(self, data):
return str(data["sub"])
diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py
index 7fc52d06..2d9b5af5 100644
--- a/gregor_django/drupal_oauth_provider/tests.py
+++ b/gregor_django/drupal_oauth_provider/tests.py
@@ -1,11 +1,18 @@
+import base64
import datetime
+import hashlib
import json
+from urllib.parse import parse_qs, urlparse
import jwt
+import requests
+from allauth.socialaccount import app_settings
from allauth.socialaccount.adapter import get_adapter
+from allauth.socialaccount.models import SocialApp
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
from django.conf import settings
+from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
from django.test import RequestFactory
from django.test.utils import override_settings
@@ -83,10 +90,18 @@ def sign_id_token(payload):
# disable token storing for testing as it conflicts with drupals use
# of tokens for user info
-@override_settings(SOCIALACCOUNT_STORE_TOKENS=False)
+@override_settings(SOCIALACCOUNT_STORE_TOKENS=True)
class CustomProviderTests(OAuth2TestsMixin, TestCase):
provider_id = CustomProvider.id
+ def setUp(self):
+ super(CustomProviderTests, self).setUp()
+ # workaround to create a session. see:
+ # https://code.djangoproject.com/ticket/11475
+ User = get_user_model()
+ User.objects.create_user("testuser", "testuser@testuser.com", "testpw")
+ self.client.login(username="testuser", password="testpw")
+
# Provide two mocked responses, first is to the public key request
# second is used for the profile request for extra data
def get_mocked_response(self):
@@ -104,13 +119,68 @@ def get_mocked_response(self):
),
]
- # This login response mimics drupals in that it contains a set of scopes
- # and the uid which has the name sub
- def get_login_response_json(self, with_refresh_token=True):
+ def login(self, resp_mock=None, process="login", with_refresh_token=True):
+ """
+ Unfortunately due to how our provider works we need to alter
+ this test login function as the default one fails.
+ """
+ with self.mocked_response():
+ resp = self.client.post(self.provider.get_login_url(self.request, process=process))
+ p = urlparse(resp["location"])
+ q = parse_qs(p.query)
+ pkce_enabled = app_settings.PROVIDERS.get(self.app.provider, {}).get(
+ "OAUTH_PKCE_ENABLED", self.provider.pkce_enabled_default
+ )
+
+ self.assertEqual("code_challenge" in q, pkce_enabled)
+ self.assertEqual("code_challenge_method" in q, pkce_enabled)
+ if pkce_enabled:
+ code_challenge = q["code_challenge"][0]
+ self.assertEqual(q["code_challenge_method"][0], "S256")
+
+ complete_url = self.provider.get_callback_url()
+ self.assertGreater(q["redirect_uri"][0].find(complete_url), 0)
+ response_json = self.get_login_response_json(with_refresh_token=with_refresh_token)
+
+ resp_mocks = resp_mock if isinstance(resp_mock, list) else ([resp_mock] if resp_mock is not None else [])
+
+ with self.mocked_response(
+ MockedResponse(200, response_json, {"content-type": "application/json"}),
+ *resp_mocks,
+ ):
+ resp = self.client.get(complete_url, self.get_complete_parameters(q))
+
+ # Find the access token POST request, and assert that it contains
+ # the correct code_verifier if and only if PKCE is enabled
+ request_calls = requests.Session.request.call_args_list
+ import sys
+
+ print(f"REQUEST CALLS {request_calls}", file=sys.stderr)
+ for args, kwargs in request_calls:
+ print(f"RC: {args} kwargs: {kwargs}", file=sys.stderr)
+ data = kwargs.get("data", {})
+ if (
+ args
+ and args[0] == "POST"
+ and isinstance(data, dict)
+ and data.get("redirect_uri", "").endswith(complete_url)
+ ):
+ self.assertEqual("code_verifier" in data, pkce_enabled)
+
+ if pkce_enabled:
+ hashed_code_verifier = hashlib.sha256(data["code_verifier"].encode("ascii"))
+ expected_code_challenge = (
+ base64.urlsafe_b64encode(hashed_code_verifier.digest()).rstrip(b"=").decode()
+ )
+ self.assertEqual(code_challenge, expected_code_challenge)
+
+ return resp
+
+ def get_id_token(self):
now = datetime.datetime.now(datetime.timezone.utc)
app = get_adapter().get_app(request=None, provider=self.provider_id)
allowed_audience = app.client_id
- id_token = sign_id_token(
+ return sign_id_token(
{
"exp": now + datetime.timedelta(hours=1),
"iat": now,
@@ -119,6 +189,17 @@ def get_login_response_json(self, with_refresh_token=True):
"sub": 20122,
}
)
+
+ def get_access_token(self) -> str:
+ return self.get_id_token()
+
+ def get_expected_to_str(self):
+ return "test@testmaster.net"
+
+ # This login response mimics drupals in that it contains a set of scopes
+ # and the uid which has the name sub
+ def get_login_response_json(self, with_refresh_token=True):
+ id_token = self.get_id_token()
response_data = {
"access_token": id_token,
"expires_in": 3600,
@@ -131,6 +212,19 @@ def get_login_response_json(self, with_refresh_token=True):
class TestProviderConfig(TestCase):
+ def setUp(self):
+ # workaround to create a session. see:
+ # https://code.djangoproject.com/ticket/11475
+
+ app = SocialApp.objects.create(
+ provider=CustomProvider.id,
+ name=CustomProvider.id,
+ client_id="app123id",
+ key=CustomProvider.id,
+ secret="dummy",
+ )
+ self.app = app
+
def test_custom_provider_scope_config(self):
custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS
rf = RequestFactory()
@@ -138,7 +232,7 @@ def test_custom_provider_scope_config(self):
custom_provider_settings["drupal_oauth_provider"]["SCOPES"] = None
with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings):
with self.assertRaises(ImproperlyConfigured):
- CustomProvider(request).get_provider_scope_config()
+ CustomProvider(request, app=self.app).get_provider_scope_config()
def test_custom_provider_scope_detail_config(self):
custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS
@@ -153,7 +247,7 @@ def test_custom_provider_scope_detail_config(self):
]
with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings):
with self.assertRaises(ImproperlyConfigured):
- CustomProvider(request).get_provider_managed_scope_status()
+ CustomProvider(request, app=self.app).get_provider_managed_scope_status()
def test_custom_provider_has_scope(self):
custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS
@@ -167,4 +261,4 @@ def test_custom_provider_has_scope(self):
}
]
with override_settings(SOCIALACCOUNT_PROVIDERS=custom_provider_settings):
- CustomProvider(request).get_provider_managed_scope_status(scopes_granted=["X"])
+ CustomProvider(request, app=self.app).get_provider_managed_scope_status(scopes_granted=["X"])
diff --git a/gregor_django/drupal_oauth_provider/views.py b/gregor_django/drupal_oauth_provider/views.py
index 317021a8..655f6f4d 100644
--- a/gregor_django/drupal_oauth_provider/views.py
+++ b/gregor_django/drupal_oauth_provider/views.py
@@ -12,13 +12,13 @@
OAuth2LoginView,
)
-from .provider import CustomProvider
+# from .provider import CustomProvider
logger = logging.getLogger(__name__)
class CustomAdapter(OAuth2Adapter):
- provider_id = CustomProvider.id
+ provider_id = "drupal_oauth_provider"
provider_settings = app_settings.PROVIDERS.get(provider_id, {})
diff --git a/gregor_django/templates/socialaccount/authentication_error.html b/gregor_django/templates/socialaccount/authentication_error.html
index 92a5b4c8..ba0bb87a 100644
--- a/gregor_django/templates/socialaccount/authentication_error.html
+++ b/gregor_django/templates/socialaccount/authentication_error.html
@@ -1,4 +1,4 @@
-{% extends "socialaccount/base.html" %}
+{% extends "base.html" %}
{% load i18n %}
diff --git a/gregor_django/users/adapters.py b/gregor_django/users/adapters.py
index 26f0075c..90272c3b 100644
--- a/gregor_django/users/adapters.py
+++ b/gregor_django/users/adapters.py
@@ -24,6 +24,10 @@ def is_open_for_signup(self, request: HttpRequest, sociallogin: Any):
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
def update_user_info(self, user, extra_data: Dict):
+ import sys
+
+ print(f"USER3: {user} {user.username} id: {user.id}", file=sys.stderr)
+ logger.info(f"User {user} username {user.username}")
drupal_username = extra_data.get("preferred_username")
drupal_email = extra_data.get("email")
first_name = extra_data.get("first_name")
@@ -32,21 +36,21 @@ def update_user_info(self, user, extra_data: Dict):
user_changed = False
if user.name != full_name:
logger.info(
- f"[SocialAccountAdatpter:update_user_name] user {user} " f"name updated from {user.name} to {full_name}"
+ f"[SocialAccountAdatpter:update_user_info] user {user} " f"name updated from {user.name} to {full_name}"
)
user.name = full_name
user_changed = True
if user.username != drupal_username:
logger.info(
- f"[SocialAccountAdatpter:update_user_name] user {user} "
+ f"[SocialAccountAdatpter:update_user_info] user {user} "
f"username updated from {user.username} to {drupal_username}"
)
user.username = drupal_username
user_changed = True
if user.email != drupal_email:
logger.info(
- f"[SocialAccountAdatpter:update_user_name] user {user}"
- f" email updated from {user.email} to {drupal_email}"
+ f"[SocialAccountAdatpter:update_user_info] user {user.username}"
+ # f" email updated from {user.email} to {drupal_email}"
)
user.email = drupal_email
user_changed = True
@@ -186,10 +190,13 @@ def update_user_data(self, sociallogin: Any):
self.update_user_partner_groups(user, extra_data)
self.update_user_groups(user, extra_data)
- def authentication_error(self, request, provider_id, error, exception, extra_context):
+ def on_authentication_error(self, request, provider_id, error, exception, extra_context):
"""
Invoked when there is an error in auth cycle.
Log so we know what is going on.
"""
- logger.error(f"[SocialAccountAdapter:authentication_error] Error {error} Exception: {exception}")
- super().authentication_error(request, provider_id, error, exception, extra_context)
+ logger.error(
+ f"[SocialAccountAdapter:on_authentication_error] Provider: {provider_id} "
+ f"Error {error} Exception: {exception} extra {extra_context}"
+ )
+ super().on_authentication_error(request, provider_id, error, exception, extra_context)
diff --git a/gregor_django/users/tests/test_adapters.py b/gregor_django/users/tests/test_adapters.py
index 4e37d11e..4e6b77ae 100644
--- a/gregor_django/users/tests/test_adapters.py
+++ b/gregor_django/users/tests/test_adapters.py
@@ -1,15 +1,20 @@
+# myapp/tests.py
+
import pytest
from allauth.account import app_settings as account_settings
-from allauth.socialaccount.helpers import complete_social_login
-from allauth.socialaccount.models import SocialAccount, SocialLogin
-from allauth.utils import get_user_model
+from allauth.account import signals
+from allauth.socialaccount.models import SocialAccount, SocialApp, SocialLogin
+from django.contrib.auth import get_user_model
+from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.auth.models import AnonymousUser
-from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
+from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
+from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
+from gregor_django.drupal_oauth_provider.provider import CustomProvider
from gregor_django.gregor_anvil.tests.factories import (
PartnerGroupFactory,
ResearchCenterFactory,
@@ -18,48 +23,77 @@
from .factories import GroupFactory, UserFactory
+User = get_user_model()
-@pytest.mark.django_db
-class TestsUserSocialLoginAdapter(object):
- @override_settings(
- SOCIALACCOUNT_AUTO_SIGNUP=True,
- ACCOUNT_SIGNUP_FORM_CLASS=None,
- ACCOUNT_EMAIL_VERIFICATION=account_settings.EmailVerificationMethod.NONE, # noqa
- )
- def test_drupal_social_login_adapter(self):
- factory = RequestFactory()
- request = factory.get("/accounts/login/callback/")
- request.user = AnonymousUser()
- SessionMiddleware(lambda request: None).process_request(request)
- MessageMiddleware(lambda request: None).process_request(request)
-
- User = get_user_model()
- user = User()
- old_name = "Old Name"
- old_username = "test"
- old_email = "test@example.com"
- setattr(user, account_settings.USER_MODEL_USERNAME_FIELD, "test")
- setattr(user, "name", "Old Name")
- setattr(user, account_settings.USER_MODEL_EMAIL_FIELD, "test@example.com")
- account = SocialAccount(
+class SocialAccountAdapterTest(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+ # Setup a mock social app
+ current_site = Site.objects.get_current()
+ self.social_app = SocialApp.objects.create(
+ provider=CustomProvider.id,
+ name="DOA",
+ client_id="test-client-id",
+ secret="test-client-secret",
+ )
+ self.social_app.sites.add(current_site)
+
+ def extract_state_from_url(self, url):
+ from urllib.parse import parse_qs, urlparse
+
+ parsed_url = urlparse(url)
+ query_params = parse_qs(parsed_url.query)
+ return query_params.get("state", [None])[0]
+
+ def test_social_login_success(self):
+ # Mock user
+ request = self.factory.get("/")
+ middleware = SessionMiddleware(lambda x: None)
+ middleware.process_request(request)
+ request.session.save()
+ middleware = AuthenticationMiddleware(lambda x: None)
+ middleware.process_request(request)
+ request.user = AnonymousUser()
+ user = User.objects.create(username="testuser", email="testuser@example.com")
+
+ # # Mock social login
+ # Create a mock SocialAccount and link it to the user
+ new_first_name = "Bob"
+ new_last_name = "Rob"
+ social_account = SocialAccount.objects.create(
+ user=user,
provider="drupal_oauth_provider",
- uid="123",
- extra_data=dict(
- first_name="Old",
- last_name="Name",
- email=old_email,
- preferred_username=old_username,
- ),
+ uid="12345",
+ extra_data={
+ "preferred_username": "testuser",
+ "first_name": new_first_name,
+ "last_name": new_last_name,
+ "email": "testuser@example.com",
+ },
)
- sociallogin = SocialLogin(user=user, account=account)
- complete_social_login(request, sociallogin)
- user = User.objects.get(**{account_settings.USER_MODEL_USERNAME_FIELD: "test"})
- assert SocialAccount.objects.filter(user=user, uid=account.uid).exists() is True
- assert user.name == old_name
- assert user.username == old_username
- assert user.email == old_email
+ # Create a mock SocialLogin object and associate the user and social account
+ sociallogin = SocialLogin(user=user, account=social_account)
+
+ # Simulate social login
+ from allauth.account.adapter import get_adapter
+
+ # adapter = SocialAccountAdapter()
+ adapter = get_adapter(request)
+
+ adapter.login(request, user)
+
+ signals.user_logged_in.send(
+ sender=user.__class__,
+ request=request,
+ user=user,
+ sociallogin=sociallogin,
+ )
+ # Check if the login completed successfully
+ self.assertEqual(sociallogin.user, user)
+ self.assertEqual(request.user, user)
+ self.assertEqual(user.name, f"{new_first_name} {new_last_name}")
def test_update_user_info(self):
adapter = SocialAccountAdapter()
@@ -105,6 +139,21 @@ def test_update_user_research_centers_add(self):
assert user.research_centers.filter(pk=rc1.pk).exists()
assert user.research_centers.all().count() == 1
+ def test_update_user_research_centers_short_name_add(self):
+ adapter = SocialAccountAdapter()
+ rc1 = ResearchCenterFactory(short_name="rc1")
+
+ User = get_user_model()
+ user = User()
+ setattr(user, account_settings.USER_MODEL_USERNAME_FIELD, "test")
+ setattr(user, account_settings.USER_MODEL_EMAIL_FIELD, "test@example.com")
+
+ user.save()
+
+ adapter.update_user_research_centers(user, dict(research_center_or_site=[rc1.short_name]))
+ assert user.research_centers.filter(pk=rc1.pk).exists()
+ assert user.research_centers.all().count() == 1
+
def test_update_user_research_centers_remove(self):
adapter = SocialAccountAdapter()
rc1 = ResearchCenterFactory(short_name="rc1")
diff --git a/requirements/requirements.in b/requirements/requirements.in
index 313b7167..ecf9c6a2 100644
--- a/requirements/requirements.in
+++ b/requirements/requirements.in
@@ -5,7 +5,7 @@ pip-tools
whitenoise # https://github.com/evansd/whitenoise
# Login via oauth
oauthlib # https://github.com/oauthlib/oauthlib
-
+cryptography # https://github.com/pyca/cryptography
# Password hashing
argon2-cffi # https://github.com/hynek/argon2_cffi
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index a314a609..789a5fa5 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -51,7 +51,7 @@ django==4.2.16
# django-picklefield
# django-simple-history
# django-tables2
-django-allauth==0.54.0
+django-allauth==65.0.2
# via -r requirements/requirements.in
django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.25
# via -r requirements/requirements.in
From b3bb77feec18a4e7bef022cf81297c734fc99c36 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 9 Oct 2024 19:47:00 +0000
Subject: [PATCH 59/97] Bump actions/upload-artifact from 4.4.0 to 4.4.3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.0 to 4.4.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.4.0...v4.4.3)
---
updated-dependencies:
- dependency-name: actions/upload-artifact
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dfb8827d..0fc29662 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -99,7 +99,7 @@ jobs:
run: ls -lhta
- name: Upload coverage data
- uses: actions/upload-artifact@v4.4.0
+ uses: actions/upload-artifact@v4.4.3
with:
name: coverage-data-${{ strategy.job-index }}
path: coverage-${{ strategy.job-index }}
From 705893b6f85ae687cf85a4b19c6d5e7ef8a05f19 Mon Sep 17 00:00:00 2001
From: Jonas Carson
Date: Thu, 10 Oct 2024 08:50:31 -0700
Subject: [PATCH 60/97] Clean up un-needed changes introduced when working
through compatiblity issues with new allauth.
---
gregor_django/drupal_oauth_provider/tests.py | 3 ---
gregor_django/drupal_oauth_provider/views.py | 2 --
gregor_django/users/adapters.py | 8 ++------
3 files changed, 2 insertions(+), 11 deletions(-)
diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py
index 2d9b5af5..c27164ab 100644
--- a/gregor_django/drupal_oauth_provider/tests.py
+++ b/gregor_django/drupal_oauth_provider/tests.py
@@ -153,11 +153,8 @@ def login(self, resp_mock=None, process="login", with_refresh_token=True):
# Find the access token POST request, and assert that it contains
# the correct code_verifier if and only if PKCE is enabled
request_calls = requests.Session.request.call_args_list
- import sys
- print(f"REQUEST CALLS {request_calls}", file=sys.stderr)
for args, kwargs in request_calls:
- print(f"RC: {args} kwargs: {kwargs}", file=sys.stderr)
data = kwargs.get("data", {})
if (
args
diff --git a/gregor_django/drupal_oauth_provider/views.py b/gregor_django/drupal_oauth_provider/views.py
index 655f6f4d..80274a40 100644
--- a/gregor_django/drupal_oauth_provider/views.py
+++ b/gregor_django/drupal_oauth_provider/views.py
@@ -12,8 +12,6 @@
OAuth2LoginView,
)
-# from .provider import CustomProvider
-
logger = logging.getLogger(__name__)
diff --git a/gregor_django/users/adapters.py b/gregor_django/users/adapters.py
index 90272c3b..27ac8a2b 100644
--- a/gregor_django/users/adapters.py
+++ b/gregor_django/users/adapters.py
@@ -24,10 +24,6 @@ def is_open_for_signup(self, request: HttpRequest, sociallogin: Any):
return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
def update_user_info(self, user, extra_data: Dict):
- import sys
-
- print(f"USER3: {user} {user.username} id: {user.id}", file=sys.stderr)
- logger.info(f"User {user} username {user.username}")
drupal_username = extra_data.get("preferred_username")
drupal_email = extra_data.get("email")
first_name = extra_data.get("first_name")
@@ -49,8 +45,8 @@ def update_user_info(self, user, extra_data: Dict):
user_changed = True
if user.email != drupal_email:
logger.info(
- f"[SocialAccountAdatpter:update_user_info] user {user.username}"
- # f" email updated from {user.email} to {drupal_email}"
+ f"[SocialAccountAdatpter:update_user_info] user {user}"
+ f" email updated from {user.email} to {drupal_email}"
)
user.email = drupal_email
user_changed = True
From a1262e97c01403c1672345fa4e30401ee399629a Mon Sep 17 00:00:00 2001
From: Jonas Carson
Date: Thu, 10 Oct 2024 10:09:23 -0700
Subject: [PATCH 61/97] Updated requirements needed due to allauth upgrade
---
requirements/requirements.in | 2 ++
requirements/requirements.txt | 15 +++++----------
2 files changed, 7 insertions(+), 10 deletions(-)
diff --git a/requirements/requirements.in b/requirements/requirements.in
index ecf9c6a2..e39662ee 100644
--- a/requirements/requirements.in
+++ b/requirements/requirements.in
@@ -6,6 +6,8 @@ whitenoise # https://github.com/evansd/whitenoise
# Login via oauth
oauthlib # https://github.com/oauthlib/oauthlib
cryptography # https://github.com/pyca/cryptography
+pyjwt # https://github.com/jpadilla/pyjwt
+requests-oauthlib # https://github.com/requests/requests-oauthlib
# Password hashing
argon2-cffi # https://github.com/hynek/argon2_cffi
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index d66cb52f..36695d7a 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -32,9 +32,7 @@ crispy-bootstrap5==2024.2
# -r requirements/requirements.in
# django-anvil-consortium-manager
cryptography==43.0.1
- # via pyjwt
-defusedxml==0.7.1
- # via python3-openid
+ # via -r requirements/requirements.in
django==4.2.16
# via
# -r requirements/requirements.in
@@ -124,28 +122,25 @@ pyasn1-modules==0.3.0
# via google-auth
pycparser==2.21
# via cffi
-pyjwt[crypto]==2.8.0
- # via django-allauth
+pyjwt==2.8.0
+ # via -r requirements/requirements.in
pyproject-hooks==1.0.0
# via
# build
# pip-tools
python-fsutil==0.13.1
# via django-maintenance-mode
-python3-openid==3.2.0
- # via django-allauth
pytz==2023.4
# via
# django-anvil-consortium-manager
# django-dbbackup
requests==2.32.3
# via
- # django-allauth
# django-anvil-consortium-manager
# jsonapi-requests
# requests-oauthlib
-requests-oauthlib==1.3.1
- # via django-allauth
+requests-oauthlib==2.0.0
+ # via -r requirements/requirements.in
rsa==4.9
# via google-auth
six==1.16.0
From 89b640c1f7718c9db98e5e2d8cb86031730b10ef Mon Sep 17 00:00:00 2001
From: Jonas Carson
Date: Thu, 10 Oct 2024 11:01:41 -0700
Subject: [PATCH 62/97] Fix missing coverage and remove unused test function
---
gregor_django/drupal_oauth_provider/tests.py | 10 +++++++++-
.../templates/socialaccount/authentication_error.html | 2 +-
gregor_django/users/tests/test_adapters.py | 7 -------
3 files changed, 10 insertions(+), 9 deletions(-)
diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py
index c27164ab..214787e6 100644
--- a/gregor_django/drupal_oauth_provider/tests.py
+++ b/gregor_django/drupal_oauth_provider/tests.py
@@ -13,6 +13,7 @@
from allauth.tests import MockedResponse, TestCase
from django.conf import settings
from django.contrib.auth import get_user_model
+from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.test import RequestFactory
from django.test.utils import override_settings
@@ -212,7 +213,7 @@ class TestProviderConfig(TestCase):
def setUp(self):
# workaround to create a session. see:
# https://code.djangoproject.com/ticket/11475
-
+ current_site = Site.objects.get_current()
app = SocialApp.objects.create(
provider=CustomProvider.id,
name=CustomProvider.id,
@@ -221,6 +222,13 @@ def setUp(self):
secret="dummy",
)
self.app = app
+ self.app.sites.add(current_site)
+
+ def test_custom_provider_no_app(self):
+ rf = RequestFactory()
+ request = rf.get("/fake-url/")
+ provider = CustomProvider(request)
+ assert provider.app is not None
def test_custom_provider_scope_config(self):
custom_provider_settings = settings.SOCIALACCOUNT_PROVIDERS
diff --git a/gregor_django/templates/socialaccount/authentication_error.html b/gregor_django/templates/socialaccount/authentication_error.html
index ba0bb87a..76166bfe 100644
--- a/gregor_django/templates/socialaccount/authentication_error.html
+++ b/gregor_django/templates/socialaccount/authentication_error.html
@@ -2,7 +2,7 @@
{% load i18n %}
-{% block head_title %}{% trans "Social Network Login Failure" %}{% endblock %}
+{% block title %}{% trans "Social Network Login Failure" %}{% endblock %}
{% block content %}
{% trans "Social Network Login Failure" %}
diff --git a/gregor_django/users/tests/test_adapters.py b/gregor_django/users/tests/test_adapters.py
index 4e6b77ae..c21abe34 100644
--- a/gregor_django/users/tests/test_adapters.py
+++ b/gregor_django/users/tests/test_adapters.py
@@ -39,13 +39,6 @@ def setUp(self):
)
self.social_app.sites.add(current_site)
- def extract_state_from_url(self, url):
- from urllib.parse import parse_qs, urlparse
-
- parsed_url = urlparse(url)
- query_params = parse_qs(parsed_url.query)
- return query_params.get("state", [None])[0]
-
def test_social_login_success(self):
# Mock user
request = self.factory.get("/")
From a5613f0658d2b3d8bd27a00078260e17c046c5ff Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 10 Oct 2024 19:25:56 +0000
Subject: [PATCH 63/97] Bump pylint-django from 2.5.5 to 2.6.1
Bumps [pylint-django](https://github.com/pylint-dev/pylint-django) from 2.5.5 to 2.6.1.
- [Release notes](https://github.com/pylint-dev/pylint-django/releases)
- [Changelog](https://github.com/pylint-dev/pylint-django/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pylint-dev/pylint-django/commits/v2.6.1)
---
updated-dependencies:
- dependency-name: pylint-django
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 132223a6..c9ad1d43 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -137,7 +137,7 @@ pylint==3.1.0
# via
# pylint-django
# pylint-plugin-utils
-pylint-django==2.5.5
+pylint-django==2.6.1
# via -r requirements/dev-requirements.in
pylint-plugin-utils==0.8.2
# via pylint-django
From c81ff3ed28314255bc2307184b114185e4e0ad38 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 10 Oct 2024 19:32:41 +0000
Subject: [PATCH 64/97] Bump github/combine-prs from 5.1.0 to 5.2.0
Bumps [github/combine-prs](https://github.com/github/combine-prs) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/github/combine-prs/releases)
- [Commits](https://github.com/github/combine-prs/compare/v5.1.0...v5.2.0)
---
updated-dependencies:
- dependency-name: github/combine-prs
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
.github/workflows/combine-prs.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/combine-prs.yml b/.github/workflows/combine-prs.yml
index cf9c0d74..82d39e94 100644
--- a/.github/workflows/combine-prs.yml
+++ b/.github/workflows/combine-prs.yml
@@ -16,7 +16,7 @@ jobs:
steps:
- name: combine-prs
id: combine-prs
- uses: github/combine-prs@v5.1.0
+ uses: github/combine-prs@v5.2.0
with:
labels: combined-pr # Optional: add a label to the combined PR
ci_required: true # require all checks to pass before combining
From 926b429850730df622720896d699337ce90ded07 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 14 Oct 2024 19:16:02 +0000
Subject: [PATCH 65/97] Bump gitleaks/gitleaks-action from 2.3.6 to 2.3.7
Bumps [gitleaks/gitleaks-action](https://github.com/gitleaks/gitleaks-action) from 2.3.6 to 2.3.7.
- [Release notes](https://github.com/gitleaks/gitleaks-action/releases)
- [Commits](https://github.com/gitleaks/gitleaks-action/compare/v2.3.6...v2.3.7)
---
updated-dependencies:
- dependency-name: gitleaks/gitleaks-action
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
.github/workflows/gitleaks.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml
index b898bbac..00a67cc8 100644
--- a/.github/workflows/gitleaks.yml
+++ b/.github/workflows/gitleaks.yml
@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4.2.1
with:
fetch-depth: 0
- - uses: gitleaks/gitleaks-action@v2.3.6
+ - uses: gitleaks/gitleaks-action@v2.3.7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE}} # Only required for Organizations, not personal accounts.
From 501cd1cf8978433aa53e1a7aaa68d68b6fc0ee11 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 8 Oct 2024 12:05:31 -0700
Subject: [PATCH 66/97] Add (and use) a mixin for running audits
This mixin defines a method to run an audit, and then adds the
results of that audit to the context. Use the new mixin in the views.
Note that we aren't using the mixin in the audit resolve method,
because it doesn't run audits in the same way.
---
gregor_django/gregor_anvil/viewmixins.py | 16 +++
gregor_django/gregor_anvil/views.py | 136 ++++++++---------------
2 files changed, 63 insertions(+), 89 deletions(-)
create mode 100644 gregor_django/gregor_anvil/viewmixins.py
diff --git a/gregor_django/gregor_anvil/viewmixins.py b/gregor_django/gregor_anvil/viewmixins.py
new file mode 100644
index 00000000..4eba88f8
--- /dev/null
+++ b/gregor_django/gregor_anvil/viewmixins.py
@@ -0,0 +1,16 @@
+class AuditMixin:
+ """Mixin to assist with auditing views."""
+
+ def run_audit(self):
+ raise NotImplementedError("AuditMixin.run_audit() must be implemented in a subclass")
+
+ def get_context_data(self, **kwargs):
+ """Run the audit and add it to the context."""
+ context = super().get_context_data(**kwargs)
+ # Run the audit.
+ audit_results = self.run_audit()
+ context["verified_table"] = audit_results.get_verified_table()
+ context["errors_table"] = audit_results.get_errors_table()
+ context["needs_action_table"] = audit_results.get_needs_action_table()
+ context["audit_results"] = audit_results
+ return context
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index b1e90636..343adf0b 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -24,7 +24,7 @@
from gregor_django.users.tables import UserTable
-from . import forms, models, tables
+from . import forms, models, tables, viewmixins
from .audit import (
combined_workspace_audit,
upload_workspace_audit,
@@ -193,24 +193,20 @@ def get_context_data(self, **kwargs):
return context
-class UploadWorkspaceSharingAudit(AnVILConsortiumManagerStaffViewRequired, TemplateView):
+class UploadWorkspaceSharingAudit(AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, TemplateView):
"""View to audit UploadWorkspace sharing for all UploadWorkspaces."""
template_name = "gregor_anvil/upload_workspace_sharing_audit.html"
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- # Run the audit.
+ def run_audit(self, **kwargs):
audit = upload_workspace_audit.UploadWorkspaceSharingAudit()
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
-class UploadWorkspaceSharingAuditByWorkspace(AnVILConsortiumManagerStaffViewRequired, DetailView):
+class UploadWorkspaceSharingAuditByWorkspace(
+ AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, DetailView
+):
"""View to audit UploadWorkspace sharing for a specific UploadWorkspace."""
template_name = "gregor_anvil/upload_workspace_sharing_audit.html"
@@ -234,21 +230,17 @@ def get_object(self, queryset=None):
)
return obj
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- # Run the audit.
+ def run_audit(self, **kwargs):
audit = upload_workspace_audit.UploadWorkspaceSharingAudit(
queryset=self.model.objects.filter(pk=self.object.pk)
)
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
-class UploadWorkspaceSharingAuditByUploadCycle(AnVILConsortiumManagerStaffViewRequired, DetailView):
+class UploadWorkspaceSharingAuditByUploadCycle(
+ AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, DetailView
+):
"""View to audit UploadWorkspace sharing for a specific UploadWorkspace."""
template_name = "gregor_anvil/upload_workspace_sharing_audit.html"
@@ -268,18 +260,13 @@ def get_object(self, queryset=None):
)
return obj
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
+ def run_audit(self, **kwargs):
# Run the audit.
audit = upload_workspace_audit.UploadWorkspaceSharingAudit(
queryset=models.UploadWorkspace.objects.filter(upload_cycle=self.object)
)
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
class UploadWorkspaceSharingAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
@@ -396,24 +383,20 @@ def form_valid(self, form):
return super().form_valid(form)
-class UploadWorkspaceAuthDomainAudit(AnVILConsortiumManagerStaffViewRequired, TemplateView):
+class UploadWorkspaceAuthDomainAudit(AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, TemplateView):
"""View to audit UploadWorkspace auth domain membership for all UploadWorkspaces."""
template_name = "gregor_anvil/upload_workspace_auth_domain_audit.html"
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- # Run the audit.
+ def run_audit(self):
audit = upload_workspace_audit.UploadWorkspaceAuthDomainAudit()
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
-class UploadWorkspaceAuthDomainAuditByWorkspace(AnVILConsortiumManagerStaffEditRequired, DetailView):
+class UploadWorkspaceAuthDomainAuditByWorkspace(
+ AnVILConsortiumManagerStaffEditRequired, viewmixins.AuditMixin, DetailView
+):
"""View to audit UploadWorkspace sharing for a specific UploadWorkspace."""
template_name = "gregor_anvil/upload_workspace_auth_domain_audit.html"
@@ -437,21 +420,17 @@ def get_object(self, queryset=None):
)
return obj
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- # Run the audit.
+ def run_audit(self):
audit = upload_workspace_audit.UploadWorkspaceAuthDomainAudit(
queryset=self.model.objects.filter(pk=self.object.pk)
)
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
-class UploadWorkspaceAuthDomainAuditByUploadCycle(AnVILConsortiumManagerStaffViewRequired, DetailView):
+class UploadWorkspaceAuthDomainAuditByUploadCycle(
+ AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, DetailView
+):
"""View to audit UploadWorkspace sharing for a specific UploadWorkspace."""
template_name = "gregor_anvil/upload_workspace_auth_domain_audit.html"
@@ -471,18 +450,12 @@ def get_object(self, queryset=None):
)
return obj
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- # Run the audit.
+ def run_audit(self, **kwargs):
audit = upload_workspace_audit.UploadWorkspaceAuthDomainAudit(
queryset=models.UploadWorkspace.objects.filter(upload_cycle=self.object)
)
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
class UploadWorkspaceAuthDomainAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
@@ -599,24 +572,22 @@ def form_valid(self, form):
return super().form_valid(form)
-class CombinedConsortiumDataWorkspaceSharingAudit(AnVILConsortiumManagerStaffViewRequired, TemplateView):
+class CombinedConsortiumDataWorkspaceSharingAudit(
+ AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, TemplateView
+):
"""View to audit CombinedConsortiumDataWorkspace sharing for all CombinedConsortiumDataWorkspacs."""
template_name = "gregor_anvil/combinedconsortiumdataworkspace_sharing_audit.html"
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- # Run the audit.
+ def run_audit(self, **kwargs):
audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceSharingAudit()
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
-class CombinedConsortiumDataWorkspaceSharingAuditByWorkspace(AnVILConsortiumManagerStaffViewRequired, DetailView):
+class CombinedConsortiumDataWorkspaceSharingAuditByWorkspace(
+ AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, DetailView
+):
"""View to audit CombinedConsortiumDataWorkspace sharing for a specific CombinedConsortiumDataWorkspace."""
template_name = "gregor_anvil/combinedconsortiumdataworkspace_sharing_audit.html"
@@ -640,18 +611,12 @@ def get_object(self, queryset=None):
)
return obj
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- # Run the audit.
+ def run_audit(self, **kwargs):
audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceSharingAudit(
queryset=self.model.objects.filter(pk=self.object.pk)
)
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
class CombinedConsortiumDataWorkspaceSharingAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
@@ -768,24 +733,22 @@ def form_valid(self, form):
return super().form_valid(form)
-class CombinedConsortiumDataWorkspaceAuthDomainAudit(AnVILConsortiumManagerStaffViewRequired, TemplateView):
+class CombinedConsortiumDataWorkspaceAuthDomainAudit(
+ AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, TemplateView
+):
"""View to audit auth domain membership for all CombinedConsortiumDataWorkspaces."""
template_name = "gregor_anvil/combinedconsortiumdataworkspace_auth_domain_audit.html"
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- # Run the audit.
+ def run_audit(self, **kwargs):
audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit()
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
-class CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace(AnVILConsortiumManagerStaffViewRequired, DetailView):
+class CombinedConsortiumDataWorkspaceAuthDomainAuditByWorkspace(
+ AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, DetailView
+):
"""View to audit auth domain membership for a specific CombinedConsortiumDataWorkspace."""
template_name = "gregor_anvil/combinedconsortiumdataworkspace_auth_domain_audit.html"
@@ -809,18 +772,13 @@ def get_object(self, queryset=None):
)
return obj
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
+ def run_audit(self, **kwargs):
# Run the audit.
audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit(
queryset=self.model.objects.filter(pk=self.object.pk)
)
audit.run_audit()
- context["verified_table"] = audit.get_verified_table()
- context["errors_table"] = audit.get_errors_table()
- context["needs_action_table"] = audit.get_needs_action_table()
- context["audit_results"] = audit
- return context
+ return audit
class CombinedConsortiumDataWorkspaceAuthDomainAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
From bcb60fec830d2d1ec254238ca7852de50ca4fb37 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Mon, 14 Oct 2024 13:56:09 -0700
Subject: [PATCH 67/97] Move code to handle audit results into the result
subclasses
Instead of having the logic in the view, move it into the audit
result classes themselves. This makes it so the view only has
to handle exceptions instead of logic.
---
.../workspace_auth_domain_audit_results.py | 57 +-
.../audit/workspace_sharing_audit_results.py | 79 ++
.../gregor_anvil/tests/test_audit.py | 762 ++++++++++++++++--
gregor_django/gregor_anvil/views.py | 140 +---
4 files changed, 835 insertions(+), 203 deletions(-)
diff --git a/gregor_django/gregor_anvil/audit/workspace_auth_domain_audit_results.py b/gregor_django/gregor_anvil/audit/workspace_auth_domain_audit_results.py
index 11ccf580..493203a8 100644
--- a/gregor_django/gregor_anvil/audit/workspace_auth_domain_audit_results.py
+++ b/gregor_django/gregor_anvil/audit/workspace_auth_domain_audit_results.py
@@ -14,6 +14,7 @@ class WorkspaceAuthDomainAuditResult(GREGoRAuditResult):
managed_group: ManagedGroup
action: str = None
current_membership_instance: GroupGroupMembership = None
+ handled: bool = False
def get_table_dictionary(self):
"""Return a dictionary that can be used to populate an instance of `dbGaPDataSharingSnapshotAuditTable`."""
@@ -26,6 +27,13 @@ def get_table_dictionary(self):
}
return row
+ def _handle(self):
+ raise NotImplementedError("Subclasses must implement this method.")
+
+ def handle(self):
+ self._handle()
+ self.handled = True
+
@dataclass
class VerifiedMember(WorkspaceAuthDomainAuditResult):
@@ -34,16 +42,20 @@ class VerifiedMember(WorkspaceAuthDomainAuditResult):
def __str__(self):
return f"Verified member: {self.note}"
+ def _handle(self):
+ pass
+
@dataclass
class VerifiedAdmin(WorkspaceAuthDomainAuditResult):
"""Audit results class for when membership with an admin role has been verified."""
- is_shared: bool = False
-
def __str__(self):
return f"Verified admin: {self.note}"
+ def _handle(self):
+ pass
+
@dataclass
class VerifiedNotMember(WorkspaceAuthDomainAuditResult):
@@ -52,6 +64,9 @@ class VerifiedNotMember(WorkspaceAuthDomainAuditResult):
def __str__(self):
return f"Verified member: {self.note}"
+ def _handle(self):
+ pass
+
@dataclass
class AddMember(WorkspaceAuthDomainAuditResult):
@@ -62,6 +77,16 @@ class AddMember(WorkspaceAuthDomainAuditResult):
def __str__(self):
return f"Add member: {self.note}"
+ def _handle(self):
+ membership = GroupGroupMembership(
+ parent_group=self.workspace.authorization_domains.first(),
+ child_group=self.managed_group,
+ role=GroupGroupMembership.MEMBER,
+ )
+ membership.full_clean()
+ membership.save()
+ membership.anvil_create()
+
@dataclass
class AddAdmin(WorkspaceAuthDomainAuditResult):
@@ -72,6 +97,16 @@ class AddAdmin(WorkspaceAuthDomainAuditResult):
def __str__(self):
return f"Add admin: {self.note}"
+ def _handle(self):
+ membership = GroupGroupMembership(
+ parent_group=self.workspace.authorization_domains.first(),
+ child_group=self.managed_group,
+ role=GroupGroupMembership.ADMIN,
+ )
+ membership.full_clean()
+ membership.save()
+ membership.anvil_create()
+
@dataclass
class ChangeToMember(WorkspaceAuthDomainAuditResult):
@@ -82,6 +117,13 @@ class ChangeToMember(WorkspaceAuthDomainAuditResult):
def __str__(self):
return f"Change to member: {self.note}"
+ def _handle(self):
+ self.current_membership_instance.anvil_delete()
+ self.current_membership_instance.role = GroupGroupMembership.MEMBER
+ self.current_membership_instance.full_clean()
+ self.current_membership_instance.save()
+ self.current_membership_instance.anvil_create()
+
@dataclass
class ChangeToAdmin(WorkspaceAuthDomainAuditResult):
@@ -92,6 +134,13 @@ class ChangeToAdmin(WorkspaceAuthDomainAuditResult):
def __str__(self):
return f"Change to admin: {self.note}"
+ def _handle(self):
+ self.current_membership_instance.anvil_delete()
+ self.current_membership_instance.role = GroupGroupMembership.ADMIN
+ self.current_membership_instance.full_clean()
+ self.current_membership_instance.save()
+ self.current_membership_instance.anvil_create()
+
@dataclass
class Remove(WorkspaceAuthDomainAuditResult):
@@ -101,3 +150,7 @@ class Remove(WorkspaceAuthDomainAuditResult):
def __str__(self):
return f"Share as owner: {self.note}"
+
+ def _handle(self):
+ self.current_membership_instance.anvil_delete()
+ self.current_membership_instance.delete()
diff --git a/gregor_django/gregor_anvil/audit/workspace_sharing_audit_results.py b/gregor_django/gregor_anvil/audit/workspace_sharing_audit_results.py
index 7d9b93f6..42cc6398 100644
--- a/gregor_django/gregor_anvil/audit/workspace_sharing_audit_results.py
+++ b/gregor_django/gregor_anvil/audit/workspace_sharing_audit_results.py
@@ -18,6 +18,7 @@ class WorkspaceSharingAuditResult(GREGoRAuditResult):
managed_group: ManagedGroup
action: str = None
current_sharing_instance: WorkspaceGroupSharing = None
+ handled: bool = False
def get_table_dictionary(self):
"""Return a dictionary that can be used to populate an instance of `dbGaPDataSharingSnapshotAuditTable`."""
@@ -34,6 +35,13 @@ def get_table_dictionary(self):
}
return row
+ def _handle(self):
+ raise NotImplementedError("Subclasses must implement this method.")
+
+ def handle(self):
+ self._handle()
+ self.handled = True
+
@dataclass
class VerifiedShared(WorkspaceSharingAuditResult):
@@ -42,6 +50,9 @@ class VerifiedShared(WorkspaceSharingAuditResult):
def __str__(self):
return f"Verified sharing: {self.note}"
+ def _handle(self):
+ pass
+
@dataclass
class VerifiedNotShared(WorkspaceSharingAuditResult):
@@ -50,6 +61,9 @@ class VerifiedNotShared(WorkspaceSharingAuditResult):
def __str__(self):
return f"Verified not shared: {self.note}"
+ def _handle(self):
+ pass
+
@dataclass
class ShareAsReader(WorkspaceSharingAuditResult):
@@ -60,6 +74,21 @@ class ShareAsReader(WorkspaceSharingAuditResult):
def __str__(self):
return f"Share as reader: {self.note}"
+ def _handle(self):
+ # Create or update the sharing record.
+ if self.current_sharing_instance:
+ sharing = self.current_sharing_instance
+ else:
+ sharing = WorkspaceGroupSharing(
+ workspace=self.workspace,
+ group=self.managed_group,
+ )
+ sharing.access = WorkspaceGroupSharing.READER
+ sharing.can_compute = False
+ sharing.full_clean()
+ sharing.save()
+ sharing.anvil_create_or_update()
+
@dataclass
class ShareAsWriter(WorkspaceSharingAuditResult):
@@ -70,6 +99,21 @@ class ShareAsWriter(WorkspaceSharingAuditResult):
def __str__(self):
return f"Share as writer: {self.note}"
+ def _handle(self):
+ # Create or update the sharing record.
+ if self.current_sharing_instance:
+ sharing = self.current_sharing_instance
+ else:
+ sharing = WorkspaceGroupSharing(
+ workspace=self.workspace,
+ group=self.managed_group,
+ )
+ sharing.access = WorkspaceGroupSharing.WRITER
+ sharing.can_compute = False
+ sharing.full_clean()
+ sharing.save()
+ sharing.anvil_create_or_update()
+
@dataclass
class ShareAsOwner(WorkspaceSharingAuditResult):
@@ -80,6 +124,21 @@ class ShareAsOwner(WorkspaceSharingAuditResult):
def __str__(self):
return f"Share as owner: {self.note}"
+ def _handle(self):
+ # Create or update the sharing record.
+ if self.current_sharing_instance:
+ sharing = self.current_sharing_instance
+ else:
+ sharing = WorkspaceGroupSharing(
+ workspace=self.workspace,
+ group=self.managed_group,
+ )
+ sharing.access = WorkspaceGroupSharing.OWNER
+ sharing.can_compute = True
+ sharing.full_clean()
+ sharing.save()
+ sharing.anvil_create_or_update()
+
@dataclass
class ShareWithCompute(WorkspaceSharingAuditResult):
@@ -90,6 +149,21 @@ class ShareWithCompute(WorkspaceSharingAuditResult):
def __str__(self):
return f"Share with compute: {self.note}"
+ def _handle(self):
+ # Create or update the sharing record.
+ if self.current_sharing_instance:
+ sharing = self.current_sharing_instance
+ else:
+ sharing = WorkspaceGroupSharing(
+ workspace=self.workspace,
+ group=self.managed_group,
+ )
+ sharing.access = WorkspaceGroupSharing.WRITER
+ sharing.can_compute = True
+ sharing.full_clean()
+ sharing.save()
+ sharing.anvil_create_or_update()
+
@dataclass
class StopSharing(WorkspaceSharingAuditResult):
@@ -100,6 +174,11 @@ class StopSharing(WorkspaceSharingAuditResult):
def __str__(self):
return f"Stop sharing: {self.note}"
+ def _handle(self):
+ # Remove the sharing record.
+ self.current_sharing_instance.anvil_delete()
+ self.current_sharing_instance.delete()
+
@dataclass
class Error(WorkspaceSharingAuditResult):
diff --git a/gregor_django/gregor_anvil/tests/test_audit.py b/gregor_django/gregor_anvil/tests/test_audit.py
index 8f8125eb..dae1fd63 100644
--- a/gregor_django/gregor_anvil/tests/test_audit.py
+++ b/gregor_django/gregor_anvil/tests/test_audit.py
@@ -1,17 +1,24 @@
"""Tests for the `py` module."""
from dataclasses import dataclass
+from datetime import timedelta
import django_tables2 as tables
+import responses
from anvil_consortium_manager.models import GroupGroupMembership, WorkspaceGroupSharing
from anvil_consortium_manager.tests.factories import (
GroupGroupMembershipFactory,
ManagedGroupFactory,
+ WorkspaceAuthorizationDomainFactory,
+ WorkspaceFactory,
WorkspaceGroupSharingFactory,
)
+from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin
from django.conf import settings
from django.test import TestCase, override_settings
+from django.utils import timezone
from faker import Faker
+from freezegun import freeze_time
from .. import models
from ..audit import (
@@ -167,17 +174,17 @@ def test_get_errors_table(self):
self.assertEqual(table.rows[0].get_cell("value"), "c")
-class UploadWorkspaceSharingAuditResultTest(TestCase):
+class WorkspaceSharingAuditResultTest(AnVILAPIMockTestMixin, TestCase):
"""General tests of the UploadWorkspaceSharingAuditResult dataclasses."""
def test_shared_as_owner(self):
- upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ workspace = WorkspaceFactory.create()
group = ManagedGroupFactory.create()
sharing = WorkspaceGroupSharingFactory.create(
- workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.OWNER, can_compute=True
+ workspace=workspace, group=group, access=WorkspaceGroupSharing.OWNER, can_compute=True
)
instance = workspace_sharing_audit_results.WorkspaceSharingAuditResult(
- workspace=upload_workspace.workspace,
+ workspace=workspace,
managed_group=group,
current_sharing_instance=sharing,
note="foo",
@@ -187,13 +194,13 @@ def test_shared_as_owner(self):
self.assertEqual(table_dictionary["can_compute"], True)
def test_shared_as_writer_with_compute(self):
- upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ workspace = WorkspaceFactory.create()
group = ManagedGroupFactory.create()
sharing = WorkspaceGroupSharingFactory.create(
- workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.WRITER, can_compute=True
+ workspace=workspace, group=group, access=WorkspaceGroupSharing.WRITER, can_compute=True
)
instance = workspace_sharing_audit_results.WorkspaceSharingAuditResult(
- workspace=upload_workspace.workspace,
+ workspace=workspace,
managed_group=group,
current_sharing_instance=sharing,
note="foo",
@@ -203,13 +210,13 @@ def test_shared_as_writer_with_compute(self):
self.assertEqual(table_dictionary["can_compute"], True)
def test_shared_as_writer_without_compute(self):
- upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ workspace = WorkspaceFactory.create()
group = ManagedGroupFactory.create()
sharing = WorkspaceGroupSharingFactory.create(
- workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.WRITER, can_compute=False
+ workspace=workspace, group=group, access=WorkspaceGroupSharing.WRITER, can_compute=False
)
instance = workspace_sharing_audit_results.WorkspaceSharingAuditResult(
- workspace=upload_workspace.workspace,
+ workspace=workspace,
managed_group=group,
current_sharing_instance=sharing,
note="foo",
@@ -219,13 +226,13 @@ def test_shared_as_writer_without_compute(self):
self.assertEqual(table_dictionary["can_compute"], False)
def test_shared_as_reader(self):
- upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ workspace = WorkspaceFactory.create()
group = ManagedGroupFactory.create()
sharing = WorkspaceGroupSharingFactory.create(
- workspace=upload_workspace.workspace, group=group, access=WorkspaceGroupSharing.READER
+ workspace=workspace, group=group, access=WorkspaceGroupSharing.READER
)
instance = workspace_sharing_audit_results.WorkspaceSharingAuditResult(
- workspace=upload_workspace.workspace,
+ workspace=workspace,
managed_group=group,
current_sharing_instance=sharing,
note="foo",
@@ -235,10 +242,10 @@ def test_shared_as_reader(self):
self.assertIsNone(table_dictionary["can_compute"])
def test_not_shared(self):
- upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ workspace = WorkspaceFactory.create()
group = ManagedGroupFactory.create()
instance = workspace_sharing_audit_results.WorkspaceSharingAuditResult(
- workspace=upload_workspace.workspace,
+ workspace=workspace,
managed_group=group,
current_sharing_instance=None,
note="foo",
@@ -247,6 +254,681 @@ def test_not_shared(self):
self.assertIsNone(table_dictionary["access"])
self.assertIsNone(table_dictionary["can_compute"])
+ def test_handle_verified_shared(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=workspace,
+ group=group,
+ access=WorkspaceGroupSharing.READER,
+ )
+ instance = workspace_sharing_audit_results.VerifiedShared(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing.refresh_from_db()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.READER)
+ self.assertEqual(sharing.created, date_created)
+ self.assertEqual(sharing.modified, date_created)
+
+ def test_handle_verified_not_shared(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ group = ManagedGroupFactory.create()
+ instance = workspace_sharing_audit_results.VerifiedNotShared(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=None,
+ note="foo",
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 0)
+
+ def test_handle_share_as_reader_new(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ instance = workspace_sharing_audit_results.ShareAsReader(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=None,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "READER",
+ "canShare": False,
+ "canCompute": False,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing = WorkspaceGroupSharing.objects.first()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.READER)
+ self.assertFalse(sharing.can_compute)
+
+ def test_handle_share_as_reader_update(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=workspace,
+ group=group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=True,
+ )
+ instance = workspace_sharing_audit_results.ShareAsReader(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "READER",
+ "canShare": False,
+ "canCompute": False,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing.refresh_from_db()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.READER)
+ self.assertFalse(sharing.can_compute)
+ self.assertEqual(sharing.created, date_created)
+ self.assertGreater(sharing.modified, date_created)
+
+ def test_handle_share_as_writer_new(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ instance = workspace_sharing_audit_results.ShareAsWriter(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=None,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "WRITER",
+ "canShare": False,
+ "canCompute": False,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing = WorkspaceGroupSharing.objects.first()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.WRITER)
+ self.assertFalse(sharing.can_compute)
+
+ def test_handle_share_as_writer_update(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=workspace,
+ group=group,
+ access=WorkspaceGroupSharing.OWNER,
+ can_compute=True,
+ )
+ instance = workspace_sharing_audit_results.ShareAsWriter(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "WRITER",
+ "canShare": False,
+ "canCompute": False,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing.refresh_from_db()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.WRITER)
+ self.assertFalse(sharing.can_compute)
+ self.assertEqual(sharing.created, date_created)
+ self.assertGreater(sharing.modified, date_created)
+
+ def test_handle_share_with_compute_new(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ instance = workspace_sharing_audit_results.ShareWithCompute(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=None,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "WRITER",
+ "canShare": False,
+ "canCompute": True,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing = WorkspaceGroupSharing.objects.first()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.WRITER)
+ self.assertTrue(sharing.can_compute)
+
+ def test_handle_share_with_compute_update(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=workspace,
+ group=group,
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+ )
+ instance = workspace_sharing_audit_results.ShareWithCompute(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "WRITER",
+ "canShare": False,
+ "canCompute": True,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing.refresh_from_db()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.WRITER)
+ self.assertTrue(sharing.can_compute)
+ self.assertEqual(sharing.created, date_created)
+ self.assertGreater(sharing.modified, date_created)
+
+ def test_handle_share_as_owner_new(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ instance = workspace_sharing_audit_results.ShareAsOwner(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=None,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "OWNER",
+ "canShare": False, # We're not tracking this in ACM so we always send False.
+ "canCompute": True,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing = WorkspaceGroupSharing.objects.first()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER)
+ self.assertTrue(sharing.can_compute)
+
+ def test_handle_share_as_owner_update(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=workspace,
+ group=group,
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+ )
+ instance = workspace_sharing_audit_results.ShareAsOwner(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "OWNER",
+ "canShare": False, # We're not tracking this in ACM so we always send False.
+ "canCompute": True,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 1)
+ sharing.refresh_from_db()
+ self.assertEqual(sharing.workspace, workspace)
+ self.assertEqual(sharing.group, group)
+ self.assertEqual(sharing.access, WorkspaceGroupSharing.OWNER)
+ self.assertTrue(sharing.can_compute)
+ self.assertEqual(sharing.created, date_created)
+ self.assertGreater(sharing.modified, date_created)
+
+ def test_handle_stop_sharing(self):
+ workspace = WorkspaceFactory.create(billing_project__name="test-bp", name="test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ sharing = WorkspaceGroupSharingFactory.create(
+ workspace=workspace,
+ group=group,
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+ )
+ instance = workspace_sharing_audit_results.StopSharing(
+ workspace=workspace,
+ managed_group=group,
+ current_sharing_instance=sharing,
+ note="foo",
+ )
+ # Add the mocked API response.
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "NO ACCESS",
+ "canShare": False,
+ "canCompute": False,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ status=200,
+ match=[responses.matchers.json_params_matcher(acls)],
+ json={"invitesSent": {}, "usersNotFound": {}, "usersUpdated": acls},
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(WorkspaceGroupSharing.objects.count(), 0)
+
+
+class WorkspaceAuthDomainAuditResultTest(AnVILAPIMockTestMixin, TestCase):
+ """General tests of the WorkspaceAuthDomainAuditResult dataclasses."""
+
+ def test_handle_verified_member(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="auth-test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.MEMBER,
+ )
+ instance = workspace_auth_domain_audit_results.VerifiedMember(
+ workspace=workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.parent_group, workspace.authorization_domains.first())
+ self.assertEqual(membership.child_group, group)
+ self.assertEqual(membership.role, GroupGroupMembership.MEMBER)
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_handle_verified_admin(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="auth-test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=workspace.authorization_domains.first(), child_group=group, role=GroupGroupMembership.ADMIN
+ )
+ instance = workspace_auth_domain_audit_results.VerifiedAdmin(
+ workspace=workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.parent_group, workspace.authorization_domains.first())
+ self.assertEqual(membership.child_group, group)
+ self.assertEqual(membership.role, GroupGroupMembership.ADMIN)
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_handle_verified_not_member(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="auth-test-ws")
+ group = ManagedGroupFactory.create()
+ instance = workspace_auth_domain_audit_results.VerifiedMember(
+ workspace=workspace,
+ managed_group=group,
+ current_membership_instance=None,
+ note="foo",
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(GroupGroupMembership.objects.count(), 0)
+
+ def test_handle_add_member(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="auth-test-ws")
+ group = ManagedGroupFactory.create()
+ instance = workspace_auth_domain_audit_results.AddMember(
+ workspace=workspace,
+ managed_group=group,
+ current_membership_instance=None,
+ note="foo",
+ )
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth-test-ws/member/{group.email}",
+ status=204,
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(GroupGroupMembership.objects.count(), 1)
+ membership = GroupGroupMembership.objects.first()
+ self.assertEqual(membership.parent_group, workspace.authorization_domains.first())
+ self.assertEqual(membership.child_group, group)
+ self.assertEqual(membership.role, GroupGroupMembership.MEMBER)
+
+ def test_handle_add_admin(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="auth-test-ws")
+ group = ManagedGroupFactory.create()
+ instance = workspace_auth_domain_audit_results.AddAdmin(
+ workspace=workspace,
+ managed_group=group,
+ current_membership_instance=None,
+ note="foo",
+ )
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth-test-ws/admin/{group.email}",
+ status=204,
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(GroupGroupMembership.objects.count(), 1)
+ membership = GroupGroupMembership.objects.first()
+ self.assertEqual(membership.parent_group, workspace.authorization_domains.first())
+ self.assertEqual(membership.child_group, group)
+ self.assertEqual(membership.role, GroupGroupMembership.ADMIN)
+
+ def test_handle_change_to_member(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="auth-test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.ADMIN,
+ )
+ instance = workspace_auth_domain_audit_results.ChangeToMember(
+ workspace=workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth-test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth-test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(GroupGroupMembership.objects.count(), 1)
+ membership = GroupGroupMembership.objects.first()
+ self.assertEqual(membership.parent_group, workspace.authorization_domains.first())
+ self.assertEqual(membership.child_group, group)
+ self.assertEqual(membership.role, GroupGroupMembership.MEMBER)
+ self.assertGreater(membership.modified, date_created)
+
+ def test_handle_change_to_admin(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="auth-test-ws")
+ group = ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(days=1)
+ with freeze_time(date_created):
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.MEMBER,
+ )
+ instance = workspace_auth_domain_audit_results.ChangeToAdmin(
+ workspace=workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth-test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth-test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(GroupGroupMembership.objects.count(), 1)
+ membership = GroupGroupMembership.objects.first()
+ self.assertEqual(membership.parent_group, workspace.authorization_domains.first())
+ self.assertEqual(membership.child_group, group)
+ self.assertEqual(membership.role, GroupGroupMembership.ADMIN)
+ self.assertGreater(membership.modified, date_created)
+
+ def test_handle_remove(self):
+ workspace = WorkspaceFactory.create(name="test-ws")
+ WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="auth-test-ws")
+ group = ManagedGroupFactory.create()
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.MEMBER,
+ )
+ instance = workspace_auth_domain_audit_results.Remove(
+ workspace=workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth-test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.assertFalse(instance.handled)
+ instance.handle()
+ self.assertTrue(instance.handled)
+ self.assertEqual(GroupGroupMembership.objects.count(), 0)
+
+ def test_member_as_admin(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.ADMIN,
+ )
+ instance = workspace_auth_domain_audit_results.WorkspaceAuthDomainAuditResult(
+ workspace=upload_workspace.workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertEqual(table_dictionary["role"], membership.ADMIN)
+
+ def test_member_as_member(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ membership = GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=GroupGroupMembership.MEMBER,
+ )
+ instance = workspace_auth_domain_audit_results.WorkspaceAuthDomainAuditResult(
+ workspace=upload_workspace.workspace,
+ managed_group=group,
+ current_membership_instance=membership,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertEqual(table_dictionary["role"], membership.MEMBER)
+
+ def test_not_member(self):
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = ManagedGroupFactory.create()
+ instance = workspace_auth_domain_audit_results.WorkspaceAuthDomainAuditResult(
+ workspace=upload_workspace.workspace,
+ managed_group=group,
+ current_membership_instance=None,
+ note="foo",
+ )
+ table_dictionary = instance.get_table_dictionary()
+ self.assertIsNone(table_dictionary["role"])
+
class UploadWorkspaceSharingAuditTableTest(TestCase):
"""General tests of the UploadWorkspaceSharingAuditTable class."""
@@ -3967,56 +4649,6 @@ def test_other_group_shared_as_owner(self):
self.assertEqual(record.note, upload_workspace_audit.UploadWorkspaceSharingAudit.OTHER_GROUP_NO_ACCESS)
-class WorkspaceAuthDomainAuditResultTest(TestCase):
- """General tests of the WorkspaceAuthDomainAuditResult dataclasses."""
-
- def test_member_as_admin(self):
- upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
- group = ManagedGroupFactory.create()
- membership = GroupGroupMembershipFactory.create(
- parent_group=upload_workspace.workspace.authorization_domains.first(),
- child_group=group,
- role=GroupGroupMembership.ADMIN,
- )
- instance = workspace_auth_domain_audit_results.WorkspaceAuthDomainAuditResult(
- workspace=upload_workspace.workspace,
- managed_group=group,
- current_membership_instance=membership,
- note="foo",
- )
- table_dictionary = instance.get_table_dictionary()
- self.assertEqual(table_dictionary["role"], membership.ADMIN)
-
- def test_member_as_member(self):
- upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
- group = ManagedGroupFactory.create()
- membership = GroupGroupMembershipFactory.create(
- parent_group=upload_workspace.workspace.authorization_domains.first(),
- child_group=group,
- role=GroupGroupMembership.MEMBER,
- )
- instance = workspace_auth_domain_audit_results.WorkspaceAuthDomainAuditResult(
- workspace=upload_workspace.workspace,
- managed_group=group,
- current_membership_instance=membership,
- note="foo",
- )
- table_dictionary = instance.get_table_dictionary()
- self.assertEqual(table_dictionary["role"], membership.MEMBER)
-
- def test_not_member(self):
- upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
- group = ManagedGroupFactory.create()
- instance = workspace_auth_domain_audit_results.WorkspaceAuthDomainAuditResult(
- workspace=upload_workspace.workspace,
- managed_group=group,
- current_membership_instance=None,
- note="foo",
- )
- table_dictionary = instance.get_table_dictionary()
- self.assertIsNone(table_dictionary["role"])
-
-
class UploadWorkspaceAuthDomainAuditTableTest(TestCase):
"""General tests of the UploadWorkspaceAuthDomainAuditTable class."""
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index 343adf0b..f8c37741 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -6,10 +6,8 @@
from anvil_consortium_manager.exceptions import AnVILGroupNotFound
from anvil_consortium_manager.models import (
Account,
- GroupGroupMembership,
ManagedGroup,
Workspace,
- WorkspaceGroupSharing,
)
from django.contrib import messages
from django.contrib.auth import get_user_model
@@ -28,8 +26,6 @@
from .audit import (
combined_workspace_audit,
upload_workspace_audit,
- workspace_auth_domain_audit_results,
- workspace_sharing_audit_results,
)
User = get_user_model()
@@ -336,40 +332,8 @@ def get_success_url(self):
def form_valid(self, form):
# Handle the result.
try:
- # Set up the sharing instance.
- if self.audit_result.current_sharing_instance:
- sharing = self.audit_result.current_sharing_instance
- else:
- sharing = WorkspaceGroupSharing(
- workspace=self.upload_workspace.workspace,
- group=self.managed_group,
- )
with transaction.atomic():
- if isinstance(self.audit_result, workspace_sharing_audit_results.VerifiedShared):
- # No changes needed.
- pass
- elif isinstance(self.audit_result, workspace_sharing_audit_results.VerifiedNotShared):
- # No changes needed.
- pass
- elif isinstance(self.audit_result, workspace_sharing_audit_results.StopSharing):
- sharing.anvil_delete()
- sharing.delete()
- else:
- if isinstance(self.audit_result, workspace_sharing_audit_results.ShareAsReader):
- sharing.access = WorkspaceGroupSharing.READER
- sharing.can_compute = False
- elif isinstance(self.audit_result, workspace_sharing_audit_results.ShareAsWriter):
- sharing.access = WorkspaceGroupSharing.WRITER
- sharing.can_compute = False
- elif isinstance(self.audit_result, workspace_sharing_audit_results.ShareWithCompute):
- sharing.access = WorkspaceGroupSharing.WRITER
- sharing.can_compute = True
- elif isinstance(self.audit_result, workspace_sharing_audit_results.ShareAsOwner):
- sharing.access = WorkspaceGroupSharing.OWNER
- sharing.can_compute = True
- sharing.full_clean()
- sharing.save()
- sharing.anvil_create_or_update()
+ self.audit_result.handle()
except (AnVILAPIError, AnVILGroupNotFound) as e:
if self.request.htmx:
return HttpResponse(self.htmx_error)
@@ -526,39 +490,7 @@ def form_valid(self, form):
# Handle the result.
try:
with transaction.atomic():
- # Set up the membership instance.
- if self.audit_result.current_membership_instance:
- membership = self.audit_result.current_membership_instance
- else:
- membership = GroupGroupMembership(
- parent_group=self.upload_workspace.workspace.authorization_domains.first(),
- child_group=self.managed_group,
- )
- # Now process the result.
- if isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedMember):
- pass
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedAdmin):
- pass
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedNotMember):
- pass
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.Remove):
- membership.anvil_delete()
- membership.delete()
- else:
- if isinstance(self.audit_result, workspace_auth_domain_audit_results.ChangeToMember):
- membership.anvil_delete()
- membership.role = GroupGroupMembership.MEMBER
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.ChangeToAdmin):
- membership.anvil_delete()
- membership.role = GroupGroupMembership.ADMIN
- else:
- if isinstance(self.audit_result, workspace_auth_domain_audit_results.AddMember):
- membership.role = GroupGroupMembership.MEMBER
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.AddAdmin):
- membership.role = GroupGroupMembership.ADMIN
- membership.full_clean()
- membership.save()
- membership.anvil_create()
+ self.audit_result.handle()
except (AnVILAPIError, AnVILGroupNotFound) as e:
if self.request.htmx:
return HttpResponse(self.htmx_error)
@@ -686,40 +618,8 @@ def get_success_url(self):
def form_valid(self, form):
# Handle the result.
try:
- # Set up the sharing instance.
- if self.audit_result.current_sharing_instance:
- sharing = self.audit_result.current_sharing_instance
- else:
- sharing = WorkspaceGroupSharing(
- workspace=self.combined_workspace.workspace,
- group=self.managed_group,
- )
with transaction.atomic():
- if isinstance(self.audit_result, workspace_sharing_audit_results.VerifiedShared):
- # No changes needed.
- pass
- elif isinstance(self.audit_result, workspace_sharing_audit_results.VerifiedNotShared):
- # No changes needed.
- pass
- elif isinstance(self.audit_result, workspace_sharing_audit_results.StopSharing):
- sharing.anvil_delete()
- sharing.delete()
- else:
- if isinstance(self.audit_result, workspace_sharing_audit_results.ShareAsReader):
- sharing.access = WorkspaceGroupSharing.READER
- sharing.can_compute = False
- elif isinstance(self.audit_result, workspace_sharing_audit_results.ShareAsWriter):
- sharing.access = WorkspaceGroupSharing.WRITER
- sharing.can_compute = False
- elif isinstance(self.audit_result, workspace_sharing_audit_results.ShareWithCompute):
- sharing.access = WorkspaceGroupSharing.WRITER
- sharing.can_compute = True
- elif isinstance(self.audit_result, workspace_sharing_audit_results.ShareAsOwner):
- sharing.access = WorkspaceGroupSharing.OWNER
- sharing.can_compute = True
- sharing.full_clean()
- sharing.save()
- sharing.anvil_create_or_update()
+ self.audit_result.handle()
except (AnVILAPIError, AnVILGroupNotFound) as e:
if self.request.htmx:
return HttpResponse(self.htmx_error)
@@ -849,39 +749,7 @@ def form_valid(self, form):
# Handle the result.
try:
with transaction.atomic():
- # Set up the membership instance.
- if self.audit_result.current_membership_instance:
- membership = self.audit_result.current_membership_instance
- else:
- membership = GroupGroupMembership(
- parent_group=self.workspace.workspace.authorization_domains.first(),
- child_group=self.managed_group,
- )
- # Now process the result.
- if isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedMember):
- pass
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedAdmin):
- pass
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.VerifiedNotMember):
- pass
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.Remove):
- membership.anvil_delete()
- membership.delete()
- else:
- if isinstance(self.audit_result, workspace_auth_domain_audit_results.ChangeToMember):
- membership.anvil_delete()
- membership.role = GroupGroupMembership.MEMBER
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.ChangeToAdmin):
- membership.anvil_delete()
- membership.role = GroupGroupMembership.ADMIN
- else:
- if isinstance(self.audit_result, workspace_auth_domain_audit_results.AddMember):
- membership.role = GroupGroupMembership.MEMBER
- elif isinstance(self.audit_result, workspace_auth_domain_audit_results.AddAdmin):
- membership.role = GroupGroupMembership.ADMIN
- membership.full_clean()
- membership.save()
- membership.anvil_create()
+ self.audit_result.handle()
except (AnVILAPIError, AnVILGroupNotFound) as e:
if self.request.htmx:
return HttpResponse(self.htmx_error)
From 23ff9b206b677c6cdf18a73e3759461f9a48e4aa Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 15 Oct 2024 11:26:09 -0700
Subject: [PATCH 68/97] Add a view mixin for audit resolve views
Use the new mixin in the existing views.
---
gregor_django/gregor_anvil/viewmixins.py | 64 +++++++
gregor_django/gregor_anvil/views.py | 227 +++--------------------
2 files changed, 85 insertions(+), 206 deletions(-)
diff --git a/gregor_django/gregor_anvil/viewmixins.py b/gregor_django/gregor_anvil/viewmixins.py
index 4eba88f8..37d2c48b 100644
--- a/gregor_django/gregor_anvil/viewmixins.py
+++ b/gregor_django/gregor_anvil/viewmixins.py
@@ -1,3 +1,13 @@
+from anvil_consortium_manager.anvil_api import AnVILAPIError
+from anvil_consortium_manager.exceptions import AnVILGroupNotFound
+from anvil_consortium_manager.models import (
+ ManagedGroup,
+)
+from django.contrib import messages
+from django.db import transaction
+from django.http import Http404, HttpResponse
+
+
class AuditMixin:
"""Mixin to assist with auditing views."""
@@ -14,3 +24,57 @@ def get_context_data(self, **kwargs):
context["needs_action_table"] = audit_results.get_needs_action_table()
context["audit_results"] = audit_results
return context
+
+
+class AuditResolveMixin:
+ """Mixin to assist with audit resolution views."""
+
+ def get_workspace_data_object(self):
+ raise NotImplementedError("AuditResolveMixin.get_workspace_data_object() must be implemented in a subclass")
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["workspace_data_object"] = self.workspace_data_object
+ context["managed_group"] = self.managed_group
+ context["audit_result"] = self.audit_result
+ return context
+
+ def get_managed_group(self, queryset=None):
+ """Look up the ManagedGroup by name."""
+ try:
+ obj = ManagedGroup.objects.get(name=self.kwargs.get("managed_group_slug", None))
+ except ManagedGroup.DoesNotExist:
+ raise Http404("No ManagedGroups found matching the query")
+ return obj
+
+ def get(self, request, *args, **kwargs):
+ self.workspace_data_object = self.get_workspace_data_object()
+ self.managed_group = self.get_managed_group()
+ self.audit_result = self.get_audit_result()
+ return super().get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ self.workspace_data_object = self.get_workspace_data_object()
+ self.managed_group = self.get_managed_group()
+ self.audit_result = self.get_audit_result()
+ return super().post(request, *args, **kwargs)
+
+ def form_valid(self, form):
+ # Handle the result.
+ try:
+ with transaction.atomic():
+ self.audit_result.handle()
+ except (AnVILAPIError, AnVILGroupNotFound) as e:
+ if self.request.htmx:
+ return HttpResponse(self.htmx_error)
+ else:
+ messages.error(self.request, "AnVIL API Error: " + str(e))
+ return super().form_invalid(form)
+ # Otherwise, the audit resolution succeeded.
+ if self.request.htmx:
+ return HttpResponse(self.htmx_success)
+ else:
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return self.workspace_data_object.get_absolute_url()
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index f8c37741..0b138be3 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -1,21 +1,16 @@
-from anvil_consortium_manager.anvil_api import AnVILAPIError
from anvil_consortium_manager.auth import (
AnVILConsortiumManagerStaffEditRequired,
AnVILConsortiumManagerStaffViewRequired,
)
-from anvil_consortium_manager.exceptions import AnVILGroupNotFound
from anvil_consortium_manager.models import (
Account,
- ManagedGroup,
Workspace,
)
-from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.messages.views import SuccessMessageMixin
-from django.db import transaction
from django.db.models import Count, Q
from django.forms import Form
-from django.http import Http404, HttpResponse
+from django.http import Http404
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, FormView, TemplateView, UpdateView
from django_tables2 import MultiTableMixin, SingleTableView
@@ -265,7 +260,9 @@ def run_audit(self, **kwargs):
return audit
-class UploadWorkspaceSharingAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
+class UploadWorkspaceSharingAuditResolve(
+ AnVILConsortiumManagerStaffEditRequired, viewmixins.AuditResolveMixin, FormView
+):
"""View to resolve UploadWorkspace audit results."""
form_class = Form
@@ -273,7 +270,7 @@ class UploadWorkspaceSharingAuditResolve(AnVILConsortiumManagerStaffEditRequired
htmx_success = """ Handled!"""
htmx_error = """ Error!"""
- def get_upload_workspace(self):
+ def get_workspace_data_object(self):
"""Look up the UploadWorkspace by billing project and name."""
# Filter the queryset based on kwargs.
billing_project_slug = self.kwargs.get("billing_project_slug", None)
@@ -291,61 +288,14 @@ def get_upload_workspace(self):
)
return obj
- def get_managed_group(self, queryset=None):
- """Look up the ManagedGroup by name."""
- try:
- obj = ManagedGroup.objects.get(name=self.kwargs.get("managed_group_slug", None))
- except ManagedGroup.DoesNotExist:
- raise Http404("No ManagedGroups found matching the query")
- return obj
-
def get_audit_result(self):
audit = upload_workspace_audit.UploadWorkspaceSharingAudit()
# No way to set the group queryset, since it is dynamically determined by the workspace.
- audit.audit_workspace_and_group(self.upload_workspace, self.managed_group)
+ audit.audit_workspace_and_group(self.workspace_data_object, self.managed_group)
# Set to completed, because we are just running this one specific check.
audit.completed = True
return audit.get_all_results()[0]
- def get(self, request, *args, **kwargs):
- self.upload_workspace = self.get_upload_workspace()
- self.managed_group = self.get_managed_group()
- self.audit_result = self.get_audit_result()
- return super().get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- self.upload_workspace = self.get_upload_workspace()
- self.managed_group = self.get_managed_group()
- self.audit_result = self.get_audit_result()
- return super().post(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["upload_workspace"] = self.upload_workspace
- context["managed_group"] = self.managed_group
- context["audit_result"] = self.audit_result
- return context
-
- def get_success_url(self):
- return self.upload_workspace.get_absolute_url()
-
- def form_valid(self, form):
- # Handle the result.
- try:
- with transaction.atomic():
- self.audit_result.handle()
- except (AnVILAPIError, AnVILGroupNotFound) as e:
- if self.request.htmx:
- return HttpResponse(self.htmx_error)
- else:
- messages.error(self.request, "AnVIL API Error: " + str(e))
- return super().form_invalid(form)
- # Otherwise, the audit resolution succeeded.
- if self.request.htmx:
- return HttpResponse(self.htmx_success)
- else:
- return super().form_valid(form)
-
class UploadWorkspaceAuthDomainAudit(AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, TemplateView):
"""View to audit UploadWorkspace auth domain membership for all UploadWorkspaces."""
@@ -422,7 +372,9 @@ def run_audit(self, **kwargs):
return audit
-class UploadWorkspaceAuthDomainAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
+class UploadWorkspaceAuthDomainAuditResolve(
+ AnVILConsortiumManagerStaffEditRequired, viewmixins.AuditResolveMixin, FormView
+):
"""View to resolve UploadWorkspace auth domain audit results."""
form_class = Form
@@ -430,7 +382,7 @@ class UploadWorkspaceAuthDomainAuditResolve(AnVILConsortiumManagerStaffEditRequi
htmx_success = """ Handled!"""
htmx_error = """ Error!"""
- def get_upload_workspace(self):
+ def get_workspace_data_object(self):
"""Look up the UploadWorkspace by billing project and name."""
# Filter the queryset based on kwargs.
billing_project_slug = self.kwargs.get("billing_project_slug", None)
@@ -448,61 +400,14 @@ def get_upload_workspace(self):
)
return obj
- def get_managed_group(self, queryset=None):
- """Look up the ManagedGroup by name."""
- try:
- obj = ManagedGroup.objects.get(name=self.kwargs.get("managed_group_slug", None))
- except ManagedGroup.DoesNotExist:
- raise Http404("No ManagedGroups found matching the query")
- return obj
-
def get_audit_result(self):
audit = upload_workspace_audit.UploadWorkspaceAuthDomainAudit()
# No way to set the group queryset, since it is dynamically determined by the workspace.
- audit.audit_workspace_and_group(self.upload_workspace, self.managed_group)
+ audit.audit_workspace_and_group(self.workspace_data_object, self.managed_group)
# Set to completed, because we are just running this one specific check.
audit.completed = True
return audit.get_all_results()[0]
- def get(self, request, *args, **kwargs):
- self.upload_workspace = self.get_upload_workspace()
- self.managed_group = self.get_managed_group()
- self.audit_result = self.get_audit_result()
- return super().get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- self.upload_workspace = self.get_upload_workspace()
- self.managed_group = self.get_managed_group()
- self.audit_result = self.get_audit_result()
- return super().post(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["upload_workspace"] = self.upload_workspace
- context["managed_group"] = self.managed_group
- context["audit_result"] = self.audit_result
- return context
-
- def get_success_url(self):
- return self.upload_workspace.get_absolute_url()
-
- def form_valid(self, form):
- # Handle the result.
- try:
- with transaction.atomic():
- self.audit_result.handle()
- except (AnVILAPIError, AnVILGroupNotFound) as e:
- if self.request.htmx:
- return HttpResponse(self.htmx_error)
- else:
- messages.error(self.request, "AnVIL API Error: " + str(e))
- return super().form_invalid(form)
- # Otherwise, the audit resolution succeeded.
- if self.request.htmx:
- return HttpResponse(self.htmx_success)
- else:
- return super().form_valid(form)
-
class CombinedConsortiumDataWorkspaceSharingAudit(
AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, TemplateView
@@ -551,7 +456,9 @@ def run_audit(self, **kwargs):
return audit
-class CombinedConsortiumDataWorkspaceSharingAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
+class CombinedConsortiumDataWorkspaceSharingAuditResolve(
+ AnVILConsortiumManagerStaffEditRequired, viewmixins.AuditResolveMixin, FormView
+):
"""View to resolve CombinedConsortiumDataWorkspace audit results."""
form_class = Form
@@ -559,7 +466,7 @@ class CombinedConsortiumDataWorkspaceSharingAuditResolve(AnVILConsortiumManagerS
htmx_success = """ Handled!"""
htmx_error = """ Error!"""
- def get_combined_workspace(self):
+ def get_workspace_data_object(self):
"""Look up the CombinedConsortiumDataWorkspace by billing project and name."""
# Filter the queryset based on kwargs.
billing_project_slug = self.kwargs.get("billing_project_slug", None)
@@ -577,61 +484,14 @@ def get_combined_workspace(self):
)
return obj
- def get_managed_group(self, queryset=None):
- """Look up the ManagedGroup by name."""
- try:
- obj = ManagedGroup.objects.get(name=self.kwargs.get("managed_group_slug", None))
- except ManagedGroup.DoesNotExist:
- raise Http404("No ManagedGroups found matching the query")
- return obj
-
def get_audit_result(self):
audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceSharingAudit()
# No way to set the group queryset, since it is dynamically determined by the workspace.
- audit.audit_workspace_and_group(self.combined_workspace, self.managed_group)
+ audit.audit_workspace_and_group(self.workspace_data_object, self.managed_group)
# Set to completed, because we are just running this one specific check.
audit.completed = True
return audit.get_all_results()[0]
- def get(self, request, *args, **kwargs):
- self.combined_workspace = self.get_combined_workspace()
- self.managed_group = self.get_managed_group()
- self.audit_result = self.get_audit_result()
- return super().get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- self.combined_workspace = self.get_combined_workspace()
- self.managed_group = self.get_managed_group()
- self.audit_result = self.get_audit_result()
- return super().post(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["combined_workspace"] = self.combined_workspace
- context["managed_group"] = self.managed_group
- context["audit_result"] = self.audit_result
- return context
-
- def get_success_url(self):
- return self.combined_workspace.get_absolute_url()
-
- def form_valid(self, form):
- # Handle the result.
- try:
- with transaction.atomic():
- self.audit_result.handle()
- except (AnVILAPIError, AnVILGroupNotFound) as e:
- if self.request.htmx:
- return HttpResponse(self.htmx_error)
- else:
- messages.error(self.request, "AnVIL API Error: " + str(e))
- return super().form_invalid(form)
- # Otherwise, the audit resolution succeeded.
- if self.request.htmx:
- return HttpResponse(self.htmx_success)
- else:
- return super().form_valid(form)
-
class CombinedConsortiumDataWorkspaceAuthDomainAudit(
AnVILConsortiumManagerStaffViewRequired, viewmixins.AuditMixin, TemplateView
@@ -681,7 +541,9 @@ def run_audit(self, **kwargs):
return audit
-class CombinedConsortiumDataWorkspaceAuthDomainAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
+class CombinedConsortiumDataWorkspaceAuthDomainAuditResolve(
+ AnVILConsortiumManagerStaffEditRequired, viewmixins.AuditResolveMixin, FormView
+):
"""View to resolve UploadWorkspace auth domain audit results."""
form_class = Form
@@ -689,7 +551,7 @@ class CombinedConsortiumDataWorkspaceAuthDomainAuditResolve(AnVILConsortiumManag
htmx_success = """ Handled!"""
htmx_error = """ Error!"""
- def get_workspace(self):
+ def get_workspace_data_object(self):
"""Look up the CombinedConsortiumDataWorkspace by billing project and name."""
# Filter the queryset based on kwargs.
billing_project_slug = self.kwargs.get("billing_project_slug", None)
@@ -707,57 +569,10 @@ def get_workspace(self):
)
return obj
- def get_managed_group(self, queryset=None):
- """Look up the ManagedGroup by name."""
- try:
- obj = ManagedGroup.objects.get(name=self.kwargs.get("managed_group_slug", None))
- except ManagedGroup.DoesNotExist:
- raise Http404("No ManagedGroups found matching the query")
- return obj
-
def get_audit_result(self):
audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit()
# No way to set the group queryset, since it is dynamically determined by the workspace.
- audit.audit_workspace_and_group(self.workspace, self.managed_group)
+ audit.audit_workspace_and_group(self.workspace_data_object, self.managed_group)
# Set to completed, because we are just running this one specific check.
audit.completed = True
return audit.get_all_results()[0]
-
- def get(self, request, *args, **kwargs):
- self.workspace = self.get_workspace()
- self.managed_group = self.get_managed_group()
- self.audit_result = self.get_audit_result()
- return super().get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- self.workspace = self.get_workspace()
- self.managed_group = self.get_managed_group()
- self.audit_result = self.get_audit_result()
- return super().post(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["workspace"] = self.workspace
- context["managed_group"] = self.managed_group
- context["audit_result"] = self.audit_result
- return context
-
- def get_success_url(self):
- return self.workspace.get_absolute_url()
-
- def form_valid(self, form):
- # Handle the result.
- try:
- with transaction.atomic():
- self.audit_result.handle()
- except (AnVILAPIError, AnVILGroupNotFound) as e:
- if self.request.htmx:
- return HttpResponse(self.htmx_error)
- else:
- messages.error(self.request, "AnVIL API Error: " + str(e))
- return super().form_invalid(form)
- # Otherwise, the audit resolution succeeded.
- if self.request.htmx:
- return HttpResponse(self.htmx_success)
- else:
- return super().form_valid(form)
From 94058f98796cb7e8478b0550b1b0a57083235c80 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Oct 2024 19:50:08 +0000
Subject: [PATCH 69/97] Bump sphinx from 8.0.2 to 8.1.3
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 8.0.2 to 8.1.3.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v8.0.2...v8.1.3)
---
updated-dependencies:
- dependency-name: sphinx
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 132223a6..fcd7f3c7 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -161,21 +161,21 @@ sniffio==1.3.1
# via anyio
snowballstemmer==2.2.0
# via sphinx
-sphinx==8.0.2
+sphinx==8.1.3
# via
# -r requirements/dev-requirements.in
# sphinx-autobuild
sphinx-autobuild==2024.10.3
# via -r requirements/dev-requirements.in
-sphinxcontrib-applehelp==1.0.4
+sphinxcontrib-applehelp==2.0.0
# via sphinx
-sphinxcontrib-devhelp==1.0.2
+sphinxcontrib-devhelp==2.0.0
# via sphinx
-sphinxcontrib-htmlhelp==2.0.1
+sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
-sphinxcontrib-qthelp==1.0.3
+sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
From 61eabea281a8f6ec4c29fcc3188bb1b65f4958f0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Oct 2024 19:50:10 +0000
Subject: [PATCH 70/97] Bump starlette from 0.38.2 to 0.40.0 in /requirements
Bumps [starlette](https://github.com/encode/starlette) from 0.38.2 to 0.40.0.
- [Release notes](https://github.com/encode/starlette/releases)
- [Changelog](https://github.com/encode/starlette/blob/master/docs/release-notes.md)
- [Commits](https://github.com/encode/starlette/compare/0.38.2...0.40.0)
---
updated-dependencies:
- dependency-name: starlette
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 70 +++++++++++++++----------------
1 file changed, 35 insertions(+), 35 deletions(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 132223a6..3927dc49 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -12,7 +12,7 @@ anyio==4.4.0
# watchfiles
asgiref==3.8.1
# via
- # -c requirements/requirements.txt
+ # -c requirements.txt
# django
astroid==3.1.0
# via pylint
@@ -24,19 +24,19 @@ backcall==0.2.0
# via ipython
certifi==2024.7.4
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# requests
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.3.2
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# requests
click==8.1.7
# via
- # -c requirements/requirements.txt
+ # -c requirements.txt
# uvicorn
colorama==0.4.6
# via sphinx-autobuild
@@ -50,21 +50,21 @@ distlib==0.3.8
# via virtualenv
django==4.2.16
# via
- # -c requirements/requirements.txt
+ # -c requirements.txt
# django-debug-toolbar
# django-stubs
# django-stubs-ext
django-debug-toolbar==4.4.6
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
django-stubs==4.2.7
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
django-stubs-ext==5.0.0
# via django-stubs
docutils==0.20.1
# via sphinx
exceptiongroup==1.2.0
# via
- # -c requirements/test-requirements.txt
+ # -c test-requirements.txt
# anyio
executing==2.0.1
# via stack-data
@@ -76,14 +76,14 @@ identify==2.5.35
# via pre-commit
idna==3.7
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# anyio
# requests
imagesize==1.4.1
# via sphinx
ipdb==0.13.13
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
ipython==8.12.3
# via ipdb
isort==5.13.2
@@ -101,15 +101,15 @@ matplotlib-inline==0.1.6
mccabe==0.7.0
# via pylint
mypy==1.11.2
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
mypy-extensions==1.0.0
# via mypy
nodeenv==1.8.0
# via pre-commit
packaging==23.2
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# sphinx
parso==0.8.3
# via jedi
@@ -122,7 +122,7 @@ platformdirs==4.2.0
# pylint
# virtualenv
pre-commit==3.8.0
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
prompt-toolkit==3.0.43
# via ipython
ptyprocess==0.7.0
@@ -138,24 +138,24 @@ pylint==3.1.0
# pylint-django
# pylint-plugin-utils
pylint-django==2.5.5
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
pylint-plugin-utils==0.8.2
# via pylint-django
pyyaml==6.0.1
# via
- # -c requirements/test-requirements.txt
+ # -c test-requirements.txt
# pre-commit
requests==2.32.3
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# sphinx
ruff==0.6.9
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
six==1.16.0
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# asttokens
sniffio==1.3.1
# via anyio
@@ -163,10 +163,10 @@ snowballstemmer==2.2.0
# via sphinx
sphinx==8.0.2
# via
- # -r requirements/dev-requirements.in
+ # -r dev-requirements.in
# sphinx-autobuild
sphinx-autobuild==2024.10.3
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.4
# via sphinx
sphinxcontrib-devhelp==1.0.2
@@ -181,17 +181,17 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sqlparse==0.5.1
# via
- # -c requirements/requirements.txt
+ # -c requirements.txt
# django
# django-debug-toolbar
stack-data==0.6.3
# via ipython
-starlette==0.38.2
+starlette==0.40.0
# via sphinx-autobuild
tomli==2.0.1
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# django-stubs
# ipdb
# mypy
@@ -209,8 +209,8 @@ types-pyyaml==6.0.12.12
# via django-stubs
typing-extensions==4.8.0
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# anyio
# asgiref
# astroid
@@ -220,8 +220,8 @@ typing-extensions==4.8.0
# uvicorn
urllib3==2.2.2
# via
- # -c requirements/requirements.txt
- # -c requirements/test-requirements.txt
+ # -c requirements.txt
+ # -c test-requirements.txt
# requests
uvicorn==0.30.5
# via sphinx-autobuild
@@ -234,7 +234,7 @@ wcwidth==0.2.13
websockets==12.0
# via sphinx-autobuild
werkzeug==3.0.4
- # via -r requirements/dev-requirements.in
+ # via -r dev-requirements.in
# The following packages are considered to be unsafe in a requirements file:
# setuptools
From c42b072484ae738eeb7d535a6be1a5b2ec260dc4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Oct 2024 19:50:14 +0000
Subject: [PATCH 71/97] Bump mypy from 1.11.2 to 1.12.0
Bumps [mypy](https://github.com/python/mypy) from 1.11.2 to 1.12.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11.2...v1.12.0)
---
updated-dependencies:
- dependency-name: mypy
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 132223a6..fc40dd21 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -100,7 +100,7 @@ matplotlib-inline==0.1.6
# via ipython
mccabe==0.7.0
# via pylint
-mypy==1.11.2
+mypy==1.12.0
# via -r requirements/dev-requirements.in
mypy-extensions==1.0.0
# via mypy
From c4361287401226f33ddda025c9a0ece60e2f4836 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Oct 2024 19:50:54 +0000
Subject: [PATCH 72/97] Compile requirements files
---
requirements/dev-requirements.txt | 68 +++++++++++++++----------------
1 file changed, 34 insertions(+), 34 deletions(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 3927dc49..83a40d48 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -12,7 +12,7 @@ anyio==4.4.0
# watchfiles
asgiref==3.8.1
# via
- # -c requirements.txt
+ # -c requirements/requirements.txt
# django
astroid==3.1.0
# via pylint
@@ -24,19 +24,19 @@ backcall==0.2.0
# via ipython
certifi==2024.7.4
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# requests
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.3.2
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# requests
click==8.1.7
# via
- # -c requirements.txt
+ # -c requirements/requirements.txt
# uvicorn
colorama==0.4.6
# via sphinx-autobuild
@@ -50,21 +50,21 @@ distlib==0.3.8
# via virtualenv
django==4.2.16
# via
- # -c requirements.txt
+ # -c requirements/requirements.txt
# django-debug-toolbar
# django-stubs
# django-stubs-ext
django-debug-toolbar==4.4.6
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
django-stubs==4.2.7
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
django-stubs-ext==5.0.0
# via django-stubs
docutils==0.20.1
# via sphinx
exceptiongroup==1.2.0
# via
- # -c test-requirements.txt
+ # -c requirements/test-requirements.txt
# anyio
executing==2.0.1
# via stack-data
@@ -76,14 +76,14 @@ identify==2.5.35
# via pre-commit
idna==3.7
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# anyio
# requests
imagesize==1.4.1
# via sphinx
ipdb==0.13.13
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
ipython==8.12.3
# via ipdb
isort==5.13.2
@@ -101,15 +101,15 @@ matplotlib-inline==0.1.6
mccabe==0.7.0
# via pylint
mypy==1.11.2
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
mypy-extensions==1.0.0
# via mypy
nodeenv==1.8.0
# via pre-commit
packaging==23.2
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# sphinx
parso==0.8.3
# via jedi
@@ -122,7 +122,7 @@ platformdirs==4.2.0
# pylint
# virtualenv
pre-commit==3.8.0
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
prompt-toolkit==3.0.43
# via ipython
ptyprocess==0.7.0
@@ -138,24 +138,24 @@ pylint==3.1.0
# pylint-django
# pylint-plugin-utils
pylint-django==2.5.5
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
pylint-plugin-utils==0.8.2
# via pylint-django
pyyaml==6.0.1
# via
- # -c test-requirements.txt
+ # -c requirements/test-requirements.txt
# pre-commit
requests==2.32.3
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# sphinx
ruff==0.6.9
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
six==1.16.0
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# asttokens
sniffio==1.3.1
# via anyio
@@ -163,10 +163,10 @@ snowballstemmer==2.2.0
# via sphinx
sphinx==8.0.2
# via
- # -r dev-requirements.in
+ # -r requirements/dev-requirements.in
# sphinx-autobuild
sphinx-autobuild==2024.10.3
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
sphinxcontrib-applehelp==1.0.4
# via sphinx
sphinxcontrib-devhelp==1.0.2
@@ -181,7 +181,7 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sqlparse==0.5.1
# via
- # -c requirements.txt
+ # -c requirements/requirements.txt
# django
# django-debug-toolbar
stack-data==0.6.3
@@ -190,8 +190,8 @@ starlette==0.40.0
# via sphinx-autobuild
tomli==2.0.1
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# django-stubs
# ipdb
# mypy
@@ -209,8 +209,8 @@ types-pyyaml==6.0.12.12
# via django-stubs
typing-extensions==4.8.0
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# anyio
# asgiref
# astroid
@@ -220,8 +220,8 @@ typing-extensions==4.8.0
# uvicorn
urllib3==2.2.2
# via
- # -c requirements.txt
- # -c test-requirements.txt
+ # -c requirements/requirements.txt
+ # -c requirements/test-requirements.txt
# requests
uvicorn==0.30.5
# via sphinx-autobuild
@@ -234,7 +234,7 @@ wcwidth==0.2.13
websockets==12.0
# via sphinx-autobuild
werkzeug==3.0.4
- # via -r dev-requirements.in
+ # via -r requirements/dev-requirements.in
# The following packages are considered to be unsafe in a requirements file:
# setuptools
From f06f15d8cf9f2dcd904bc88cd490d707041f1bf5 Mon Sep 17 00:00:00 2001
From: Jonas Carson
Date: Wed, 16 Oct 2024 08:22:18 -0700
Subject: [PATCH 73/97] Fix issue where id token was changing when test past
second boundary. Use same time variable.
---
gregor_django/drupal_oauth_provider/tests.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/gregor_django/drupal_oauth_provider/tests.py b/gregor_django/drupal_oauth_provider/tests.py
index 214787e6..ca2b598d 100644
--- a/gregor_django/drupal_oauth_provider/tests.py
+++ b/gregor_django/drupal_oauth_provider/tests.py
@@ -102,6 +102,7 @@ def setUp(self):
User = get_user_model()
User.objects.create_user("testuser", "testuser@testuser.com", "testpw")
self.client.login(username="testuser", password="testpw")
+ self.setup_time = datetime.datetime.now(datetime.timezone.utc)
# Provide two mocked responses, first is to the public key request
# second is used for the profile request for extra data
@@ -175,13 +176,12 @@ def login(self, resp_mock=None, process="login", with_refresh_token=True):
return resp
def get_id_token(self):
- now = datetime.datetime.now(datetime.timezone.utc)
app = get_adapter().get_app(request=None, provider=self.provider_id)
allowed_audience = app.client_id
return sign_id_token(
{
- "exp": now + datetime.timedelta(hours=1),
- "iat": now,
+ "exp": self.setup_time + datetime.timedelta(hours=1),
+ "iat": self.setup_time,
"aud": allowed_audience,
"scope": ["authenticated", "oauth_client_user"],
"sub": 20122,
From dde4051a893f79881acc5e0f419261806a191e31 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 16 Oct 2024 20:03:11 +0000
Subject: [PATCH 74/97] Bump types-requests from 2.32.0.20240914 to
2.32.0.20241016
Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20240914 to 2.32.0.20241016.
- [Commits](https://github.com/python/typeshed/commits)
---
updated-dependencies:
- dependency-name: types-requests
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/test-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-requirements.txt b/requirements/test-requirements.txt
index 0256c88b..795d352d 100644
--- a/requirements/test-requirements.txt
+++ b/requirements/test-requirements.txt
@@ -87,7 +87,7 @@ tomli==2.0.1
# -c requirements/requirements.txt
# coverage
# pytest
-types-requests==2.32.0.20240914
+types-requests==2.32.0.20241016
# via -r requirements/test-requirements.in
typing-extensions==4.8.0
# via
From 5633de6d4574667a6fcf6ecfaaf42f3c1135ee14 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Wed, 16 Oct 2024 13:36:23 -0700
Subject: [PATCH 75/97] Add a management command to audit combined workspaces
---
.../commands/run_combined_workspace_audit.py | 72 +++++
.../gregor_anvil/tests/test_commands.py | 300 ++++++++++++++++++
2 files changed, 372 insertions(+)
create mode 100644 gregor_django/gregor_anvil/management/commands/run_combined_workspace_audit.py
diff --git a/gregor_django/gregor_anvil/management/commands/run_combined_workspace_audit.py b/gregor_django/gregor_anvil/management/commands/run_combined_workspace_audit.py
new file mode 100644
index 00000000..f010052f
--- /dev/null
+++ b/gregor_django/gregor_anvil/management/commands/run_combined_workspace_audit.py
@@ -0,0 +1,72 @@
+from django.contrib.sites.models import Site
+from django.core.mail import send_mail
+from django.core.management.base import BaseCommand
+from django.template.loader import render_to_string
+from django.urls import reverse
+
+from ...audit import combined_workspace_audit
+
+
+class Command(BaseCommand):
+ help = "Run access audits on CombinedConsortiumDataWorkspaces."
+
+ def add_arguments(self, parser):
+ email_group = parser.add_argument_group(title="Email reports")
+ email_group.add_argument(
+ "--email",
+ help="""Email to which to send audit reports that need action or have errors.""",
+ )
+
+ def run_sharing_audit(self, *args, **options):
+ self.stdout.write("Running CombinedConsortiumDataWorkspace sharing audit... ", ending="")
+ audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceSharingAudit()
+ audit.run_audit()
+ self._handle_audit_results(audit, reverse("gregor_anvil:audit:combined_workspaces:sharing:all"), **options)
+
+ def run_auth_domain_audit(self, *args, **options):
+ self.stdout.write("Running CombinedConsortiumDataWorkspace auth domain audit... ", ending="")
+ audit = combined_workspace_audit.CombinedConsortiumDataWorkspaceAuthDomainAudit()
+ audit.run_audit()
+ self._handle_audit_results(audit, reverse("gregor_anvil:audit:combined_workspaces:auth_domains:all"), **options)
+
+ def _handle_audit_results(self, audit, url, **options):
+ # Report errors and needs access.
+ audit_ok = audit.ok()
+ # Construct the url for handling errors.
+ url = "https://" + Site.objects.get_current().domain + url
+ if audit_ok:
+ self.stdout.write(self.style.SUCCESS("ok!"))
+ else:
+ self.stdout.write(self.style.ERROR("problems found."))
+
+ # Print results
+ self.stdout.write("* Verified: {}".format(len(audit.verified)))
+ self.stdout.write("* Needs action: {}".format(len(audit.needs_action)))
+ self.stdout.write("* Errors: {}".format(len(audit.errors)))
+
+ if not audit_ok:
+ self.stdout.write(self.style.ERROR(f"Please visit {url} to resolve these issues."))
+
+ # Send email if requested and there are problems.
+ email = options["email"]
+ subject = "{} - problems found".format(audit.__class__.__name__)
+ html_body = render_to_string(
+ "gregor_anvil/email_audit_report.html",
+ context={
+ "title": "Combined workspace audit",
+ "audit_results": audit,
+ "url": url,
+ },
+ )
+ send_mail(
+ subject,
+ "Audit problems found. Please see attached report.",
+ None,
+ [email],
+ fail_silently=False,
+ html_message=html_body,
+ )
+
+ def handle(self, *args, **options):
+ self.run_sharing_audit(*args, **options)
+ self.run_auth_domain_audit(*args, **options)
diff --git a/gregor_django/gregor_anvil/tests/test_commands.py b/gregor_django/gregor_anvil/tests/test_commands.py
index b39cbfbe..33568383 100644
--- a/gregor_django/gregor_anvil/tests/test_commands.py
+++ b/gregor_django/gregor_anvil/tests/test_commands.py
@@ -1,5 +1,6 @@
"""Tests for management commands in the `gregor_anvil` app."""
+from datetime import timedelta
from io import StringIO
from anvil_consortium_manager.models import GroupGroupMembership, WorkspaceGroupSharing
@@ -14,6 +15,7 @@
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
+from django.utils import timezone
from . import factories
@@ -364,3 +366,301 @@ def test_auth_domain_audit_one_instance_needs_action_link_in_output(self):
self.assertIn(url, out.getvalue())
# Zero messages have been sent by default.
self.assertEqual(len(mail.outbox), 0)
+
+
+class RunCombinedWorkspaceAuditTestCase(TestCase):
+ def test_no_workspaces(self):
+ """Test command output."""
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace sharing audit... ok!",
+ "* Verified: 0",
+ "* Needs action: 0",
+ "* Errors: 0",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace auth domain audit... ok!",
+ "* Verified: 0",
+ "* Needs action: 0",
+ "* Errors: 0",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_sharing_audit_one_instance_verified(self):
+ """Test command output with one verified instance."""
+ factories.CombinedConsortiumDataWorkspaceFactory.create()
+ # Verified not shared with auth domain.
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace sharing audit... ok!",
+ "* Verified: 1",
+ "* Needs action: 0",
+ "* Errors: 0",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_sharing_audit_one_instance_needs_action(self):
+ """Test command output with one needs_action instance."""
+ # Create a workspace and matching DAR.
+ factories.CombinedConsortiumDataWorkspaceFactory.create(date_completed=timezone.now() - timedelta(days=1))
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace sharing audit... problems found.",
+ "* Verified: 0",
+ "* Needs action: 1",
+ "* Errors: 0",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_sharing_audit_one_instance_error(self):
+ """Test command output with one error instance."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=workspace.workspace.authorization_domains.first(),
+ access=WorkspaceGroupSharing.OWNER,
+ )
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace sharing audit... problems found.",
+ "* Verified: 0",
+ "* Needs action: 0",
+ "* Errors: 1",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_sharing_audit_one_instance_verified_email(self):
+ """No email is sent when there are no errors."""
+ factories.CombinedConsortiumDataWorkspaceFactory.create()
+ # Verified not shared with auth domain.
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", email="test@example.com", stdout=out)
+ self.assertIn("Running CombinedConsortiumDataWorkspace sharing audit... ok!", out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_sharing_audit_one_instance_needs_action_email(self):
+ """Email is sent for one needs_action instance."""
+ # Create a workspace and matching DAR.
+ factories.CombinedConsortiumDataWorkspaceFactory.create(date_completed=timezone.now() - timedelta(days=1))
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", email="test@example.com", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace sharing audit... problems found.",
+ "* Verified: 0",
+ "* Needs action: 1",
+ "* Errors: 0",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # One message has been sent by default.
+ self.assertEqual(len(mail.outbox), 1)
+ email = mail.outbox[0]
+ self.assertEqual(email.to, ["test@example.com"])
+ self.assertEqual(email.subject, "CombinedConsortiumDataWorkspaceSharingAudit - problems found")
+
+ def test_sharing_audit_one_instance_error_email(self):
+ """Test command output with one error instance."""
+ # Create a workspace and matching DAR.
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=workspace.workspace.authorization_domains.first(),
+ access=WorkspaceGroupSharing.OWNER,
+ )
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", email="test@example.com", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace sharing audit... problems found.",
+ "* Verified: 0",
+ "* Needs action: 0",
+ "* Errors: 1",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # One message has been sent by default.
+ self.assertEqual(len(mail.outbox), 1)
+ email = mail.outbox[0]
+ self.assertEqual(email.to, ["test@example.com"])
+ self.assertEqual(email.subject, "CombinedConsortiumDataWorkspaceSharingAudit - problems found")
+
+ def test_sharing_audit_one_instance_needs_action_link_in_output(self):
+ factories.CombinedConsortiumDataWorkspaceFactory.create(date_completed=timezone.now() - timedelta(days=1))
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ url = reverse("gregor_anvil:audit:combined_workspaces:sharing:all")
+ self.assertIn(url, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_sharing_audit_different_domain(self):
+ """Test command output when a different domain is specified."""
+ site = Site.objects.create(domain="foobar.com", name="test")
+ site.save()
+ with self.settings(SITE_ID=site.id):
+ factories.CombinedConsortiumDataWorkspaceFactory.create(date_completed=timezone.now() - timedelta(days=1))
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ self.assertIn("Running CombinedConsortiumDataWorkspace sharing audit... problems found.", out.getvalue())
+ self.assertIn("https://foobar.com", out.getvalue())
+
+ def test_auth_domain_audit_one_instance_verified(self):
+ """Test command output with one verified instance."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group__name=settings.ANVIL_DCC_ADMINS_GROUP_NAME,
+ role=GroupGroupMembership.ADMIN,
+ )
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace auth domain audit... ok!",
+ "* Verified: 1",
+ "* Needs action: 0",
+ "* Errors: 0",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_auth_domain_audit_one_instance_needs_action(self):
+ """Test command output with one needs_action instance."""
+ factories.CombinedConsortiumDataWorkspaceFactory.create()
+ ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace auth domain audit... problems found.",
+ "* Verified: 0",
+ "* Needs action: 1",
+ "* Errors: 0",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_auth_domain_audit_one_instance_error(self):
+ """Test command output with one error instance."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ role=GroupGroupMembership.ADMIN,
+ )
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace auth domain audit... problems found.",
+ "* Verified: 0",
+ "* Needs action: 0",
+ "* Errors: 1",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_auth_domain_audit_one_instance_verified_email(self):
+ """No email is sent when there are no errors."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ child_group__name="GREGOR_ALL",
+ role=GroupGroupMembership.MEMBER,
+ )
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", email="test@example.com", stdout=out)
+ self.assertIn("Running CombinedConsortiumDataWorkspace auth domain audit... ok!", out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_auth_domain_audit_one_instance_needs_action_email(self):
+ """Email is sent for one needs_action instance."""
+ # Create a workspace and matching DAR.
+ factories.CombinedConsortiumDataWorkspaceFactory.create()
+ factories.ManagedGroupFactory.create(name="GREGOR_ALL")
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", email="test@example.com", stdout=out)
+ # One message has been sent by default.
+ self.assertEqual(len(mail.outbox), 1)
+ email = mail.outbox[0]
+ self.assertEqual(email.to, ["test@example.com"])
+ self.assertEqual(email.subject, "CombinedConsortiumDataWorkspaceAuthDomainAudit - problems found")
+
+ def test_auth_domain_audit_one_instance_error_email(self):
+ """Test command output with one error instance."""
+ workspace = factories.CombinedConsortiumDataWorkspaceFactory.create()
+ GroupGroupMembershipFactory.create(
+ parent_group=workspace.workspace.authorization_domains.first(),
+ role=GroupGroupMembership.ADMIN,
+ )
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", email="test@example.com", stdout=out)
+ expected_string = "\n".join(
+ [
+ "Running CombinedConsortiumDataWorkspace auth domain audit... problems found.",
+ "* Verified: 0",
+ "* Needs action: 0",
+ "* Errors: 1",
+ ]
+ )
+ self.assertIn(expected_string, out.getvalue())
+ # One message has been sent by default.
+ self.assertEqual(len(mail.outbox), 1)
+ email = mail.outbox[0]
+ self.assertEqual(email.to, ["test@example.com"])
+ self.assertEqual(email.subject, "CombinedConsortiumDataWorkspaceAuthDomainAudit - problems found")
+
+ def test_auth_domain_audit_different_domain(self):
+ """Test command output when a different domain is specified."""
+ site = Site.objects.create(domain="foobar.com", name="test")
+ site.save()
+ with self.settings(SITE_ID=site.id):
+ factories.CombinedConsortiumDataWorkspaceFactory.create()
+ ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ self.assertIn(
+ "Running CombinedConsortiumDataWorkspace auth domain audit... problems found.", out.getvalue()
+ )
+ self.assertIn("https://foobar.com", out.getvalue())
+
+ def test_auth_domain_audit_one_instance_needs_action_link_in_output(self):
+ factories.CombinedConsortiumDataWorkspaceFactory.create()
+ ManagedGroupFactory.create(name="GREGOR_ALL")
+ out = StringIO()
+ call_command("run_combined_workspace_audit", "--no-color", stdout=out)
+ url = reverse("gregor_anvil:audit:combined_workspaces:auth_domains:all")
+ self.assertIn(url, out.getvalue())
+ # Zero messages have been sent by default.
+ self.assertEqual(len(mail.outbox), 0)
From ca9a0fbbca8205d8bad03112c4a21dee0cabf630 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Wed, 16 Oct 2024 14:57:20 -0700
Subject: [PATCH 76/97] Clean up test coverage
---
.../audit/combined_workspace_audit.py | 38 +------------------
1 file changed, 1 insertion(+), 37 deletions(-)
diff --git a/gregor_django/gregor_anvil/audit/combined_workspace_audit.py b/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
index 0885a4c6..05a8e7fc 100644
--- a/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
+++ b/gregor_django/gregor_anvil/audit/combined_workspace_audit.py
@@ -18,7 +18,7 @@ class CombinedConsortiumDataWorkspaceAuthDomainAuditTable(tables.Table):
note = tables.Column()
action = tables.TemplateColumn(
# Temporarily use this button template, until we have the resolve view working for this workspace type.
- template_name="gregor_anvil/snippets/upload_workspace_auth_domain_audit_action_button.html"
+ template_name="gregor_anvil/snippets/combinedconsortiumdataworkspace_auth_domain_audit_action_button.html"
)
class Meta:
@@ -101,42 +101,6 @@ def _audit_workspace_and_dcc_admin_group(self, combined_workspace, managed_group
else:
self.needs_action.append(workspace_auth_domain_audit_results.AddAdmin(**audit_result_args))
- def _audit_workspace_and_dcc_group(self, combined_workspace, managed_group):
- """Audit the auth domain membership for a specific workspace and the DCC writers/members groups.
-
- Expectations:
- - Member before the workspace is completed.
- - Not a member after the workspace is completed.
- """
- current_membership = self._get_current_membership(combined_workspace, managed_group)
- audit_result_args = {
- "workspace": combined_workspace.workspace,
- "managed_group": managed_group,
- "current_membership_instance": current_membership,
- }
-
- if not combined_workspace.date_completed:
- note = self.DCC_BEFORE_COMPLETE
- if not current_membership:
- self.needs_action.append(workspace_auth_domain_audit_results.AddMember(note=note, **audit_result_args))
- elif current_membership and current_membership.role == GroupGroupMembership.MEMBER:
- self.verified.append(workspace_auth_domain_audit_results.VerifiedMember(note=note, **audit_result_args))
- else:
- self.errors.append(workspace_auth_domain_audit_results.ChangeToMember(note=note, **audit_result_args))
- else:
- note = self.DCC_AFTER_COMPLETE
- if not current_membership:
- self.verified.append(
- workspace_auth_domain_audit_results.VerifiedNotMember(note=note, **audit_result_args)
- )
- elif current_membership and current_membership.role == GroupGroupMembership.MEMBER:
- self.needs_action.append(workspace_auth_domain_audit_results.Remove(note=note, **audit_result_args))
- else:
- self.errors.append(workspace_auth_domain_audit_results.Remove(note=note, **audit_result_args))
-
- def _audit_workspace_and_dcc_member_group(self, combined_workspace, managed_group):
- pass
-
def _audit_workspace_and_gregor_all_group(self, combined_workspace, managed_group):
"""Audit the auth domain membership for a specific workspace and the GREGOR_ALL group.
From 2841a97aad96a4cdb689a1bfb29b4aca3f143a88 Mon Sep 17 00:00:00 2001
From: Jonas Carson
Date: Thu, 17 Oct 2024 11:04:23 -0700
Subject: [PATCH 77/97] Add some missing coverage
---
gregor_django/users/tests/test_views.py | 56 ++++++++++++++++++++++++-
gregor_django/users/views.py | 1 -
2 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/gregor_django/users/tests/test_views.py b/gregor_django/users/tests/test_views.py
index e0d8431a..e1611657 100644
--- a/gregor_django/users/tests/test_views.py
+++ b/gregor_django/users/tests/test_views.py
@@ -1,19 +1,26 @@
import json
import pytest
+from allauth.socialaccount.models import SocialApp
from anvil_consortium_manager.models import AnVILProjectManagerAccess
-from anvil_consortium_manager.tests.factories import AccountFactory
+from anvil_consortium_manager.tests.factories import AccountFactory, UserEmailEntryFactory
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, Permission
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
+from django.contrib.sites.models import Site
from django.http import HttpRequest
from django.shortcuts import resolve_url
from django.test import RequestFactory, TestCase
from django.urls import reverse
+from gregor_django.drupal_oauth_provider.provider import CustomProvider
+from gregor_django.gregor_anvil.tests.factories import (
+ PartnerGroupFactory,
+ ResearchCenterFactory,
+)
from gregor_django.users.forms import UserChangeForm, UserLookupForm
from gregor_django.users.tests.factories import UserFactory
from gregor_django.users.views import UserRedirectView, UserUpdateView, user_detail_view
@@ -92,13 +99,42 @@ def test_get_redirect_url(self, user: User, rf: RequestFactory):
class TestUserDetailView:
- def test_authenticated(self, client, user: User, rf: RequestFactory):
+ def test_authenticated(self, client, user: User):
client.force_login(user)
user_detail_url = reverse("users:detail", kwargs=dict(username=user.username))
response = client.get(user_detail_url)
assert response.status_code == 200
+ def test_configured_user_with_anvil_account(self, client):
+ account = AccountFactory.create(verified=True)
+ account_user = account.user
+ client.force_login(account_user)
+ pg1 = PartnerGroupFactory(short_name="pg1")
+ rc1 = ResearchCenterFactory(short_name="rc1")
+
+ account.save()
+ account_user.partner_groups.add(pg1)
+ account_user.research_centers.add(rc1)
+ user_detail_url = reverse("users:detail", kwargs=dict(username=account_user.username))
+ response = client.get(user_detail_url)
+
+ assert response.status_code == 200
+
+ def test_configured_users_no_anvil_account(self, client, user: User):
+ client.force_login(user)
+ pg1 = PartnerGroupFactory(short_name="pg1")
+ rc1 = ResearchCenterFactory(short_name="rc1")
+ UserEmailEntryFactory.create(user=user)
+
+ user.partner_groups.add(pg1)
+ user.research_centers.add(rc1)
+ user_detail_url = reverse("users:detail", kwargs=dict(username=user.username))
+
+ response = client.get(user_detail_url)
+
+ assert response.status_code == 200
+
def test_not_authenticated(self, user: User, rf: RequestFactory):
request = rf.get("/fake-url/")
request.user = AnonymousUser()
@@ -129,6 +165,22 @@ def test_unlinked_accounts(self):
self.assertContains(response, "Previously-linked accounts")
+class LoginViewTest(TestCase):
+ def setUp(self):
+ current_site = Site.objects.get_current()
+ self.social_app = SocialApp.objects.create(
+ provider=CustomProvider.id,
+ name="DOA",
+ client_id="test-client-id",
+ secret="test-client-secret",
+ )
+ self.social_app.sites.add(current_site)
+
+ def test_basic_login_view_render(self):
+ response = self.client.get(reverse("account_login"))
+ assert response.status_code == 200
+
+
class UserAutocompleteViewTest(TestCase):
def setUp(self):
"""Set up test class."""
diff --git a/gregor_django/users/views.py b/gregor_django/users/views.py
index e27834ab..ef0dc927 100644
--- a/gregor_django/users/views.py
+++ b/gregor_django/users/views.py
@@ -20,7 +20,6 @@ class UserDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["unlinked_accounts"] = self.object.unlinked_accounts.all()
- print(context["unlinked_accounts"])
context["user_email_entries"] = self.object.useremailentry_set.filter(date_verified=None)
context["is_me"] = self.request.user == self.object
return context
From 5e0ef303b197674f8e22b30f65ed8a166f02afa6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 17 Oct 2024 19:43:35 +0000
Subject: [PATCH 78/97] Bump ruff from 0.6.9 to 0.7.0
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.9 to 0.7.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.9...0.7.0)
---
updated-dependencies:
- dependency-name: ruff
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index eada11bd..0c856314 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -150,7 +150,7 @@ requests==2.32.3
# -c requirements/requirements.txt
# -c requirements/test-requirements.txt
# sphinx
-ruff==0.6.9
+ruff==0.7.0
# via -r requirements/dev-requirements.in
six==1.16.0
# via
From 63a1030b0864161820339246cde795517ca8e816 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 18 Oct 2024 19:50:13 +0000
Subject: [PATCH 79/97] Bump pyjwt from 2.8.0 to 2.9.0
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.8.0 to 2.9.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.8.0...2.9.0)
---
updated-dependencies:
- dependency-name: pyjwt
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index d0e752c3..710fda88 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -122,7 +122,7 @@ pyasn1-modules==0.3.0
# via google-auth
pycparser==2.21
# via cffi
-pyjwt==2.8.0
+pyjwt==2.9.0
# via -r requirements/requirements.in
pyproject-hooks==1.0.0
# via
From 257e946f041bab9da0f8c5b9cc5a6135bb012529 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 18 Oct 2024 19:51:53 +0000
Subject: [PATCH 80/97] Bump cryptography from 43.0.1 to 43.0.3
Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.1 to 43.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/43.0.1...43.0.3)
---
updated-dependencies:
- dependency-name: cryptography
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index d0e752c3..2e7ea700 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -31,7 +31,7 @@ crispy-bootstrap5==2024.10
# via
# -r requirements/requirements.in
# django-anvil-consortium-manager
-cryptography==43.0.1
+cryptography==43.0.3
# via -r requirements/requirements.in
django==4.2.16
# via
From da15adb7fee165913ccfd5db48ab880f23b5c493 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 21 Oct 2024 16:25:52 +0000
Subject: [PATCH 81/97] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.9 → v0.7.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.7.0)
- [github.com/gitleaks/gitleaks: v8.20.0 → v8.21.1](https://github.com/gitleaks/gitleaks/compare/v8.20.0...v8.21.1)
---
.pre-commit-config.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 808e8b7b..34714409 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -12,7 +12,7 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
- rev: v0.6.9
+ rev: v0.7.0
hooks:
# Run the linter.
- id: ruff
@@ -21,7 +21,7 @@ repos:
- id: ruff-format
- repo: https://github.com/gitleaks/gitleaks
- rev: v8.20.0
+ rev: v8.21.1
hooks:
- id: gitleaks
From 41a09d854b4ce82db5911a2ed2aaece0b04abef1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 21 Oct 2024 19:04:54 +0000
Subject: [PATCH 82/97] Bump mypy from 1.12.0 to 1.12.1
Bumps [mypy](https://github.com/python/mypy) from 1.12.0 to 1.12.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.12.0...v1.12.1)
---
updated-dependencies:
- dependency-name: mypy
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index eada11bd..f02685ac 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -100,7 +100,7 @@ matplotlib-inline==0.1.6
# via ipython
mccabe==0.7.0
# via pylint
-mypy==1.12.0
+mypy==1.12.1
# via -r requirements/dev-requirements.in
mypy-extensions==1.0.0
# via mypy
From 9bec7d1d4246a1d3e569bd814b3e71aee1cd8b62 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 21 Oct 2024 19:06:19 +0000
Subject: [PATCH 83/97] Bump mysqlclient from 2.2.4 to 2.2.5
Bumps [mysqlclient](https://github.com/PyMySQL/mysqlclient) from 2.2.4 to 2.2.5.
- [Release notes](https://github.com/PyMySQL/mysqlclient/releases)
- [Changelog](https://github.com/PyMySQL/mysqlclient/blob/main/HISTORY.rst)
- [Commits](https://github.com/PyMySQL/mysqlclient/compare/v2.2.4...v2.2.5)
---
updated-dependencies:
- dependency-name: mysqlclient
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index d0e752c3..15d51bd6 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -96,7 +96,7 @@ idna==3.7
# via requests
jsonapi-requests==0.8.0
# via -r requirements/requirements.in
-mysqlclient==2.2.4
+mysqlclient==2.2.5
# via -r requirements/requirements.in
networkx==3.1
# via django-anvil-consortium-manager
From 13fa73b66a6e0fa5a02e150dcc220ce330e85320 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Mon, 21 Oct 2024 15:38:57 -0700
Subject: [PATCH 84/97] Bump gitleaks version
Handled in main branch, but I want CI to run in this branch.
---
.github/workflows/gitleaks.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml
index f6cca493..00a67cc8 100644
--- a/.github/workflows/gitleaks.yml
+++ b/.github/workflows/gitleaks.yml
@@ -10,10 +10,10 @@ jobs:
name: gitleaks
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4.1.7
+ - uses: actions/checkout@v4.2.1
with:
fetch-depth: 0
- - uses: gitleaks/gitleaks-action@v2.3.6
+ - uses: gitleaks/gitleaks-action@v2.3.7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE}} # Only required for Organizations, not personal accounts.
From 5422e5f20bd0c7e71820e518b7279511f0d8cad3 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Mon, 21 Oct 2024 15:51:34 -0700
Subject: [PATCH 85/97] Add nightly combined workspace audit to cron file
---
gregor_apps.cron | 3 +++
1 file changed, 3 insertions(+)
diff --git a/gregor_apps.cron b/gregor_apps.cron
index d857f24c..2be3d68f 100644
--- a/gregor_apps.cron
+++ b/gregor_apps.cron
@@ -13,3 +13,6 @@ MAILTO="gregorweb@uw.edu"
# Nightly upload workspace audit
0 3 * * * . /var/www/django/gregor_apps/gregor-apps-activate.sh; python manage.py run_upload_workspace_audit --email gregorweb@uw.edu >> cron.log
+
+# Nightly combined workspace audit
+0 3 * * * . /var/www/django/gregor_apps/gregor-apps-activate.sh; python manage.py run_combined_workspace_audit --email gregorweb@uw.edu >> cron.log
From 475245d08f8bfded89ebe5f08e65ac10d98e1054 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 22 Oct 2024 10:45:05 -0700
Subject: [PATCH 86/97] Clean up audit templates for consistency
---
...nedconsortiumdataworkspace_auth_domain_audit_explanation.html | 1 +
...ombinedconsortiumdataworkspace_sharing_audit_explanation.html | 1 +
.../snippets/upload_workspace_sharing_audit_explanation.html | 1 +
3 files changed, 3 insertions(+)
diff --git a/gregor_django/templates/gregor_anvil/snippets/combinedconsortiumdataworkspace_auth_domain_audit_explanation.html b/gregor_django/templates/gregor_anvil/snippets/combinedconsortiumdataworkspace_auth_domain_audit_explanation.html
index f2785a71..32c7632d 100644
--- a/gregor_django/templates/gregor_anvil/snippets/combinedconsortiumdataworkspace_auth_domain_audit_explanation.html
+++ b/gregor_django/templates/gregor_anvil/snippets/combinedconsortiumdataworkspace_auth_domain_audit_explanation.html
@@ -17,6 +17,7 @@ Any errors should be reported!
diff --git a/gregor_django/templates/gregor_anvil/snippets/combinedconsortiumdataworkspace_sharing_audit_explanation.html b/gregor_django/templates/gregor_anvil/snippets/combinedconsortiumdataworkspace_sharing_audit_explanation.html
index 3cfa15e2..5c1b9f0e 100644
--- a/gregor_django/templates/gregor_anvil/snippets/combinedconsortiumdataworkspace_sharing_audit_explanation.html
+++ b/gregor_django/templates/gregor_anvil/snippets/combinedconsortiumdataworkspace_sharing_audit_explanation.html
@@ -37,6 +37,7 @@ Errors
- The workspace has been shared with an unexpected group.
+ - A group other than the DCC admins group is an owner of the workspace.
diff --git a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_sharing_audit_explanation.html b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_sharing_audit_explanation.html
index cdf2a379..f86cafc6 100644
--- a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_sharing_audit_explanation.html
+++ b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_sharing_audit_explanation.html
@@ -37,6 +37,7 @@ Errors
- The workspace has been shared with an unexpected group.
+ - A group other than the DCC admins group is an owner of the workspace.
From 05c632faf89a0a2d8755d78fbd0ac08ac9e58531 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 23 Oct 2024 19:39:43 +0000
Subject: [PATCH 87/97] Bump mypy from 1.12.1 to 1.13.0
Bumps [mypy](https://github.com/python/mypy) from 1.12.1 to 1.13.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.12.1...v1.13.0)
---
updated-dependencies:
- dependency-name: mypy
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 9096c9c6..4446b498 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -100,7 +100,7 @@ matplotlib-inline==0.1.6
# via ipython
mccabe==0.7.0
# via pylint
-mypy==1.12.1
+mypy==1.13.0
# via -r requirements/dev-requirements.in
mypy-extensions==1.0.0
# via mypy
From a2a785bc802a6b5e17563f1648d12a8bd8171d4d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 23 Oct 2024 19:46:07 +0000
Subject: [PATCH 88/97] Bump actions/checkout from 4.2.1 to 4.2.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.2.1...v4.2.2)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 4 ++--
.github/workflows/gitleaks.yml | 2 +-
.github/workflows/pip-compile.yml | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 26e7b012..70379dd4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,7 +54,7 @@ jobs:
steps:
- name: Checkout Code Repository
- uses: actions/checkout@v4.2.1
+ uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5.2.0
@@ -111,7 +111,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
- uses: actions/checkout@v4.2.1
+ uses: actions/checkout@v4.2.2
- name: Set up Python
uses: actions/setup-python@v5.2.0
diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml
index 00a67cc8..5c7d8e73 100644
--- a/.github/workflows/gitleaks.yml
+++ b/.github/workflows/gitleaks.yml
@@ -10,7 +10,7 @@ jobs:
name: gitleaks
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4.2.1
+ - uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2.3.7
diff --git a/.github/workflows/pip-compile.yml b/.github/workflows/pip-compile.yml
index 9f906bd1..c64f510e 100644
--- a/.github/workflows/pip-compile.yml
+++ b/.github/workflows/pip-compile.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout Code Repository
- uses: actions/checkout@v4.2.1
+ uses: actions/checkout@v4.2.2
with:
ref: ${{ github.head_ref }}
From 7e9e68b6f7a3f6edd84574ef103ef0965d045397 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 24 Oct 2024 19:35:38 +0000
Subject: [PATCH 89/97] Bump django-allauth from 65.0.2 to 65.1.0
Bumps [django-allauth](https://github.com/sponsors/pennersr) from 65.0.2 to 65.1.0.
- [Commits](https://github.com/sponsors/pennersr/commits)
---
updated-dependencies:
- dependency-name: django-allauth
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index a5ca25ce..cdf7ef7c 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -12,6 +12,7 @@ asgiref==3.8.1
# via
# -r requirements/requirements.in
# django
+ # django-allauth
# django-htmx
build==1.0.3
# via pip-tools
@@ -49,7 +50,7 @@ django==4.2.16
# django-picklefield
# django-simple-history
# django-tables2
-django-allauth==65.0.2
+django-allauth==65.1.0
# via -r requirements/requirements.in
django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-consortium-manager.git@v0.25
# via -r requirements/requirements.in
From 6e50dbec870caa9618c647e45d9f75f1e1916187 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 24 Oct 2024 19:55:42 +0000
Subject: [PATCH 90/97] Bump actions/setup-python from 5.2.0 to 5.3.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.2.0...v5.3.0)
---
updated-dependencies:
- dependency-name: actions/setup-python
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 4 ++--
.github/workflows/pip-compile.yml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 26e7b012..af91a982 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -57,7 +57,7 @@ jobs:
uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5.2.0
+ uses: actions/setup-python@v5.3.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
@@ -114,7 +114,7 @@ jobs:
uses: actions/checkout@v4.2.1
- name: Set up Python
- uses: actions/setup-python@v5.2.0
+ uses: actions/setup-python@v5.3.0
with:
python-version: '3.10'
diff --git a/.github/workflows/pip-compile.yml b/.github/workflows/pip-compile.yml
index 9f906bd1..1b04610d 100644
--- a/.github/workflows/pip-compile.yml
+++ b/.github/workflows/pip-compile.yml
@@ -18,7 +18,7 @@ jobs:
ref: ${{ github.head_ref }}
- name: Set up Python
- uses: actions/setup-python@v5.2.0
+ uses: actions/setup-python@v5.3.0
with:
python-version: "3.10"
From 59c6b82ade4470c773d9f56e8f3a3a82c909b975 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 7 Nov 2024 13:37:33 -0800
Subject: [PATCH 91/97] Use new version of update-requirements-files action
This fixes an issue with pip-tools and pip versions, where pip-copmile
was adding absolute paths when run with a pip version of >=24.3.
---
.github/workflows/pip-compile.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/pip-compile.yml b/.github/workflows/pip-compile.yml
index 9f906bd1..72bd62a0 100644
--- a/.github/workflows/pip-compile.yml
+++ b/.github/workflows/pip-compile.yml
@@ -23,7 +23,7 @@ jobs:
python-version: "3.10"
- name: Update requirements files
- uses: UW-GAC/pip-tools-actions/update-requirements-files@v0.1
+ uses: UW-GAC/pip-tools-actions/update-requirements-files@v0.2
with:
requirements_files: |-
requirements/requirements.in
From 7834d986d0d62f7a73570899e18c0bcf02bc76db Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 Nov 2024 22:04:00 +0000
Subject: [PATCH 92/97] Bump pytest-cov from 5.0.0 to 6.0.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 5.0.0 to 6.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...v6.0.0)
---
updated-dependencies:
- dependency-name: pytest-cov
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
requirements/test-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-requirements.txt b/requirements/test-requirements.txt
index 795d352d..2f6f4a10 100644
--- a/requirements/test-requirements.txt
+++ b/requirements/test-requirements.txt
@@ -56,7 +56,7 @@ pytest==8.3.3
# pytest-django
# pytest-sugar
# pytest-xdist
-pytest-cov==5.0.0
+pytest-cov==6.0.0
# via -r requirements/test-requirements.in
pytest-django==4.9.0
# via -r requirements/test-requirements.in
From 7092be91950d1f5d09f8139d6904fb9551537a1f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 Nov 2024 22:04:27 +0000
Subject: [PATCH 93/97] Bump ruff from 0.7.0 to 0.7.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.0 to 0.7.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.7.0...0.7.2)
---
updated-dependencies:
- dependency-name: ruff
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 9096c9c6..eb7ff9b6 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -150,7 +150,7 @@ requests==2.32.3
# -c requirements/requirements.txt
# -c requirements/test-requirements.txt
# sphinx
-ruff==0.7.0
+ruff==0.7.2
# via -r requirements/dev-requirements.in
six==1.16.0
# via
From 5a7bf8f4e0dea4538ceb1017917a07f4fd716aa4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 Nov 2024 22:04:34 +0000
Subject: [PATCH 94/97] Bump django-htmx from 1.19.0 to 1.21.0
Bumps [django-htmx](https://github.com/adamchainz/django-htmx) from 1.19.0 to 1.21.0.
- [Changelog](https://github.com/adamchainz/django-htmx/blob/main/docs/changelog.rst)
- [Commits](https://github.com/adamchainz/django-htmx/compare/1.19.0...1.21.0)
---
updated-dependencies:
- dependency-name: django-htmx
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index a5ca25ce..10b517ce 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -72,7 +72,7 @@ django-extensions==3.2.1
# django-anvil-consortium-manager
django-filter==23.5
# via django-anvil-consortium-manager
-django-htmx==1.19.0
+django-htmx==1.21.0
# via -r requirements/requirements.in
django-login-required-middleware==0.9.0
# via -r requirements/requirements.in
From 8a054636bd35848fbae173217b8c5ae55ffe8cef Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 Nov 2024 22:04:37 +0000
Subject: [PATCH 95/97] Bump werkzeug from 3.0.4 to 3.1.2
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.4...3.1.2)
---
updated-dependencies:
- dependency-name: werkzeug
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 9096c9c6..6bc075bf 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -233,7 +233,7 @@ wcwidth==0.2.13
# via prompt-toolkit
websockets==12.0
# via sphinx-autobuild
-werkzeug==3.0.4
+werkzeug==3.1.2
# via -r requirements/dev-requirements.in
# The following packages are considered to be unsafe in a requirements file:
From 09af3b9428c466cfe36f1a4989d9a483a458a1f6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 Nov 2024 22:04:40 +0000
Subject: [PATCH 96/97] Bump whitenoise from 6.7.0 to 6.8.2
Bumps [whitenoise](https://github.com/evansd/whitenoise) from 6.7.0 to 6.8.2.
- [Changelog](https://github.com/evansd/whitenoise/blob/main/docs/changelog.rst)
- [Commits](https://github.com/evansd/whitenoise/compare/6.7.0...6.8.2)
---
updated-dependencies:
- dependency-name: whitenoise
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index a5ca25ce..bfc2d19b 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -164,7 +164,7 @@ urllib3==2.2.2
# via requests
wheel==0.42.0
# via pip-tools
-whitenoise==6.7.0
+whitenoise==6.8.2
# via -r requirements/requirements.in
# The following packages are considered to be unsafe in a requirements file:
From 85a0f0dc2d0d21e1cfbf921b124a9808d7ca23ce Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 Nov 2024 22:04:45 +0000
Subject: [PATCH 97/97] Bump django-constance from 4.1.2 to 4.1.3
Bumps [django-constance](https://github.com/jazzband/django-constance) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/jazzband/django-constance/releases)
- [Changelog](https://github.com/jazzband/django-constance/blob/master/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-constance/compare/4.1.2...4.1.3)
---
updated-dependencies:
- dependency-name: django-constance
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index a5ca25ce..a899dde9 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -55,7 +55,7 @@ django-anvil-consortium-manager @ git+https://github.com/UW-GAC/django-anvil-con
# via -r requirements/requirements.in
django-autocomplete-light==3.11.0
# via django-anvil-consortium-manager
-django-constance==4.1.2
+django-constance==4.1.3
# via -r requirements/requirements.in
django-crispy-forms==2.3
# via