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 view to audit all dbGaP Applications and Workspaces at the same time #418

Merged
merged 12 commits into from
Feb 6, 2024
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ primed/media/

primed.db
backup_primed.db
*.db

### dbbackups
dbbackups/
136 changes: 136 additions & 0 deletions add_dbgap_example_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Temporary script to create some test data.
# Run with: python manage.py shell < add_cdsa_example_data.py

from anvil_consortium_manager.tests.factories import GroupGroupMembershipFactory

from primed.dbgap import models
from primed.dbgap.tests import factories
from primed.primed_anvil.tests.factories import StudyFactory
from primed.users.tests.factories import UserFactory

# Studies
fhs = StudyFactory.create(short_name="FHS", full_name="Framingham Heart Study")
mesa = StudyFactory.create(
short_name="MESA", full_name="Multi-Ethnic Study of Atherosclerosis"
)
aric = StudyFactory.create(
short_name="ARIC", full_name="Atherosclerosis Risk in Communities"
)

# dbGaP study accessions
dbgap_study_accession_fhs = factories.dbGaPStudyAccessionFactory.create(
dbgap_phs=7, studies=[fhs]
)
dbgap_study_accession_mesa = factories.dbGaPStudyAccessionFactory.create(
dbgap_phs=209, studies=[mesa]
)
dbgap_study_accession_aric = factories.dbGaPStudyAccessionFactory.create(
dbgap_phs=280, studies=[aric]
)


# Create some dbGaP workspaces.
workspace_fhs_1 = factories.dbGaPWorkspaceFactory.create(
dbgap_study_accession=dbgap_study_accession_fhs,
dbgap_version=33,
dbgap_participant_set=12,
dbgap_consent_code=1,
dbgap_consent_abbreviation="HMB",
workspace__name="DBGAP_FHS_v33_p12_HMB",
)
workspace_fhs_2 = factories.dbGaPWorkspaceFactory.create(
dbgap_study_accession=dbgap_study_accession_fhs,
dbgap_version=33,
dbgap_participant_set=12,
dbgap_consent_code=2,
dbgap_consent_abbreviation="GRU",
workspace__name="DBGAP_FHS_v33_p12_GRU",
)
workspace_mesa_1 = factories.dbGaPWorkspaceFactory.create(
dbgap_study_accession=dbgap_study_accession_mesa,
dbgap_version=2,
dbgap_participant_set=1,
dbgap_consent_code=1,
dbgap_consent_abbreviation="HMB-NPU",
workspace__name="DBGAP_MESA_v2_p1_HMB-NPU",
)
workspace_mesa_2 = factories.dbGaPWorkspaceFactory.create(
dbgap_study_accession=dbgap_study_accession_mesa,
dbgap_version=2,
dbgap_participant_set=1,
dbgap_consent_code=2,
dbgap_consent_abbreviation="HMB-NPU-IRB",
workspace__name="DBGAP_MESA_v2_p1_HMB-NPU-IRB",
)
workspace_aric = factories.dbGaPWorkspaceFactory.create(
dbgap_study_accession=dbgap_study_accession_aric,
dbgap_version=3,
dbgap_participant_set=1,
dbgap_consent_code=1,
dbgap_consent_abbreviation="HMB",
workspace__name="DBGAP_ARIC_v3_p1_HMB",
)


# Create some dbGaP applications
dbgap_application_1 = factories.dbGaPApplicationFactory.create(
principal_investigator=UserFactory.create(name="Ken", username="Ken"),
dbgap_project_id=33119,
)
# Add a snapshot
dar_snapshot_1 = factories.dbGaPDataAccessSnapshotFactory.create(
dbgap_application=dbgap_application_1
)
# Add some data access requests.
dar_1_1 = factories.dbGaPDataAccessRequestForWorkspaceFactory.create(
dbgap_workspace=workspace_fhs_1,
dbgap_data_access_snapshot=dar_snapshot_1,
dbgap_dac="NHLBI",
dbgap_current_status=models.dbGaPDataAccessRequest.APPROVED,
)
dar_1_2 = factories.dbGaPDataAccessRequestForWorkspaceFactory.create(
dbgap_workspace=workspace_fhs_2,
dbgap_data_access_snapshot=dar_snapshot_1,
dbgap_dac="NHLBI",
dbgap_current_status=models.dbGaPDataAccessRequest.REJECTED,
)
dar_1_3 = factories.dbGaPDataAccessRequestForWorkspaceFactory.create(
dbgap_workspace=workspace_mesa_1,
dbgap_data_access_snapshot=dar_snapshot_1,
dbgap_dac="NHLBI",
dbgap_current_status=models.dbGaPDataAccessRequest.APPROVED,
)
dar_1_4 = factories.dbGaPDataAccessRequestForWorkspaceFactory.create(
dbgap_workspace=workspace_mesa_2,
dbgap_data_access_snapshot=dar_snapshot_1,
dbgap_dac="NHLBI",
dbgap_current_status=models.dbGaPDataAccessRequest.REJECTED,
)

