-
Notifications
You must be signed in to change notification settings - Fork 4
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 location-based row level security to group and individual #121
Conversation
Aka GroupIndividual
Also refactor individual mutation & query test to extract setup code
I wonder about adding this row security in 'group' like: |
@sniedzielski I'm marking it as ready for review as this PR is already a bit too big. It covers row-level security changes for the models and corresponding CRUD operations. Kindly review please. I'll make separate PRs for the rest of the changes, tentatively one for csv import/export and another for enrollment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added some comments, please check them and give me you thoughts
@@ -13,12 +15,50 @@ class Individual(HistoryModel): | |||
#TODO WHY the HistoryModel json_ext was not enough | |||
json_ext = models.JSONField(db_column="Json_ext", blank=True, default=dict) | |||
|
|||
village = models.ForeignKey( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should not have location instead, related names should be 'individuals'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting point. I thought about this long and hard – the pro is the flexibility, but the con is also the flexibility. Say we go with location and allow it to be defined at any of the 4 geo levels, on the UI for creating & updating an individual/group, we will have the user select the location type, followed by the name. For individual import, we'd have two columns: location_type
and location_name
, instead of one village
. These are all ok.
Using location also allows individuals and groups to have different levels of granularity defined for them. Would this be good for data consistency from a record keeping point of view? e.g. one might end up with a database of 20k individuals' locations at district level while another 10k at village level – would this serve program administration well? @andreamartin @malike @amschel-de-r would be good to have your thoughts on this.
Another minor-ish point to consider – What would be the validation logic on the relationship between the individual.location and individual.group.location when they are both present? Should they match exactly? One contains another? Would there be enrollment implications if group and individual locations do not match exactly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah the concept of village as a geolocation is not available in all countries. The best approach will be to have labels which can be named at implementation level but tbh I don't know if its possible with the current openIMIS as the level of effort might be too big.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@malike the term "village" can be customized through translation on the UI. The decision point here is to either pin it down to the lowest geo level out of the 4 (most specific) or leave it flexible to allow it to be defined at any geo level for each individual/group.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be good practice that every instance has one geographic level that is required for all individuals, i.e. no location_type
flexibility. Having columns in data upload for location_type
and location_name
would add unnecessary confusion for users for all the countries I can think of. I would also be very surprised if there was a country that didn't have at least one geo level that all individuals have data on.
What I would say, is that it might be good for the level the mandatory field is at (i.e. geo_4, geo_3, geo_5) to be customizable in one place, maybe in core_moduleConfigurations or something. This would help the system fit both a larger country - where they may have complete data down to geo_6 level and users that need row-level security at geo_3 level - as well as a small island country - where they may only have 3 geo levels.
If we're talking edge cases, e.g. migrants, refugees and IDPs, I think we can encourage a practice where they allow for a catch-all value. E.g. if the geo levels are Region, District, Municipality and Village, and refugees/IDPs are handled at the Region level, then for each region you would create e.g. R1D_NA - no district, R1M_NA - no municipality, R1V_NA no village. Then in the data upload, you could set all the refugees/migrants in district 1 to have R1V_NA and they would be successfully handled (if I understand the feature).
As a side note, having either village
or location
as the mandatory column name would add confusion either way - do we have somewhere where we can customize a mapping between mandatory model fields and csv upload column names?
Open to comments - also have run this past Astrid as well
prefix='village__parent__parent', | ||
loc_types=['D'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will work fine but we might change it to support all locations to just prefix='location and no loc_types'
prefix='village__parent__parent', | |
loc_types=['D'] |
prefix='groupindividual__group__village__parent__parent', | ||
loc_types=['D'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefix='groupindividual__group__village__parent__parent', | |
loc_types=['D'] | |
prefix='groupindividual__group__location' |
village = models.ForeignKey( | ||
Location, | ||
models.DO_NOTHING, | ||
blank=True, | ||
null=True | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
village = models.ForeignKey( | |
Location, | |
models.DO_NOTHING, | |
blank=True, | |
null=True | |
) | |
location = models.ForeignKey( | |
Location, | |
models.DO_NOTHING, | |
blank=True, | |
null=True, | |
related_name="groups", | |
) |
village = models.ForeignKey( | ||
Location, | ||
models.DO_NOTHING, | ||
blank=True, | ||
null=True | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
village = models.ForeignKey( | |
Location, | |
models.DO_NOTHING, | |
blank=True, | |
null=True | |
) | |
location = models.ForeignKey( | |
Location, | |
models.DO_NOTHING, | |
blank=True, | |
null=True, | |
related_name="individuals", | |
) |
individual/gql_mutations.py
Outdated
@@ -140,6 +152,15 @@ def _validate_mutation(cls, user, **data): | |||
IndividualConfig.gql_individual_delete_perms): | |||
raise ValidationError("mutation.authentication_required") | |||
|
|||
villages_qs = Location.objects.filter(individual__id__in=data['ids'], type='V') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what you are doing here filtering Location with individual_id (for id you need only one _)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's for checking that the user has sufficient (geo) permission to delete the individual. See test case IndividualGQLMutationTest.test_delete_individual_row_security
.
Good spotting on the double _. Fixed.
@@ -55,6 +58,7 @@ def resolve_recipient_type(self, info): | |||
class CreateGroupInputType(OpenIMISMutation.Input): | |||
code = graphene.String(required=True) | |||
individuals_data = graphene.List(CreateGroupIndividualInputTypeInputObjectType, required=False) | |||
village_id = graphene.Int(required=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see comment on locations
individual/gql_mutations.py
Outdated
villages_qs = Location.objects.filter(individual__id__in=data['ids'], type='V') | ||
# must first check if villages_qs exists in case none of the individuals has location | ||
if villages_qs.exists(): | ||
allowed_loc_ids = Location.get_queryset(None, user).values('id') | ||
not_in_allowed = villages_qs.exclude(id__in=Subquery(allowed_loc_ids)) | ||
# all individuals' villages must be within permission for the given user | ||
if not allowed_loc_ids.exists() or not_in_allowed.exists(): | ||
raise ValidationError("mutation.authentication_required") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to be tested but you get the idea,
villages_qs = Location.objects.filter(individual__id__in=data['ids'], type='V') | |
# must first check if villages_qs exists in case none of the individuals has location | |
if villages_qs.exists(): | |
allowed_loc_ids = Location.get_queryset(None, user).values('id') | |
not_in_allowed = villages_qs.exclude(id__in=Subquery(allowed_loc_ids)) | |
# all individuals' villages must be within permission for the given user | |
if not allowed_loc_ids.exists() or not_in_allowed.exists(): | |
raise ValidationError("mutation.authentication_required") | |
individual_not_allowed = Individual.objects.filters( | |
id__in=data['ids'], | |
Q(~location__in=LocationManager().allowed(user=user)) | |
) | |
if individual_not_allowed: | |
raise ValidationError("mutation.authentication_required") |
individual/gql_mutations.py
Outdated
@@ -175,6 +196,15 @@ def _validate_mutation(cls, user, **data): | |||
IndividualConfig.gql_individual_undo_delete_perms): | |||
raise ValidationError("mutation.authentication_required") | |||
|
|||
villages_qs = Location.objects.filter(individual__id__in=data['ids'], type='V') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed unnecessary __
@@ -232,6 +266,9 @@ def _validate_mutation(cls, user, **data): | |||
if not user.has_perms( | |||
IndividualConfig.gql_group_update_perms): | |||
raise ValidationError("mutation.authentication_required") | |||
village = Group.objects.get(id=data['id']).village | |||
if village and village not in Location.get_queryset(None, user): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBC id allowed won't be better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean?
@delcroip I see you merged these changes into 24.10 release branch. Should 24.10 be merged to develop and this PR be discarded? |
Closing this as the change set is included in #130 |
Below is a full list of GQL queries and mutations. @sniedzielski do I need to add row security to all of them or are there ones not used by the frontend or not relevant in this context?