Exemplo n.º 1
0
def test_permissions_revoke_all_iam_permissions(clean_db, monkeypatch):
    """
    Smoke test that Permissions.revoke_all_iam_permissions calls revoke_download_access the right arguments.
    """
    gcloud_client = mock_gcloud_client(monkeypatch)
    user = Users(email="*****@*****.**")
    user.insert()
    trial = TrialMetadata(trial_id=TRIAL_ID, metadata_json=METADATA)
    trial.insert()

    upload_types = ["wes_bam", "ihc", "rna_fastq", "plasma"]
    for upload_type in upload_types:
        Permissions(
            granted_to_user=user.id,
            trial_id=trial.trial_id,
            upload_type=upload_type,
            granted_by_user=user.id,
        ).insert()

    Permissions.revoke_all_iam_permissions()
    gcloud_client.revoke_download_access.assert_has_calls(
        [call(user.email, trial.trial_id, upload_type) for upload_type in upload_types]
    )

    # not called on admins or nci biobank users
    gcloud_client.revoke_download_access.reset_mock()
    for role in [CIDCRole.ADMIN.value, CIDCRole.NCI_BIOBANK_USER.value]:
        user.role = role
        user.update()
        Permissions.revoke_all_iam_permissions()
        gcloud_client.revoke_download_access.assert_not_called()
Exemplo n.º 2
0
def mock_admin_user(cidc_api, monkeypatch) -> int:
    user = Users(**{**users["json"], "email": "*****@*****.**", "id": None})
    mock_current_user(user, monkeypatch)

    with cidc_api.app_context():
        user.insert()
        return user.id
Exemplo n.º 3
0
def setup_trial_and_user(cidc_api, monkeypatch) -> int:
    """
    Insert a trial and a cimac-user into the database, and set the user
    as the current user.
    """
    # this is necessary for adding/removing permissions from this user
    # without trying to contact GCP
    mock_gcloud_client(monkeypatch)

    user = Users(email=user_email,
                 role=CIDCRole.CIMAC_USER.value,
                 approval_date=datetime.now())
    mock_current_user(user, monkeypatch)

    with cidc_api.app_context():
        TrialMetadata(
            trial_id="test_trial",
            metadata_json={
                prism.PROTOCOL_ID_FIELD_NAME: trial_id,
                "participants": [],
                "allowed_cohort_names": ["Arm_Z"],
                "allowed_collection_event_names": [],
            },
        ).insert()

        user.insert()
        return user.id
Exemplo n.º 4
0
def test_poll_upload_merge_status(cidc_api, clean_db, monkeypatch):
    """
    Check pull_upload_merge_status endpoint behavior
    """
    user_id = setup_trial_and_user(cidc_api, monkeypatch)
    with cidc_api.app_context():
        user = Users.find_by_id(user_id)
    make_cimac_biofx_user(user_id, cidc_api)

    metadata = {PROTOCOL_ID_FIELD_NAME: trial_id}

    with cidc_api.app_context():
        other_user = Users(email="*****@*****.**")
        other_user.insert()
        upload_job = UploadJobs.create(
            upload_type="wes",
            uploader_email=user.email,
            gcs_file_map={},
            metadata=metadata,
            gcs_xlsx_uri="",
        )
        upload_job.insert()
        upload_job_id = upload_job.id

    client = cidc_api.test_client()

    # Upload not found
    res = client.get(
        f"/ingestion/poll_upload_merge_status/12345?token={upload_job.token}")
    assert res.status_code == 404

    upload_job_url = (
        f"/ingestion/poll_upload_merge_status/{upload_job_id}?token={upload_job.token}"
    )

    # Upload not-yet-ready
    res = client.get(upload_job_url)
    assert res.status_code == 200
    assert "retry_in" in res.json and res.json["retry_in"] == 5
    assert "status" not in res.json

    test_details = "A human-friendly reason for this "
    for status in [
            UploadJobStatus.MERGE_COMPLETED.value,
            UploadJobStatus.MERGE_FAILED.value,
    ]:
        # Simulate cloud function merge status update
        with cidc_api.app_context():
            upload_job._set_status_no_validation(status)
            upload_job.status_details = test_details
            upload_job.update()

        # Upload ready
        res = client.get(upload_job_url)
        assert res.status_code == 200
        assert "retry_in" not in res.json
        assert "status" in res.json and res.json["status"] == status
        assert ("status_details" in res.json
                and res.json["status_details"] == test_details)
