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

Merge release branch 24.10 to develop #130

Merged
merged 34 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
80997e4
Add village to individual and group model
weilu Sep 5, 2024
e6db8b5
Unit test existing individual GQL permission control
weilu Sep 6, 2024
d4cd3ae
Enable row level security on group and individual list queries
weilu Sep 6, 2024
af44440
Row-level security for individual history query
weilu Sep 9, 2024
3269d6b
Row-level security for group history query
weilu Sep 9, 2024
48df524
Row-level security for group membership query
weilu Sep 9, 2024
93000ab
Row-level security for individual group membership history
weilu Sep 9, 2024
e01ebe2
Add row level security to individual creation query
weilu Sep 15, 2024
8cd6b76
Add row-level security to individual update query
weilu Sep 15, 2024
27a5d33
Add row-level security to individual delete query
weilu Sep 15, 2024
9b814cb
Add row-level security to individual undo delete query
weilu Sep 15, 2024
364ec1c
Add basic create group gql mutation test
weilu Sep 22, 2024
c90e48b
Add row level security to group create
weilu Sep 22, 2024
fc96164
Row level security for group update
weilu Sep 24, 2024
d9664a6
Add row level security to group delete
weilu Sep 29, 2024
80b9ba0
Add row level security to adding individual to group
weilu Sep 30, 2024
5cb938b
Verify that both individual_id and group_id are required by service
weilu Sep 30, 2024
2cbda50
Add row level security to editing individual in group
weilu Sep 30, 2024
c7556bb
Add row level security to removing individuals from group
weilu Sep 30, 2024
93b8d54
Make sure individual group move doesn’t create and delete unnecessarily
weilu Sep 30, 2024
b04a7cf
Merge pull request #128 from openimis/develop
dragos-dobre Oct 2, 2024
629486f
Remove unnecessary location-individual relationship reversal in query
weilu Oct 4, 2024
49c73f3
Merge remote-tracking branch 'wei/row-security' into release/24.10
delcroip Oct 9, 2024
77efbf4
fixing tests
delcroip Oct 11, 2024
c00b910
Fix create group and move individual service test
weilu Oct 11, 2024
daf9753
Merge branch 'develop' into feature/adding-location
delcroip Oct 11, 2024
efbcffb
Put back previously commented out test case
weilu Oct 11, 2024
324c578
Remove repeated tests & add back test case on group-individual loc match
weilu Oct 11, 2024
0925244
Merge remote-tracking branch 'origin/develop' into feature/adding-loc…
delcroip Oct 11, 2024
133b3ff
Merge branch 'feature/adding-location' of https://github.com/openimis…
delcroip Oct 11, 2024
af07aca
manage not existing individualGroup
delcroip Oct 14, 2024
78ca22f
Fix no GroupIndividual found error in test
weilu Oct 15, 2024
87c7697
Check that individual and group location match
weilu Oct 15, 2024
74f2d1c
Merge pull request #129 from openimis/feature/adding-location
delcroip Oct 16, 2024
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
158 changes: 131 additions & 27 deletions individual/gql_mutations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import graphene
from django.core.exceptions import ValidationError
from django.core.exceptions import ValidationError, PermissionDenied
from django.db import transaction
from django.db.models import Subquery, Q
from django.utils.translation import gettext as _

from core.gql.gql_mutations.base_mutation import BaseHistoryModelDeleteMutationMixin, BaseMutation, \
BaseHistoryModelUpdateMutationMixin, BaseHistoryModelCreateMutationMixin
Expand All @@ -9,13 +11,15 @@
from individual.models import Individual, Group, GroupIndividual
from individual.services import IndividualService, GroupService, GroupIndividualService, \
CreateGroupAndMoveIndividualService
from location.models import Location, LocationManager


class CreateIndividualInputType(OpenIMISMutation.Input):
first_name = graphene.String(required=True, max_length=255)
last_name = graphene.String(required=True, max_length=255)
dob = graphene.Date(required=True)
json_ext = graphene.types.json.JSONString(required=False)
location_id = graphene.Int(required=False)


class UpdateIndividualInputType(CreateIndividualInputType):
Expand Down Expand Up @@ -55,6 +59,7 @@ def resolve_recipient_type(self, info):
class CreateGroupInputType(OpenIMISMutation.Input):
code = graphene.String(required=True)
individuals_data = graphene.List(CreateGroupIndividualInputTypeInputObjectType, required=False)
location_id = graphene.Int(required=False)


class UpdateGroupInputType(CreateGroupInputType):
Expand All @@ -81,7 +86,15 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_individual_create_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))
if (
'location_id' in data and
not LocationManager().is_allowed(
user,
[data['location_id']]
)
):
raise PermissionDenied(_("unauthorized.location"))

@classmethod
def _mutate(cls, user, **data):
Expand All @@ -108,7 +121,21 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_individual_update_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))

location_from = Individual.objects.get(id=data['id']).location_id

