Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to ignore WorkspaceSharing audit results #559

Merged
merged 19 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
35c85c6
Bump dev version
amstilp Jan 9, 2025
49f5682
Add a model to track ignored WorkspaceGroupSharing audit results
amstilp Jan 9, 2025
f29dfd8
Rename model to match audit class name (IgnoredWorkspaceSharing)
amstilp Jan 9, 2025
fd3e984
Rework workspace group sharing urls to match group membership urls
amstilp Jan 9, 2025
00fe6d6
Add a detail view for IgnoredWorkspaceSharing objects
amstilp Jan 9, 2025
8789388
Register IgnoredWorkspaceSharing mode in the admin
amstilp Jan 9, 2025
5cd0b7d
Add view to create a new IgnoredWorkspaceSharing record
amstilp Jan 9, 2025
9d2c51b
Add view to update an IgnoredWorkspaceSharing record
amstilp Jan 9, 2025
a91dda4
Add view to delete an IgnoredWorkspaceSharing record
amstilp Jan 9, 2025
2c10ffe
Add a view to list IgnoredWorkspaceGroup objects
amstilp Jan 9, 2025
6ce6890
Add specific results and table classes for workspace sharing NotInApp
amstilp Jan 9, 2025
5f0f367
Update WorkspaceSharingAudit to ignore ignored records
amstilp Jan 10, 2025
dade4f6
Add links to update and delete views
amstilp Jan 10, 2025
164c654
Add IgnoreWorkspaceSharing reporting to run_anvil_audit command
amstilp Jan 10, 2025
26fcb2c
Order IgnoredWorkspaceSharing records by ignored_email in audit
amstilp Jan 10, 2025
daac42e
Separate ordering testing from existence testing
amstilp Jan 10, 2025
11f07ae
Add an IgnoredResult class specific for ManagedGroupMembership
amstilp Jan 10, 2025
31eecaa
Exclude the "record" field from custom Ignore tables
amstilp Jan 10, 2025
20402e3
Hopefully fix coverage when checking User.get_absolute_url
amstilp Jan 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion anvil_consortium_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.28.0"
__version__ = "0.29.0.dev0"
18 changes: 18 additions & 0 deletions anvil_consortium_manager/auditor/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,25 @@ class IgnoredManagedGroupMembershipAdmin(SimpleHistoryAdmin):
"ignored_email",
"added_by",
)
list_filter = ("added_by",)
search_fields = (
"group",
"ignored_email",
)


@admin.register(models.IgnoredWorkspaceSharing)
class IgnoredWorkspaceSharingAdmin(SimpleHistoryAdmin):
"""Admin class for the IgnoredWorkspaceSharing model."""

list_display = (
"pk",
"workspace",
"ignored_email",
"added_by",
)
list_filter = ("added_by",)
search_fields = (
"workspace",
"ignored_email",
)
27 changes: 22 additions & 5 deletions anvil_consortium_manager/auditor/audit/managed_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,32 @@ class Meta:
exclude = ("record",)


class ManagedGroupMembershipIgnoredResult(base.IgnoredResult):
"""Class to store a not in app audit result for a specific ManagedGroupMembership record."""

def __init__(self, *args, current_role=None, **kwargs):
super().__init__(*args, **kwargs)
self.current_role = current_role


class ManagedGroupMembershipIgnoredTable(base.IgnoredTable):
"""A table specific to the IgnoredManagedGroupMembership model."""

model_instance = tables.columns.Column(linkify=True, verbose_name="Details")
model_instance__group = tables.columns.Column(linkify=True, verbose_name="Managed group", orderable=False)
model_instance__ignored_email = tables.columns.Column(orderable=False, verbose_name="Ignored email")
model_instance__added_by = tables.columns.Column(orderable=False, verbose_name="Ignored by")
current_role = tables.columns.Column(verbose_name="Current role")

class Meta:
fields = (
"model_instance",
"model_instance__group",
"model_instance__ignored_email",
"model_instance__added_by",
"record",
"current_role",
)
exclude = ("record",)


class ManagedGroupMembershipAudit(base.AnVILAudit):
Expand Down Expand Up @@ -219,19 +229,26 @@ def run_audit(self):
self.add_result(model_instance_result)

# Add any admin that the app doesn't know about.
for obj in models.IgnoredManagedGroupMembership.objects.filter(group=self.managed_group):
ignored_qs = models.IgnoredManagedGroupMembership.objects.filter(group=self.managed_group)
for obj in ignored_qs.order_by("ignored_email"):
try:
admins_in_anvil.remove(obj.ignored_email)
record = "{}: {}".format(GroupAccountMembership.ADMIN, obj.ignored_email)
self.add_result(base.IgnoredResult(obj, record=record))
self.add_result(
ManagedGroupMembershipIgnoredResult(obj, record=record, current_role=GroupAccountMembership.ADMIN)
)
except ValueError:
try:
members_in_anvil.remove(obj.ignored_email)
record = "{}: {}".format(GroupAccountMembership.MEMBER, obj.ignored_email)
self.add_result(base.IgnoredResult(obj, record=record))
self.add_result(
ManagedGroupMembershipIgnoredResult(
obj, record=record, current_role=GroupAccountMembership.MEMBER
)
)
except ValueError:
# This email is not in the list of members or admins.
self.add_result(base.IgnoredResult(obj, record=None))
self.add_result(ManagedGroupMembershipIgnoredResult(obj, record=None))