dbgap_application_2 = factories.dbGaPApplicationFactory.create(
principal_investigator=UserFactory.create(name="Alisa", username="Alisa"),
dbgap_project_id=33371,
)
# Add a snapshot
dar_snapshot_2 = factories.dbGaPDataAccessSnapshotFactory.create(
dbgap_application=dbgap_application_2
)
# Add some data access requests, only for FHS.
dar_1_1 = factories.dbGaPDataAccessRequestForWorkspaceFactory.create(
dbgap_workspace=workspace_fhs_1,
dbgap_data_access_snapshot=dar_snapshot_2,
dbgap_dac="NHLBI",
dbgap_current_status=models.dbGaPDataAccessRequest.APPROVED,
)
dar_1_2 = factories.dbGaPDataAccessRequestForWorkspaceFactory.create(
dbgap_workspace=workspace_fhs_2,
dbgap_data_access_snapshot=dar_snapshot_2,
dbgap_dac="NHLBI",
dbgap_current_status=models.dbGaPDataAccessRequest.APPROVED,
)

# Now add dbGaP access groups.
GroupGroupMembershipFactory.create(
parent_group=workspace_fhs_1.workspace.authorization_domains.first(),
child_group=dbgap_application_1.anvil_access_group,
)
47 changes: 47 additions & 0 deletions primed/collaborative_analysis/tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from anvil_consortium_manager.tests.factories import (
ManagedGroupFactory,
WorkspaceFactory,
)
from factory import LazyAttribute, SubFactory, post_generation
from factory.django import DjangoModelFactory

from primed.users.tests.factories import UserFactory

from .. import models


class CollaborativeAnalysisWorkspaceFactory(DjangoModelFactory):
class Meta:
model = models.CollaborativeAnalysisWorkspace
skip_postgeneration_save = True

workspace = SubFactory(WorkspaceFactory, workspace_type="collab_analysis")
custodian = SubFactory(UserFactory)
analyst_group = SubFactory(
ManagedGroupFactory,
name=LazyAttribute(
lambda o: "analysts_{}".format(o.factory_parent.workspace.name)
),
)

@post_generation
def authorization_domains(self, create, extracted, **kwargs):
# Add an authorization domain.
if not create:
# Simple build, do nothing.
return

# Create an authorization domain.
auth_domain = ManagedGroupFactory.create(
name="auth_{}".format(self.workspace.name)
)
print(auth_domain)
self.workspace.authorization_domains.add(auth_domain)

@post_generation
def source_workspaces(self, create, extracted, **kwargs):
if not create or not extracted:
# Simple build, do nothing.
return

self.source_workspaces.add(*extracted)
91 changes: 44 additions & 47 deletions primed/dbgap/audit.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from abc import ABC
from dataclasses import dataclass

import django_tables2 as tables
from django.db.models import QuerySet
from django.urls import reverse
from django.utils.safestring import mark_safe

# from . import models
from primed.primed_anvil.tables import BooleanIconColumn

