Skip to content

Commit

Permalink
feat: add view families permission, restrict child/guardian info with it
Browse files Browse the repository at this point in the history
- children and guardians graphql endpoints (i.e. Children query and
  Guardians query) will now raise permission denied error if user does
  not have permission to view families
- user can't see any guardian's contact info (i.e. email and phone
  number) without view families permission, except their own

also:
 - refactor permission handling a bit to reduce code duplication

refs KK-1403
  • Loading branch information
karisal-anders committed Jan 28, 2025
1 parent 2e1e03e commit e89195b
Show file tree
Hide file tree
Showing 14 changed files with 455 additions and 249 deletions.
12 changes: 9 additions & 3 deletions children/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@

from children.notifications import NotificationType
from common.schema import (
DjangoFilterAndOffsetConnectionField,
ViewFamiliesPermissionRequiredFilterOffsetConnectionField,
set_obj_languages_spoken_at_home,
)
from common.utils import login_required, map_enums_to_values_in_kwargs, update_object
from common.utils import (
login_required,
map_enums_to_values_in_kwargs,
update_object,
)
from events.models import Event, EventGroup, EventQueryset, Occurrence
from kukkuu.exceptions import (
ApiUsageError,
Expand Down Expand Up @@ -633,7 +637,9 @@ def mutate_and_get_payload(cls, root, info, **kwargs):


class Query:
children = DjangoFilterAndOffsetConnectionField(ChildNode, projectId=graphene.ID())
children = ViewFamiliesPermissionRequiredFilterOffsetConnectionField(
ChildNode, projectId=graphene.ID()
)
child = relay.Node.Field(ChildNode)
child_notes = graphene.Field(ChildNotesNode, id=graphene.ID(required=True))

Expand Down
108 changes: 1 addition & 107 deletions children/tests/snapshots/snap_test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,37 +281,6 @@
}
}

snapshots['test_children_query_normal_user 1'] = {
'data': {
'children': {
'edges': [
{
'node': {
'birthyear': 2019,
'name': 'Richard Hayes',
'postalCode': '57776',
'relationships': {
'edges': [
{
'node': {
'guardian': {
'email': 'michellewalker@example.net',
'firstName': 'Denise',
'lastName': 'Thompson',
'phoneNumber': '001-206-575-0649x7638'
},
'type': 'OTHER_RELATION'
}
}
]
}
}
}
]
}
}
}