Exemplo n.º 5
0
def test_permissions_delete(clean_db, monkeypatch, caplog):
    gcloud_client = mock_gcloud_client(monkeypatch)
    user = Users(email="*****@*****.**")
    user.insert()
    trial = TrialMetadata(trial_id=TRIAL_ID, metadata_json=METADATA)
    trial.insert()
    perm = Permissions(
        granted_to_user=user.id,
        trial_id=trial.trial_id,
        upload_type="wes_bam",
        granted_by_user=user.id,
    )
    perm.insert()

    # Deleting a record by a user doesn't exist leads to an error
    gcloud_client.reset_mocks()
    with pytest.raises(NoResultFound, match="no user with id"):
        perm.delete(deleted_by=999999)

    # Deletion of an existing permission leads to no error
    gcloud_client.reset_mocks()
    with caplog.at_level(logging.DEBUG):
        perm.delete(deleted_by=user.id)
    gcloud_client.revoke_download_access.assert_called_once()
    gcloud_client.grant_download_access.assert_not_called()
    assert any(
        log_record.message.strip()
        == f"admin-action: {user.email} removed from {user.email} the permission wes_bam on {trial.trial_id}"
        for log_record in caplog.records
    )

    # Deleting an already-deleted record is idempotent
    gcloud_client.reset_mocks()
    perm.delete(deleted_by=user)
    gcloud_client.revoke_download_access.assert_called_once()
    gcloud_client.grant_download_access.assert_not_called()

    # Deleting a record whose user doesn't exist leads to an error
    gcloud_client.reset_mocks()
    with pytest.raises(NoResultFound, match="no user with id"):
        Permissions(granted_to_user=999999).delete(deleted_by=user)

    gcloud_client.revoke_download_access.assert_not_called()
    gcloud_client.grant_download_access.assert_not_called()

    # If revoking a permission from a "network-viewer", no GCS IAM actions are taken
    gcloud_client.revoke_download_access.reset_mock()
    user.role = CIDCRole.NETWORK_VIEWER.value
    user.update()
    perm = Permissions(
        granted_to_user=user.id,
        trial_id=trial.trial_id,
        upload_type="ihc",
        granted_by_user=user.id,
    )
    perm.insert()
    perm.delete(deleted_by=user)
    gcloud_client.revoke_download_access.assert_not_called()
Exemplo n.º 6
0
def test_user_get_data_access_report(clean_db, monkeypatch):
    """Test that user data access info is collected as expected"""
    mock_gcloud_client(monkeypatch)

    admin_user = Users(
        email="*****@*****.**",
        organization="CIDC",
        approval_date=datetime.now(),
        role=CIDCRole.ADMIN.value,
    )
    admin_user.insert()

    cimac_user = Users(
        email="*****@*****.**",
        organization="DFCI",
        approval_date=datetime.now(),
        role=CIDCRole.CIMAC_USER.value,
    )
    cimac_user.insert()

    trial = TrialMetadata(trial_id=TRIAL_ID, metadata_json=METADATA)
    trial.insert()

    upload_types = ["wes_bam", "ihc"]

    # Note that admins don't need permissions to view data,
    # so we're deliberately issuing unnecessary permissions here.
    for user in [admin_user, cimac_user]:
        for t in upload_types:
            Permissions(
                granted_to_user=user.id,
                granted_by_user=admin_user.id,
                trial_id=trial.trial_id,
                upload_type=t,
            ).insert()

    bio = io.BytesIO()
    result_df = Users.get_data_access_report(bio)
    bio.seek(0)

    # Make sure bytes were written to the BytesIO instance
    assert bio.getbuffer().nbytes > 0

    # Make sure report data has expected info
    assert set(result_df.columns) == set(
        ["email", "role", "organization", "trial_id", "permissions"]
    )
    for user in [admin_user, cimac_user]:
        user_df = result_df[result_df.email == user.email]
        assert set([user.role]) == set(user_df.role)
        assert set([user.organization]) == set(user_df.organization)
        if user == admin_user:
            assert set(["*"]) == set(user_df.permissions)
        else:
            assert set(user_df.permissions).issubset(["wes_bam,ihc", "ihc,wes_bam"])
def setup_user(cidc_api, monkeypatch) -> int:
    current_user = Users(
        email="*****@*****.**",
        role=CIDCRole.CIMAC_USER.value,
        approval_date=datetime.now(),
    )
    mock_current_user(current_user, monkeypatch)

    with cidc_api.app_context():
        current_user.insert()
        return current_user.id