for member in admins_in_anvil:
record = "{}: {}".format(GroupAccountMembership.ADMIN, member)
Expand Down
125 changes: 117 additions & 8 deletions anvil_consortium_manager/auditor/audit/workspaces.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import django_tables2 as tables

from anvil_consortium_manager.anvil_api import AnVILAPIClient
from anvil_consortium_manager.models import Workspace

from .base import AnVILAudit, ModelInstanceResult, NotInAppResult
from .. import models
from . import base


class WorkspaceAudit(AnVILAudit):
class WorkspaceAudit(base.AnVILAudit):
"""Class to runs an audit for Workspace instances."""

ERROR_NOT_IN_ANVIL = "Not in AnVIL"
Expand Down Expand Up @@ -38,7 +41,7 @@ def run_audit(self):
response = AnVILAPIClient().list_workspaces(fields=",".join(fields))
workspaces_on_anvil = response.json()
for workspace in Workspace.objects.all():
model_instance_result = ModelInstanceResult(workspace)
model_instance_result = base.ModelInstanceResult(workspace)
try:
i = next(
idx
Expand Down Expand Up @@ -89,10 +92,79 @@ def run_audit(self):
if x["accessLevel"] == "OWNER"
]
for workspace_name in not_in_app:
self.add_result(NotInAppResult(workspace_name))
self.add_result(base.NotInAppResult(workspace_name))


class WorkspaceSharingNotInAppResult(base.NotInAppResult):
"""Class to store a not in app audit result for a specific WorkspaceSharing record."""

def __init__(self, *args, workspace=None, email=None, access=None, can_compute=None, can_share=None, **kwargs):
super().__init__(*args, **kwargs)
self.workspace = workspace
self.email = email
self.access = access
self.can_compute = can_compute
self.can_share = can_share


class WorkspaceSharingNotInAppTable(base.NotInAppTable):
workspace = tables.Column()
email = tables.Column()
access = tables.Column()
can_compute = tables.Column()
can_share = tables.Column()
ignore = tables.TemplateColumn(
template_name="auditor/snippets/audit_workspacegroupsharing_notinapp_ignore_button.html",
orderable=False,
verbose_name="Ignore?",
)

class Meta:
fields = (
"workspace",
"email",
"access",
"can_compute",
"can_share",
)
exclude = ("record",)


class WorkspaceSharingIgnoredResult(base.IgnoredResult):
"""Class to store a not in app audit result for a specific WorkspaceSharing record."""

def __init__(self, *args, current_access=None, current_can_compute=None, current_can_share=None, **kwargs):
super().__init__(*args, **kwargs)
self.current_access = current_access
self.current_can_compute = current_can_compute
self.current_can_share = current_can_share


class WorkspaceSharingAudit(AnVILAudit):
class WorkspaceSharingIgnoredTable(base.IgnoredTable):
"""A table specific to the IgnoredWorkspaceSharing model."""

model_instance = tables.columns.Column(linkify=True, verbose_name="Details")
model_instance__workspace = tables.columns.Column(linkify=True, verbose_name="Workspace", orderable=False)
model_instance__ignored_email = tables.columns.Column(orderable=False, verbose_name="Ignored email")
model_instance__added_by = tables.columns.Column(orderable=False, verbose_name="Ignored by")
current_access = tables.columns.Column(orderable=False, verbose_name="Current access")
current_can_compute = tables.columns.Column(orderable=False, verbose_name="Current can compute")
current_can_share = tables.columns.Column(orderable=False, verbose_name="Current can share")

class Meta:
fields = (
"model_instance",
"model_instance__workspace",
"model_instance__ignored_email",
"model_instance__added_by",
"current_access",
"current_can_compute",
"current_can_share",
)
exclude = ("record",)


class WorkspaceSharingAudit(base.AnVILAudit):
"""Class that runs an audit for sharing of a specific Workspace instance."""

ERROR_NOT_SHARED_IN_ANVIL = "Not shared in AnVIL"
Expand All @@ -107,6 +179,9 @@ class WorkspaceSharingAudit(AnVILAudit):
ERROR_DIFFERENT_CAN_COMPUTE = "can_compute value does not match in AnVIL"
"""Error when the can_compute value for a ManagedGroup does not match what's on AnVIL."""