location_to_check = [data['location_id']] if 'location_id' in data else []
if location_from:
location_to_check.append(location_from)
if (
len(location_to_check)>0 and
not LocationManager().is_allowed(
user,
location_to_check
)
):
raise PermissionDenied(_("unauthorized.location"))

@classmethod
def _mutate(cls, user, **data):
Expand Down Expand Up @@ -138,7 +165,14 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_individual_delete_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))

locations_id = list(Location.objects.filter(individuals__id__in=data['ids']).values_list('id', flat=True))
if len(locations_id)>0 and not LocationManager().is_allowed(
user,
locations_id
):
raise PermissionDenied(_("unauthorized.location"))

@classmethod
def _mutate(cls, user, **data):
Expand All @@ -152,8 +186,8 @@ def _mutate(cls, user, **data):
ids = data.get('ids')
if ids:
with transaction.atomic():
for id in ids:
obj_data = {'id': id}
for identifier in ids:
obj_data = {'id': identifier}
if IndividualConfig.check_individual_delete:
service.create_delete_task(obj_data)
else:
Expand All @@ -173,7 +207,14 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_individual_undo_delete_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))

locations_id = list(Location.objects.filter(individuals__id__in=data['ids']).values_list('id', flat=True))
if len(locations_id)>0 and not LocationManager().is_allowed(
user,
locations_id
):
raise PermissionDenied(_("unauthorized.location"))

@classmethod
def _mutate(cls, user, **data):
Expand All @@ -187,8 +228,8 @@ def _mutate(cls, user, **data):
ids = data.get('ids')
if ids:
with transaction.atomic():
for id in ids:
service.undo_delete({'id': id})
for identifier in ids:
service.undo_delete({'id': identifier})

class Input(OpenIMISMutation.Input):
ids = graphene.List(graphene.UUID)
Expand All @@ -204,8 +245,15 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_create_perms):
raise ValidationError("mutation.authentication_required")

raise PermissionDenied(_("unauthorized"))
if (
'location_id' in data and
not LocationManager().is_allowed(
user,
[data['location_id']]
)
):
raise PermissionDenied(_("unauthorized.location"))
@classmethod
def _mutate(cls, user, **data):
if "client_mutation_id" in data:
Expand All @@ -231,8 +279,18 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_update_perms):
raise ValidationError("mutation.authentication_required")

raise PermissionDenied(_("unauthorized"))
location_from = Group.objects.get(id=data['id']).location_id
location_to_check = [data['location_id']] if 'location_id' in data else []
if location_from:
location_to_check.append(location_from)
if (
len(location_to_check)>0 and not LocationManager().is_allowed(
user,
location_to_check
)
):
raise PermissionDenied(_("unauthorized.location"))
@classmethod
def _mutate(cls, user, **data):
if "client_mutation_id" in data:
Expand All @@ -258,8 +316,14 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_delete_perms):
raise ValidationError("mutation.authentication_required")

raise PermissionDenied(_("unauthorized"))

locations_id = list(Location.objects.filter(groups__id__in=data['ids']).values_list('id', flat=True))
if len(locations_id)>0 and not LocationManager().is_allowed(
user,
locations_id
):
raise PermissionDenied(_("unauthorized.location"))
@classmethod
def _mutate(cls, user, **data):
if "client_mutation_id" in data:
Expand All @@ -272,8 +336,8 @@ def _mutate(cls, user, **data):
ids = data.get('ids')
if ids:
with transaction.atomic():
for id in ids:
service.delete({'id': id, 'user': user})
for identifier in ids:
service.delete({'id': identifier})

class Input(OpenIMISMutation.Input):
ids = graphene.List(graphene.UUID)
Expand All @@ -289,7 +353,22 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_create_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))
group_location_id = Group.objects.get(id=data['group_id']).location_id
individual_location_id = Individual.objects.get(id=data['individual_id']).location_id
location_to_check = []
if group_location_id:
location_to_check.append(group_location_id)
if individual_location_id:
location_to_check.append(individual_location_id)
if len(location_to_check)>0 and not LocationManager().is_allowed(
user,
location_to_check
):
raise PermissionDenied(_("unauthorized.location"))

if group_location_id and individual_location_id and group_location_id != individual_location_id:
raise ValidationError(_("mutation.individual_group_location_mismatch"))

@classmethod
def _mutate(cls, user, **data):
Expand All @@ -316,7 +395,23 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_update_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))

group_location_id = Group.objects.get(id=data['group_id']).location_id
individual_location_id = Individual.objects.get(id=data['individual_id']).location_id
location_to_check = []
if individual_location_id:
location_to_check.append(individual_location_id)
if group_location_id:
location_to_check.append(group_location_id)
if len(location_to_check)>0 and not LocationManager().is_allowed(
user,
location_to_check
):
raise PermissionDenied(_("unauthorized.location"))

if group_location_id and individual_location_id and group_location_id != individual_location_id:
raise ValidationError(_("mutation.individual_group_location_mismatch"))