from .models import (
dbGaPApplication,
dbGaPDataAccessRequest,
Expand All @@ -22,6 +24,7 @@ class AuditResult:
workspace: dbGaPWorkspace
note: str
dbgap_application: dbGaPApplication
has_access: bool
data_access_request: dbGaPDataAccessRequest = None

def __post_init__(self):
Expand Down Expand Up @@ -55,6 +58,7 @@ def get_table_dictionary(self):
"data_access_request": self.data_access_request,
"dar_accession": dar_accession,
"dar_consent": dar_consent,
"has_access": self.has_access,
"note": self.note,
"action": self.get_action(),
"action_url": self.get_action_url(),
Expand All @@ -66,20 +70,22 @@ def get_table_dictionary(self):
class VerifiedAccess(AuditResult):
"""Audit results class for when access has been verified."""

pass
has_access: bool = True


@dataclass
class VerifiedNoAccess(AuditResult):
"""Audit results class for when no access has been verified."""

pass
has_access: bool = False


@dataclass
class GrantAccess(AuditResult):
"""Audit results class for when access should be granted."""

has_access: bool = False

def get_action(self):
return "Grant access"

Expand All @@ -97,6 +103,8 @@ def get_action_url(self):
class RemoveAccess(AuditResult):
"""Audit results class for when access should be removed for a known reason."""

has_access: bool = True

def get_action(self):
return "Remove access"

Expand Down Expand Up @@ -125,6 +133,7 @@ class dbGaPAccessAuditTable(tables.Table):
data_access_request = tables.Column()
dar_accession = tables.Column(verbose_name="DAR accession")
dar_consent = tables.Column(verbose_name="DAR consent")
has_access = BooleanIconColumn(show_false_icon=True)
note = tables.Column()
action = tables.Column()

Expand All @@ -139,7 +148,7 @@ def render_action(self, record, value):
)


class dbGaPAccessAudit(ABC):
class dbGaPAccessAudit:

# Access verified.
APPROVED_DAR = "Approved DAR."
Expand All @@ -159,12 +168,42 @@ class dbGaPAccessAudit(ABC):

results_table_class = dbGaPAccessAuditTable

def __init__(self):
def __init__(self, dbgap_application_queryset=None, dbgap_workspace_queryset=None):
self.completed = False
# Set up lists to hold audit results.
self.verified = None
self.needs_action = None
self.errors = None
if dbgap_application_queryset is None:
dbgap_application_queryset = dbGaPApplication.objects.all()
if not (
isinstance(dbgap_application_queryset, QuerySet)
and dbgap_application_queryset.model is dbGaPApplication
):
raise ValueError(
"dbgap_application_queryset must be a queryset of dbGaPApplication objects."
)
self.dbgap_application_queryset = dbgap_application_queryset
if dbgap_workspace_queryset is None:
dbgap_workspace_queryset = dbGaPWorkspace.objects.all()
if not (
isinstance(dbgap_workspace_queryset, QuerySet)
and dbgap_workspace_queryset.model is dbGaPWorkspace
):
raise ValueError(
"dbgap_workspace_queryset must be a queryset of dbGaPWorkspace objects."
)
self.dbgap_workspace_queryset = dbgap_workspace_queryset

def run_audit(self):
self.verified = []
self.needs_action = []
self.errors = []

for dbgap_application in self.dbgap_application_queryset:
for dbgap_workspace in self.dbgap_workspace_queryset:
self.audit_application_and_workspace(dbgap_application, dbgap_workspace)
self.completed = True

def audit_application_and_workspace(self, dbgap_application, dbgap_workspace):
"""Audit access for a specific dbGaP application and a specific workspace."""
Expand Down Expand Up @@ -319,45 +358,3 @@ def get_needs_action_table(self):
def get_errors_table(self):
"""Return a table of audit errors."""
return self.results_table_class([x.get_table_dictionary() for x in self.errors])


class dbGaPApplicationAccessAudit(dbGaPAccessAudit):
def __init__(self, dbgap_application):
super().__init__()
self.dbgap_application = dbgap_application

def run_audit(self):
"""Audit all workspaces against access provided by this dbGaPApplication."""
self.verified = []
self.needs_action = []
self.errors = []

# Get a list of all dbGaP workspaces.
dbgap_workspaces = dbGaPWorkspace.objects.all()
# Loop through workspaces and verify access.
for dbgap_workspace in dbgap_workspaces:
self.audit_application_and_workspace(
self.dbgap_application, dbgap_workspace
)
self.completed = True


class dbGaPWorkspaceAccessAudit(dbGaPAccessAudit):
def __init__(self, dbgap_workspace):
super().__init__()
self.dbgap_workspace = dbgap_workspace

def run_audit(self):
"""Audit this workspace against access provided by all dbGaPApplications."""
self.verified = []
self.needs_action = []
self.errors = []

# Get a list of all dbGaP applications.
dbgap_applications = dbGaPApplication.objects.all()
# Loop through workspaces and verify access.
for dbgap_application in dbgap_applications:
self.audit_application_and_workspace(
dbgap_application, self.dbgap_workspace
)
self.completed = True
Loading
Loading