Exemplo n.º 8
0
def test_permissions_broad_perms(clean_db, monkeypatch):
    gcloud_client = mock_gcloud_client(monkeypatch)
    user = Users(email="*****@*****.**")
    user.insert()
    trial = TrialMetadata(trial_id=TRIAL_ID, metadata_json=METADATA)
    trial.insert()
    other_trial = TrialMetadata(
        trial_id="other-trial",
        metadata_json={**METADATA, "protocol_identifier": "other-trial"},
    )
    other_trial.insert()
    for ut in ["wes_fastq", "olink"]:
        for tid in [trial.trial_id, other_trial.trial_id]:
            Permissions(
                granted_to_user=user.id,
                trial_id=tid,
                upload_type=ut,
                granted_by_user=user.id,
            ).insert()

    # Can't insert a permission for access to all trials and assays
    with pytest.raises(ValueError, match="must have a trial id or upload type"):
        Permissions(granted_to_user=user.id, granted_by_user=user.id).insert()

    # Inserting a trial-level permission should delete other more specific related perms.
    trial_query = clean_db.query(Permissions).filter(
        Permissions.trial_id == trial.trial_id
    )
    assert trial_query.count() == 2
    Permissions(
        trial_id=trial.trial_id, granted_to_user=user.id, granted_by_user=user.id
    ).insert()
    assert trial_query.count() == 1
    perm = trial_query.one()
    assert perm.trial_id == trial.trial_id
    assert perm.upload_type is None

    # Inserting an upload-level permission should delete other more specific related perms.
    olink_query = clean_db.query(Permissions).filter(Permissions.upload_type == "olink")
    assert olink_query.count() == 1
    assert olink_query.one().trial_id == other_trial.trial_id
    Permissions(
        upload_type="olink", granted_to_user=user.id, granted_by_user=user.id
    ).insert()
    assert olink_query.count() == 1
    perm = olink_query.one()
    assert perm.trial_id is None
    assert perm.upload_type == "olink"

    # Getting perms for a particular user-trial-type returns broader perms
    perm = Permissions.find_for_user_trial_type(user.id, trial.trial_id, "ihc")
    assert perm is not None and perm.upload_type is None
    perm = Permissions.find_for_user_trial_type(user.id, "some random trial", "olink")
    assert perm is not None and perm.trial_id is None
def setup_user(cidc_api, monkeypatch) -> int:
    # this is necessary for adding/removing permissions from this user
    # without trying to contact GCP
    mock_gcloud_client(monkeypatch)

    current_user = Users(
        email="*****@*****.**",
        role=CIDCRole.CIMAC_USER.value,
        approval_date=datetime.now(),
    )
    mock_current_user(current_user, monkeypatch)

    with cidc_api.app_context():
        current_user.insert()
        return current_user.id
Exemplo n.º 10
0
def test_common_insert(clean_db):
    """Test insert, inherited from CommonColumns"""
    # Check disabling committing
    u1 = Users(email="a")
    u1.insert(commit=False)
    assert not u1.id

    # Insert a new record without disabling committing
    u2 = Users(email="b")
    u2.insert()
    assert u1.id and u1._etag
    assert u2.id and u2._etag
    assert u1._etag != u2._etag

    assert Users.find_by_id(u1.id)
    assert Users.find_by_id(u2.id)
Exemplo n.º 11
0
def setup_permissions(cidc_api, monkeypatch) -> Tuple[int, int]:
    """
    Create two users, one trial, and three permissions in `db`.
    Two permissions will belong to the first user, and the third will
    belong to the second one. Returns the first and second user ids 
    as a tuple.
    """
    current_user = Users(
        id=1,
        email="*****@*****.**",
        role=CIDCRole.CIMAC_USER.value,
        approval_date=datetime.now(),
    )
    other_user = Users(id=2, email="*****@*****.**")

    mock_current_user(current_user, monkeypatch)

    with cidc_api.app_context():
        # Create users
        current_user.insert()
        other_user.insert()

        # Create trial
        TrialMetadata.create(
            TRIAL_ID,
            {
                "protocol_identifier": TRIAL_ID,
                "allowed_collection_event_names": [],
                "allowed_cohort_names": [],
                "participants": [],
            },
        )

        # Create permissions
        def create_permission(uid, assay):
            Permissions(
                granted_by_user=uid,
                granted_to_user=uid,
                trial_id=TRIAL_ID,
                upload_type=assay,
            ).insert()

        create_permission(current_user.id, "ihc")
        create_permission(current_user.id, "olink")
        create_permission(other_user.id, "olink")

        return current_user.id, other_user.id