@classmethod
def _mutate(cls, user, **data):
Expand Down Expand Up @@ -346,8 +441,17 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_delete_perms):
raise ValidationError("mutation.authentication_required")

raise PermissionDenied(_("unauthorized"))
locations_qs = list(Location.objects.filter(
Q(groups__groupindividuals__id__in=data['ids'])|
Q(individuals__groupindividuals__id__in=data['ids'])
).values_list('id', flat=True))
# must first check if locations_qs exists in case none of the groups or individuals has location
if len(locations_qs)>0 and not LocationManager().is_allowed(
user,
locations_qs
):
raise PermissionDenied(_("unauthorized.location"))
@classmethod
def _mutate(cls, user, **data):
if "client_mutation_id" in data:
Expand All @@ -360,8 +464,8 @@ def _mutate(cls, user, **data):
ids = data.get('ids')
if ids:
with transaction.atomic():
for id in ids:
service.delete({'id': id, 'user': user})
for identifier in ids:
service.delete({'id': identifier})

class Input(OpenIMISMutation.Input):
ids = graphene.List(graphene.UUID)
Expand All @@ -377,7 +481,7 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_create_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))

@classmethod
def _mutate(cls, user, **data):
Expand Down Expand Up @@ -405,7 +509,7 @@ def _validate_mutation(cls, user, **data):

required_perms = IndividualConfig.gql_group_create_perms + IndividualConfig.gql_group_update_perms
if not user.has_perms(required_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))

@classmethod
def _mutate(cls, user, **data):
Expand Down Expand Up @@ -435,7 +539,7 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_create_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))

@classmethod
def _mutate(cls, user, **data):
Expand Down Expand Up @@ -469,7 +573,7 @@ def _validate_mutation(cls, user, **data):
super()._validate_mutation(user, **data)
if not user.has_perms(
IndividualConfig.gql_group_create_perms):
raise ValidationError("mutation.authentication_required")
raise PermissionDenied(_("unauthorized"))

@classmethod
def _mutate(cls, user, **data):
Expand Down
34 changes: 34 additions & 0 deletions individual/gql_queries.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import graphene
from django.contrib.auth.models import AnonymousUser
from graphene_django import DjangoObjectType
import graphene_django_optimizer as gql_optimizer

from core import prefix_filterset, ExtendedConnection
from core.gql_queries import UserGQLType
Expand Down Expand Up @@ -43,6 +44,10 @@ class Meta:
}
connection_class = ExtendedConnection

@classmethod
def get_queryset(cls, queryset, info):
return Individual.get_queryset(queryset, info.context.user)


class IndividualHistoryGQLType(DjangoObjectType):
uuid = graphene.String(source='uuid')
Expand All @@ -68,6 +73,13 @@ class Meta:
}
connection_class = ExtendedConnection

@classmethod
def get_queryset(cls, queryset, info):
accessible_individual_query = Individual.get_queryset(None, info.context.user)
accessible_individuals = gql_optimizer.query(accessible_individual_query, info)
accessible_uuids = set(accessible_individuals.values_list('uuid', flat=True))
return queryset.filter(id__in=accessible_uuids)


class IndividualDataSourceUploadGQLType(DjangoObjectType):
uuid = graphene.String(source='uuid')
Expand Down Expand Up @@ -132,6 +144,10 @@ class Meta:
}
connection_class = ExtendedConnection

@classmethod
def get_queryset(cls, queryset, info):
return Group.get_queryset(queryset, info.context.user)


class GroupHistoryGQLType(DjangoObjectType):
uuid = graphene.String(source='uuid')
Expand Down Expand Up @@ -161,6 +177,13 @@ class Meta:
}
connection_class = ExtendedConnection

@classmethod
def get_queryset(cls, queryset, info):
accessible_group_query = Group.get_queryset(None, info.context.user)
accessible_groups = gql_optimizer.query(accessible_group_query, info)
accessible_uuids = set(accessible_groups.values_list('uuid', flat=True))
return queryset.filter(id__in=accessible_uuids)


class GroupIndividualGQLType(DjangoObjectType):
uuid = graphene.String(source='uuid')
Expand All @@ -181,6 +204,10 @@ class Meta:
}
connection_class = ExtendedConnection

@classmethod
def get_queryset(cls, queryset, info):
return GroupIndividual.get_queryset(queryset, info.context.user)


class GroupIndividualHistoryGQLType(DjangoObjectType):
uuid = graphene.String(source='uuid')
Expand All @@ -205,6 +232,13 @@ class Meta:
}
connection_class = ExtendedConnection

@classmethod
def get_queryset(cls, queryset, info):
accessible_group_query = Group.get_queryset(None, info.context.user)
accessible_groups = gql_optimizer.query(accessible_group_query, info)
accessible_uuids = set(accessible_groups.values_list('uuid', flat=True))
return queryset.filter(group__id__in=accessible_uuids)


class IndividualDataUploadQGLType(DjangoObjectType, JsonExtMixin):
uuid = graphene.String(source='uuid')
Expand Down
Loading
Loading