From 6e28a1050c8ca92c7e07700acea6fac6bfc3f146 Mon Sep 17 00:00:00 2001 From: Nico Virkki Date: Mon, 29 Apr 2024 09:12:21 +0300 Subject: [PATCH] feat: add middleware for checking allowed data fields When calling profile any kind there should be checked to which fields the service has access rights by using the allowed data fields. This adds middleware and mixin class for Profile model and VerifiedPersonalInfo models for checking that the queried fields are allowed for the service. Refs HP-2319 --- profiles/schema.py | 72 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/profiles/schema.py b/profiles/schema.py index d85dc49f..339214f2 100644 --- a/profiles/schema.py +++ b/profiles/schema.py @@ -44,6 +44,7 @@ ProfileMustHavePrimaryEmailError, ServiceConnectionDoesNotExist, ServiceDoesNotExist, + ServiceNotIdentifiedError, TokenExpiredError, ) from open_city_profile.graphene import UUIDMultipleChoiceFilter @@ -370,6 +371,46 @@ def filter_by_nin_exact(self, queryset, name, value): return queryset.none() +class AllowedDataFieldsMixin: + """ + Mixin class for checking allowed data fields per service. + + `allowed_data_fields_map` is a dictionary where the key is the `field_name` of the allowed data field + `allowed_data_fields.json` and the value is an iterable of django model's field names that the `field_name` + describes. For example, if the `field_name` is `name`, the value could be `("first_name", "last_name")`. + e.g: + allowed_data_fields_map = { + "name": ("first_name", "last_name", "nickname"), + "personalidentitycode": ("national_identification_number",), + "address": ("address", "postal_code", "city", "country_code") + } + + `always_allow_fields`: Since connections are not defined in `allowed_data_fields.json` they should be + defined here. If the field is connection and the node does not inherit this mixin the data will be available + to all services. + """ + + allowed_data_fields_map = {} + always_allow_fields = ["id", "service_connections"] + check_allowed_data_fields = True + + @classmethod + def is_field_allowed_for_service(cls, field_name: str, service: Service): + if not service: + raise ServiceNotIdentifiedError("No service identified") + + if field_name in cls.always_allow_fields: + return True + + allowed_data_fields = service.allowed_data_fields.values_list( + "field_name", flat=True + ) + return any( + field_name in cls.allowed_data_fields_map.get(allowed_data_field, []) + for allowed_data_field in allowed_data_fields + ) + + class ContactNode(DjangoObjectType): class Meta: model = Contact @@ -447,7 +488,7 @@ class Meta: fields = ("street_address", "additional_address", "country_code") -class VerifiedPersonalInformationNode(DjangoObjectType): +class VerifiedPersonalInformationNode(DjangoObjectType, AllowedDataFieldsMixin): class Meta: model = VerifiedPersonalInformation fields = ( @@ -459,6 +500,18 @@ class Meta: "municipality_of_residence_number", ) + allowed_data_fields_map = { + "name": ("first_name", "last_name", "given_name"), + "personalidentitycode": ("national_identification_number",), + "address": ( + "municipality_of_residence", + "municipality_of_residence_number", + "permanent_address", + "temporary_address", + "permanent_foreign_address", + ), + } + # Need to set the national_identification_number field explicitly as non-null # because django-searchable-encrypted-fields SearchFields are always nullable # and you can't change it. @@ -567,7 +620,7 @@ def resolve_addresses(self: Profile, info, **kwargs): @key(fields="id") -class ProfileNode(RestrictedProfileNode): +class ProfileNode(RestrictedProfileNode, AllowedDataFieldsMixin): class Meta: model = Profile fields = ("first_name", "last_name", "nickname", "language") @@ -575,6 +628,21 @@ class Meta: connection_class = ProfilesConnection filterset_class = ProfileFilter + allowed_data_fields_map = { + "name": ( + "first_name", + "last_name", + "nickname", + ), + "email": ("emails", "primary_email"), + "phone": ("phones", "primary_phone"), + "address": ("addresses", "primary_address"), + "personalidentitycode": ("sensitivedata",), + } + always_allow_fields = AllowedDataFieldsMixin.always_allow_fields + [ + "verified_personal_information" + ] + sensitivedata = graphene.Field( SensitiveDataNode, description="Data that is consider to be sensitive e.g. social security number",