Exemplo n.º 12
0
def test_user_confirm_approval(clean_db, monkeypatch):
    """Ensure that users are notified when their account goes from pending to approved."""
    confirm_account_approval = MagicMock()
    monkeypatch.setattr(
        "cidc_api.shared.emails.confirm_account_approval", confirm_account_approval
    )

    user = Users(email="*****@*****.**")
    user.insert()

    # The confirmation email shouldn't be sent for updates unrelated to account approval
    user.update(changes={"first_n": "foo"})
    confirm_account_approval.assert_not_called()

    # The confirmation email should be sent for updates related to account approval
    user.update(changes={"approval_date": datetime.now()})
    confirm_account_approval.assert_called_once_with(user, send_email=True)
Exemplo n.º 13
0
def setup_upload_jobs(cidc_api) -> Tuple[int, int]:
    """
    Insert two uploads into the database created by different users
    and return their IDs.
    """
    with cidc_api.app_context():
        other_user = Users(email="*****@*****.**")
        other_user.insert()

        job1 = UploadJobs(
            uploader_email=user_email,
            trial_id=trial_id,
            status=UploadJobStatus.STARTED.value,
            metadata_patch={
                "test": {
                    "upload_placeholder": "baz"
                },
                "test2": "foo"
            },
            upload_type="",
            gcs_xlsx_uri="",
            gcs_file_map={"bip": "baz"},
            multifile=False,
        )
        job2 = UploadJobs(
            uploader_email=other_user.email,
            trial_id=trial_id,
            status=UploadJobStatus.STARTED.value,
            metadata_patch={
                "array": [{
                    "upload_placeholder": "baz"
                }, {
                    "test2": "foo"
                }]
            },
            upload_type="",
            gcs_xlsx_uri="",
            gcs_file_map={"bip": "baz"},
            multifile=False,
        )

        job1.insert()
        job2.insert()

        return job1.id, job2.id
Exemplo n.º 14
0
def setup_users(cidc_api, monkeypatch, registered=True) -> Tuple[int, int]:
    """
    Insert two users into the database. If `registered=False`, don't
    register the first user.
    """
    current_user = Users(id=1, email="*****@*****.**")
    other_user = Users(id=2, email="*****@*****.**")

    mock_current_user(current_user, monkeypatch)

    with cidc_api.app_context():
        if registered:
            current_user.role = CIDCRole.CIMAC_USER.value
            current_user.approval_date = datetime.now()
        current_user.insert()
        other_user.insert()

        return current_user.id, other_user.id
Exemplo n.º 15
0
def test_common_delete(clean_db):
    """Test delete, inherited from CommonColumns"""
    user1 = Users(email="foo")
    user2 = Users(email="bar")

    # Try to delete an uninserted record
    with pytest.raises(InvalidRequestError):
        user1.delete()

    user1.insert()
    user2.insert()

    # Defer a deletion with commit=False
    user1.delete(commit=False)
    assert Users.find_by_id(user1.id)

    # Delete with auto-commit
    user2.delete()
    assert not Users.find_by_id(user1.id)
    assert not Users.find_by_id(user2.id)
Exemplo n.º 16
0
def test_common_update(clean_db):
    """Test update, inherited from CommonColumns"""
    email = "foo"
    user = Users(id=1, email=email)

    # Record not found
    with pytest.raises(NoResultFound):
        user.update()

    user.insert()

    _updated = user._updated

    # Update via setattr and changes
    first_n = "hello"
    last_n = "goodbye"
    user.last_n = last_n
    user.update(changes={"first_n": first_n})
    user = Users.find_by_id(user.id)
    assert user._updated > _updated
    assert user.first_n == first_n
    assert user.last_n == last_n

    _updated = user._updated
    _etag = user._etag

    # Make sure you can clear a field to null
    user.update(changes={"first_n": None})
    user = Users.find_by_id(user.id)
    assert user._updated > _updated
    assert _etag != user._etag
    assert user.first_n is None

    _updated = user._updated
    _etag = user._etag

    # Make sure etags don't change if public fields don't change
    user.update()
    user = Users.find_by_id(user.id)
    assert user._updated > _updated
    assert _etag == user._etag