snapshots['test_children_query_ordering 1'] = {
'data': {
'children': {
Expand Down Expand Up @@ -369,7 +338,7 @@
}
}

snapshots['test_children_query_project_user 1'] = {
snapshots['test_children_query_project_user_with_global_view_families_perm 1'] = {
'data': {
'children': {
'edges': [
Expand Down Expand Up @@ -400,81 +369,6 @@
}
}

snapshots['test_children_query_project_user_and_guardian 1'] = {
'data': {
'children': {
'edges': [
{
'node': {
'birthyear': 2021,
'name': 'Not own child same project - Should be returned 3/3',
'postalCode': '11715',
'relationships': {
'edges': [
{
'node': {
'guardian': {
'email': 'anthonycross@example.com',
'firstName': 'Sarah',
'lastName': 'Larsen',
'phoneNumber': '4895817101'
},
'type': 'OTHER_RELATION'
}
}
]
}
}
},
{
'node': {
'birthyear': 2022,
'name': 'Own child another project - Should be returned 2/3',
'postalCode': '34669',
'relationships': {
'edges': [
{
'node': {
'guardian': {
'email': 'michellewalker@example.net',
'firstName': 'Michael',
'lastName': 'Patton',
'phoneNumber': '235.857.7767x124'
},
'type': 'OTHER_GUARDIAN'
}
}
]
}
}
},
{
'node': {
'birthyear': 2021,
'name': 'Own child same project - Should be returned 1/3',
'postalCode': '06497',
'relationships': {
'edges': [
{
'node': {
'guardian': {
'email': 'michellewalker@example.net',
'firstName': 'Michael',
'lastName': 'Patton',
'phoneNumber': '235.857.7767x124'
},
'type': 'OTHER_GUARDIAN'
}
}
]
}
}
}
]
}
}
}

snapshots['test_children_total_count[None] 1'] = {
'data': {
'children': {
Expand Down
164 changes: 133 additions & 31 deletions children/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
)
from languages.models import Language
from projects.factories import ProjectFactory
from projects.models import ProjectPermission
from users.factories import GuardianFactory
from users.models import Guardian

Expand Down Expand Up @@ -374,17 +375,23 @@ def test_children_query_unauthenticated(api_client):
assert_permission_denied(executed)


def test_children_query_normal_user(snapshot, user_api_client, project):
def test_children_query_normal_user(user_api_client, project):
ChildWithGuardianFactory(
relationship__guardian__user=user_api_client.user, project=project
)

executed = user_api_client.execute(CHILDREN_QUERY)

snapshot.assert_match(executed)
assert_permission_denied(executed)


def test_children_query_project_user(
def test_children_query_project_user(project_user_api_client):
executed = project_user_api_client.execute(CHILDREN_QUERY)

assert_permission_denied(executed)


def test_children_query_project_user_with_global_view_families_perm(
snapshot, project_user_api_client, project, another_project
):
ChildWithGuardianFactory(
Expand All @@ -395,38 +402,21 @@ def test_children_query_project_user(
project=another_project,
)

# Give user global permission to view families
assign_perm(
ProjectPermission.VIEW_FAMILIES.permission_name, project_user_api_client.user
)

executed = project_user_api_client.execute(CHILDREN_QUERY)

snapshot.assert_match(executed)


def test_children_query_project_user_and_guardian(
snapshot, project_user_api_client, project, another_project
def test_children_query_project_user_no_view_families_perm(
project_user_no_view_families_perm_api_client, project
):
guardian = GuardianFactory(user=project_user_api_client.user)

ChildWithGuardianFactory(
name="Own child same project - Should be returned 1/3",
project=project,
relationship__guardian=guardian,
)
ChildWithGuardianFactory(
name="Own child another project - Should be returned 2/3",
project=project,
relationship__guardian=guardian,
)
ChildWithGuardianFactory(
name="Not own child same project - Should be returned 3/3",
project=project,
)
ChildWithGuardianFactory(
name="Not own child another project - Should NOT be returned",
project=another_project,
)

executed = project_user_api_client.execute(CHILDREN_QUERY)

snapshot.assert_match(executed)
executed = project_user_no_view_families_perm_api_client.execute(CHILDREN_QUERY)
assert_permission_denied(executed)


def test_children_project_filter(
Expand Down Expand Up @@ -483,6 +473,116 @@ def test_child_query_not_own_child_project_user(
snapshot.assert_match(executed)


def test_child_query_not_own_child_project_user_no_view_families_perm(
project_user_no_view_families_perm_api_client, project
):
"""
Test that project user without view families permission sees others' child info
using Child query, but the guardian's email and phone number are empty.
"""
child = ChildWithGuardianFactory(
name="Test child name",
birthyear=2020,
postal_code="00100",
relationship__type=Relationship.PARENT,
relationship__guardian=GuardianFactory(
first_name="Test first name",
last_name="Test last name",
email="test@example.org",
phone_number="123456789",
),
project=project,
)

variables = {"id": to_global_id("ChildNode", child.id)}

executed = project_user_no_view_families_perm_api_client.execute(
CHILD_QUERY, variables=variables
)

assert executed == {
"data": {
"child": {
"name": "Test child name",
"birthyear": 2020,
"postalCode": "00100",
"relationships": {
"edges": [
{
"node": {
"type": "PARENT",
"guardian": {
"firstName": "Test first name",
"lastName": "Test last name",
# Guardian's contact info should be empty
"email": "",
"phoneNumber": "",
},
}
}
]
},
}
}
}


def test_child_query_own_child_project_user_no_view_families_perm(
project_user_no_view_families_perm_api_client, project
):
"""
Test that project user without view families permission sees their own child
using Child query, and the guardian's email and phone number are visible too.
"""
user = project_user_no_view_families_perm_api_client.user
child = ChildWithGuardianFactory(
name="Test child name",
birthyear=2020,
postal_code="00100",
relationship__type=Relationship.PARENT,
relationship__guardian=GuardianFactory(
user=user,
first_name="Test first name",
last_name="Test last name",
email="test@example.org",
phone_number="123456789",
),
project=project,
)

variables = {"id": to_global_id("ChildNode", child.id)}

executed = project_user_no_view_families_perm_api_client.execute(
CHILD_QUERY, variables=variables
)

assert executed == {
"data": {
"child": {
"name": "Test child name",
"birthyear": 2020,
"postalCode": "00100",
"relationships": {
"edges": [
{
"node": {
"type": "PARENT",
"guardian": {
"firstName": "Test first name",
"lastName": "Test last name",
# Guardian's contact info should be visible
"email": "test@example.org",
"phoneNumber": "123456789",
},
}
}
]
},
}
}
}


ADD_CHILD_VARIABLES = {
"input": {
"name": "Pekka",
Expand Down Expand Up @@ -1621,7 +1721,9 @@ def test_children_total_count(
snapshot.assert_match(executed)


def test_children_query_ordering(snapshot, project, project_user_api_client):
def test_children_query_ordering(
snapshot, project, project_user_with_global_view_families_perm_api_client
):
with freeze_time("2020-12-12"):
ChildWithGuardianFactory(name="Alpha", project=project)
ChildWithGuardianFactory(name="Bravo", project=project)
Expand All @@ -1633,7 +1735,7 @@ def test_children_query_ordering(snapshot, project, project_user_api_client):
ChildWithGuardianFactory(name="", project=project)
ChildWithGuardianFactory(name="Charlie", project=project)

executed = project_user_api_client.execute(
executed = project_user_with_global_view_families_perm_api_client.execute(
"""
query Children {
children {
Expand Down
Loading

0 comments on commit e89195b

Please sign in to comment.