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",