Exemplo n.º 17
0
def test_permissions_grant_iam_permissions(clean_db, monkeypatch):
    """
    Smoke test that Permissions.grant_iam_permissions calls grant_download_access with the right arguments.
    """
    refresh_intake_access = MagicMock()
    monkeypatch.setattr(
        "cidc_api.models.models.refresh_intake_access", refresh_intake_access
    )

    gcloud_client = mock_gcloud_client(monkeypatch)
    user = Users(email="*****@*****.**", role=CIDCRole.NETWORK_VIEWER.value)
    user.insert()
    trial = TrialMetadata(trial_id=TRIAL_ID, metadata_json=METADATA)
    trial.insert()

    upload_types = ["wes_bam", "ihc", "rna_fastq", "plasma"]
    for upload_type in upload_types:
        Permissions(
            granted_to_user=user.id,
            trial_id=trial.trial_id,
            upload_type=upload_type,
            granted_by_user=user.id,
        ).insert()

    # IAM permissions not granted to network viewers
    Permissions.grant_iam_permissions(user=user)
    gcloud_client.grant_download_access.assert_not_called()

    # IAM permissions should be granted for any other role
    user.role = CIDCRole.CIMAC_USER.value
    Permissions.grant_iam_permissions(user=user)
    for upload_type in upload_types:
        assert (
            call(user.email, trial.trial_id, upload_type)
            in gcloud_client.grant_download_access.call_args_list
        )

    refresh_intake_access.assert_called_once_with(user.email)
Exemplo n.º 18
0
def setup_data(cidc_api, clean_db):
    user = Users(email="*****@*****.**", approval_date=datetime.now())
    shipment = {
        "courier": "FEDEX",
        "ship_to": "",
        "ship_from": "",
        "assay_type": assay_type,
        "manifest_id": manifest_id,
        "date_shipped": "2020-06-10 00:00:00",
        "date_received": "2020-06-11 00:00:00",
        "account_number": "",
        "assay_priority": "1",
        "receiving_party": "MSSM_Rahman",
        "tracking_number": "",
        "shipping_condition": "Frozen_Dry_Ice",
        "quality_of_shipment": "Specimen shipment received in good condition",
    }
    metadata = {
        "protocol_identifier":
        trial_id,
        "shipments": [
            # we get duplicate shipment uploads sometimes
            shipment,
            shipment,
        ],
        "participants": [{
            "cimac_participant_id":
            f"CTTTPP{p}",
            "participant_id":
            "x",
            "cohort_name":
            "",
            "samples": [{
                "cimac_id": f"CTTTPP{p}SS.0{s}",
                "sample_location": "",
                "type_of_primary_container": "Other",
                "type_of_sample": "Other",
                "collection_event_name": "",
                "parent_sample_id": "",
            } for s in range(num_samples[p])],
        } for p in range(num_participants)],
        "allowed_cohort_names": [""],
        "allowed_collection_event_names": [""],
    }
    trial = TrialMetadata(trial_id=trial_id, metadata_json=metadata)
    upload_job = UploadJobs(
        uploader_email=user.email,
        trial_id=trial.trial_id,
        upload_type="pbmc",
        gcs_xlsx_uri="",
        metadata_patch=metadata,
        multifile=False,
    )
    upload_job._set_status_no_validation(UploadJobStatus.MERGE_COMPLETED.value)
    with cidc_api.app_context():
        user.insert()
        trial.insert()
        upload_job.insert()

        clean_db.refresh(user)
        clean_db.refresh(upload_job)
        clean_db.refresh(trial)

    return user, upload_job, trial