not_in_app_table_class = WorkspaceSharingNotInAppTable
ignored_table_class = WorkspaceSharingIgnoredTable

def __init__(self, workspace, *args, **kwargs):
super().__init__(*args, **kwargs)
self.workspace = workspace
Expand All @@ -124,7 +199,7 @@ def run_audit(self):
pass
for access in self.workspace.workspacegroupsharing_set.all():
# Create an audit result instance for this model.
model_instance_result = ModelInstanceResult(access)
model_instance_result = base.ModelInstanceResult(access)
try:
access_details = acl_in_anvil.pop(access.group.email.lower())
except KeyError:
Expand All @@ -144,6 +219,40 @@ def run_audit(self):
# Save the results for this model instance.
self.add_result(model_instance_result)

# Add any access that the app doesn't know about.
# Handle ignored records.
for obj in models.IgnoredWorkspaceSharing.objects.filter(workspace=self.workspace).order_by("ignored_email"):
try:
acl = acl_in_anvil.pop(obj.ignored_email)
record = "{}: {}".format(acl["accessLevel"], obj.ignored_email)
self.add_result(
WorkspaceSharingIgnoredResult(
obj,
record=record,
current_access=acl["accessLevel"],
current_can_compute=acl["canCompute"],
current_can_share=acl["canShare"],
)
)
except KeyError:
self.add_result(
WorkspaceSharingIgnoredResult(
obj,
record=None,
current_access=None,
current_can_compute=None,
current_can_share=None,
)
)

# Add any remaining records as "not in app".
for key in acl_in_anvil:
self.add_result(NotInAppResult("{}: {}".format(acl_in_anvil[key]["accessLevel"], key)))
self.add_result(
WorkspaceSharingNotInAppResult(
"{}: {}".format(acl_in_anvil[key]["accessLevel"], key),
workspace=self.workspace,
email=key,
access=acl_in_anvil[key]["accessLevel"],
can_compute=acl_in_anvil[key]["canCompute"],
can_share=acl_in_anvil[key]["canShare"],
)
)
7 changes: 7 additions & 0 deletions anvil_consortium_manager/auditor/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ class Meta:
model = models.IgnoredManagedGroupMembership
fields = {"group__name": ["icontains"], "ignored_email": ["icontains"]}
form = FilterForm


class IgnoredWorkspaceSharingFilter(FilterSet):
class Meta:
model = models.IgnoredWorkspaceSharing
fields = {"workspace__name": ["icontains"], "ignored_email": ["icontains"]}
form = FilterForm
18 changes: 18 additions & 0 deletions anvil_consortium_manager/auditor/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,21 @@ class Meta:
"ignored_email",
"note",
)


class IgnoredWorkspaceSharingForm(Bootstrap5MediaFormMixin, forms.ModelForm):
"""Form for the IgnoredWorkspaceSharing model."""

class Meta:
model = models.IgnoredWorkspaceSharing
fields = (
"workspace",
"ignored_email",
"note",
)
widgets = {
"workspace": autocomplete.ModelSelect2(
url="anvil_consortium_manager:workspaces:autocomplete",
attrs={"data-theme": "bootstrap-5"},
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,4 @@ def handle(self, *args, **options):
)

if "Workspace" in models_to_audit:
self._run_audit(workspace_audit.WorkspaceAudit(), **options)
self._run_audit(workspace_audit.WorkspaceAudit(), ignore_model=models.IgnoredWorkspaceSharing, **options)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 5.0 on 2025-01-09 00:32

import django.db.models.deletion
import django_extensions.db.fields
import simple_history.models
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('anvil_consortium_manager', '0019_accountuserarchive'),
('auditor', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='HistoricalIgnoredWorkspaceSharing',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('note', models.TextField(help_text='Note about why this email is being ignored.')),
('ignored_email', models.EmailField(help_text='Email address to ignore.', max_length=254)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('added_by', models.ForeignKey(blank=True, db_constraint=False, help_text='User who added the record to this table.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(blank=True, db_constraint=False, help_text='Workspace where email should be ignored.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='anvil_consortium_manager.workspace')),
],
options={
'verbose_name': 'historical ignored workspace sharing',
'verbose_name_plural': 'historical ignored workspace sharings',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='IgnoredWorkspaceSharing',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('note', models.TextField(help_text='Note about why this email is being ignored.')),
('ignored_email', models.EmailField(help_text='Email address to ignore.', max_length=254)),
('added_by', models.ForeignKey(help_text='User who added the record to this table.', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(help_text='Workspace where email should be ignored.', on_delete=django.db.models.deletion.CASCADE, to='anvil_consortium_manager.workspace')),
],
),
migrations.AddConstraint(
model_name='ignoredworkspacesharing',
constraint=models.UniqueConstraint(fields=('workspace', 'ignored_email'), name='unique_workspace_ignored_email'),
),
]
Loading
Loading