Exemplo n.º 19
0
def test_authorize(cidc_api, clean_db):
    """Check that authorization works as expected."""
    user = Users(**PAYLOAD)

    with cidc_api.app_context():
        # Unregistered user should not be authorized to do anything to any resource except "users"
        with pytest.raises(Unauthorized, match="not registered"):
            auth.authorize(user, [], "some-resource", "some-http-method")

        # We can't track accesses for users who aren't registered
        assert user._accessed is None

        # Unregistered user should not be able to GET users
        with pytest.raises(Unauthorized, match="not registered"):
            auth.authorize(user, [], "users", "GET")
        assert user._accessed is None

        # Unregistered user should not be able to GET self
        with pytest.raises(Unauthorized, match="not registered"):
            auth.authorize(user, [], "self", "GET")
        assert user._accessed is None

        # Unregistered user should be able to POST users
        assert auth.authorize(user, [], "self", "POST")

        # Add the user to the db but don't approve yet
        user.insert()

        # Unapproved user isn't authorized to do anything
        with pytest.raises(Unauthorized, match="pending approval"):
            auth.authorize(user, [], "self", "POST")

        # Check that we tracked this user's last access
        assert user._accessed.date() == date.today()
        _accessed = user._accessed

        # Ensure unapproved user can access their own data
        assert auth.authorize(user, [], "self", "GET")

        # Give the user a role but don't approve them
        user.role = CIDCRole.CIMAC_USER.value
        user.update()

        # Unapproved user *with an authorized role* still shouldn't be authorized
        with pytest.raises(Unauthorized, match="pending approval"):
            auth.authorize(user, [CIDCRole.CIMAC_USER.value], "self", "POST")

        # Approve the user
        user.approval_date = datetime.now()
        user.update()

        # If user doesn't have required role, they should not be authorized.
        with pytest.raises(Unauthorized, match="not authorized to access"):
            auth.authorize(user, [CIDCRole.ADMIN.value], "some-resource",
                           "some-http-method")

        # If user has an allowed role, they should be authorized
        assert auth.authorize(user, [CIDCRole.CIMAC_USER.value],
                              "some-resource", "some-http-method")

        # If the resource has no role restrictions, they should be authorized
        assert auth.authorize(user, [], "some-resource", "some-http-method")

        # Disable user
        user.disabled = True
        user.update()

        # If user has an allowed role but is disabled, they should be unauthorized
        with pytest.raises(Unauthorized, match="disabled"):
            auth.authorize(user, [CIDCRole.CIMAC_USER.value], "some-resource",
                           "some-http-method")

        # Ensure unapproved user can access their own data
        assert auth.authorize(user, [], "self", "GET")

        # If the resource has no role restrictions, they should be still unauthorized
        with pytest.raises(Unauthorized, match="disabled"):
            auth.authorize(user, [], "some-resource", "some-http-method")

        # Check that user's last access wasn't updated by all activity,
        # since it occurred on the same day as previous accesses
        assert user._accessed == _accessed
Exemplo n.º 20
0
def test_permissions_insert(clean_db, monkeypatch, caplog):
    gcloud_client = mock_gcloud_client(monkeypatch)
    user = Users(email="*****@*****.**")
    user.insert()
    trial = TrialMetadata(trial_id=TRIAL_ID, metadata_json=METADATA)
    trial.insert()

    _insert = MagicMock()
    monkeypatch.setattr(CommonColumns, "insert", _insert)

    # if upload_type is invalid
    with pytest.raises(ValueError, match="invalid upload type"):
        Permissions(upload_type="foo", granted_to_user=user.id, trial_id=trial.trial_id)

    # if don't give granted_by_user
    perm = Permissions(
        granted_to_user=user.id, trial_id=trial.trial_id, upload_type="wes_bam"
    )
    with pytest.raises(IntegrityError, match="`granted_by_user` user must be given"):
        perm.insert()
    _insert.assert_not_called()

    # if give bad granted_by_user
    _insert.reset_mock()
    perm = Permissions(
        granted_to_user=user.id,
        trial_id=trial.trial_id,
        upload_type="wes_bam",
        granted_by_user=999999,
    )
    with pytest.raises(IntegrityError, match="`granted_by_user` user must exist"):
        perm.insert()
    _insert.assert_not_called()

    # if give bad granted_to_user
    _insert.reset_mock()
    perm = Permissions(
        granted_to_user=999999,
        trial_id=trial.trial_id,
        upload_type="wes_bam",
        granted_by_user=user.id,
    )
    with pytest.raises(IntegrityError, match="`granted_to_user` user must exist"):
        perm.insert()
    _insert.assert_not_called()

    # This one will work
    _insert.reset_mock()
    perm = Permissions(
        granted_to_user=user.id,
        trial_id=trial.trial_id,
        upload_type="wes_bam",
        granted_by_user=user.id,
    )
    with caplog.at_level(logging.DEBUG):
        perm.insert()
    _insert.assert_called_once()
    assert any(
        log_record.message.strip()
        == f"admin-action: {user.email} gave {user.email} the permission wes_bam on {trial.trial_id}"
        for log_record in caplog.records
    )
    gcloud_client.grant_download_access.assert_called_once()

    # If granting a permission to a "network-viewer", no GCS IAM actions are taken
    _insert.reset_mock()
    gcloud_client.grant_download_access.reset_mock()
    user.role = CIDCRole.NETWORK_VIEWER.value
    user.update()
    perm = Permissions(
        granted_to_user=user.id,
        trial_id=trial.trial_id,
        upload_type="ihc",
        granted_by_user=user.id,
    )
    perm.insert()
    _insert.assert_called_once()
    gcloud_client.grant_download_access.assert_not_called()