This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
pii-manager-poc/manager/core/models.py

294 lines
11 KiB
Python

# pii manager - proof-of-concept implementation
# Copyright (C) 2021 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db import models
from django.utils.translation import ugettext_lazy as _
from phonenumber_field import modelfields
OAUTH = 'oauth'
SAML = 'saml'
REST = 'rest'
KERBEROS = 'kerberos'
SOURCE_TYPES = [(OAUTH, _('OAuth')), (SAML, _('SAML')), (REST, _('REST')), (KERBEROS, _('Kerberos'))]
class GenericSource(models.Model):
name = models.CharField(max_length=127, null=False, blank=False, verbose_name=_('source name'))
source_type = models.CharField(
max_length=15,
null=False,
blank=False,
default=OAUTH,
choices=SOURCE_TYPES,
verbose_name=_('source type'),
)
url = models.URLField(blank=False, null=False, unique=True, verbose_name=_('source url'))
class Meta:
abstract = True
class GenericProvider(models.Model):
name = models.CharField(max_length=127, null=False, blank=False, verbose_name=_('provider name'))
url = models.URLField(blank=False, null=False, unique=True, verbose_name=_('provider url'))
class Meta:
abstract = True
class OAuthSource(GenericSource):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.source_type = OAUTH
self.save()
class OAuthServer(GenericProvider):
pass
class RestSource(GenericSource):
def __init__(self, *args, **kwargs):
super().__init__(*arg, **kwargs)
self.source_type = REST
self.save()
class KerberosSource(GenericSource):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.source_type = KERBEROS
self.save()
class Address(models.Model):
street_number = models.CharField(max_length=31, blank=False, null=False, verbose_name=_('street number'))
street_name = models.CharField(max_length=255, blank=False, null=False, verbose_name=_('street name'))
zip_code = models.CharField(max_length=15, blank=False, null=False, verbose_name=_('zip code'))
city = models.CharField(max_length=127, blank=False, null=False, verbose_name=_('city'))
region = models.CharField(max_length=127, blank=False, null=False, verbose_name=_('region'))
country = models.CharField(max_length=127, blank=False, null=False, verbose_name=_('country'))
class PiiController(models.Model):
# xxx make it a many-to-one field to a pk model, see jwks for instance
public_key = models.CharField(max_length=8191, blank=True, null=True, verbose_name=_('public key'))
contact = models.CharField(
max_length=255, blank=False, null=False, verbose_name=_('contact name of the pii controller')
)
address = models.ForeignKey(
on_delete=models.CASCADE, to=Address, verbose_name=_('address of the pii controller')
)
email = models.EmailField(blank=False, null=False, verbose_name=_('pii controller contact email address'))
phone_number = modelfields.PhoneNumberField(
blank=False, null=False, verbose_name=_('pii controller contact phone number')
)
contact_url = models.URLField(blank=True, null=True, verbose_name=_('pii controller contact url'))
privacy_policy_url = models.URLField(
blank=False, null=False, verbose_name=_('link to the pii controller\'s privacy statement')
)
class Service(models.Model):
name = models.CharField(max_length=255, blank=False, null=False, verbose_name=_('name of the service'))
client_id = models.CharField(
max_length=255, blank=False, null=False, verbose_name=_('client identifier of the service')
)
client_secret = models.CharField(
max_length=255, blank=False, null=False, verbose_name=_('client secret of the service')
)
@classmethod
def create(cls, name):
instance = cls.objects.create(name=name)
# todo generate id/secret pair on client initialization
return instance
class AuthorizationCode(models.Model):
service = models.ForeignKey(
to=Service, verbose_name=_('authorization code for the service'), on_delete=models.CASCADE
)
value = models.CharField(
max_length=255, blank=False, null=False, verbose_name=_('value of the authorization code')
)
expires = models.DateTimeField(
blank=False, null=False, verbose_name=_('authorization code expiry timestamp')
) # todo default according to lifetime policy
def generate_token(self):
# todo create token fields, object initialization
pass
class Token(models.Model):
code = models.ForeignKey(to=AuthorizationCode, verbose_name=_('access token'), on_delete=models.CASCADE)
scopes = models.CharField(max_length=255, verbose_name=_('scopes for the access token'))
expires = models.DateTimeField(blank=False, null=False, verbose_name=_('expiration timestamp'))
audience = models.CharField(max_length=255, verbose_name=_('audience of the token'))
not_before = models.DateTimeField(blank=False, null=False, verbose_name=_('token not usable before'))
def serialize(self):
pass
@classmethod
def deserialize(cls, value):
pass
class Purpose(models.Model):
# for comprehensive list, see https://kantarainitiative.org/confluence/x/74K-BQ
CAT_CORE_FUNCTION = 'core function'
CAT_CONTRACTED_SERVICE = 'contracted service'
CAT_DELIVERY = 'delivery'
CAT_CONTACT_REQUESTED = 'contact requested'
CAT_PERSONALIZED_EXPERIENCE = 'personalized experience'
CAT_MARKETING = 'marketing'
CATEGORY_CHOICES = [
(CAT_CORE_FUNCTION, _('core function')),
(CAT_CONTRACTED_SERVICE, _('contracted service')),
(CAT_DELIVERY, _('delivery')),
(CAT_CONTACT_REQUESTED, _('contact requested')),
(CAT_PERSONALIZED_EXPERIENCE, _('personalized experience')),
(CAT_MARKETING, _('marketing')),
]
description = models.CharField(
max_length=255,
blank=False,
null=False,
verbose_name=_('short description of the pii collection purpose'),
)
category = models.CharField(
max_length=127, blank=False, null=False, default=CAT_CORE_FUNCTION, choices=CATEGORY_CHOICES
)
class PiiCategory(models.Model):
name = models.CharField(
max_length=127, blank=False, null=False, unique=True, verbose_name=_('target category of pii')
)
is_sensitive = models.BooleanField(default=False, verbose_name=_('sensitivity of pii'))
class TerminationPolicy(models.Model):
# xxx define clear categories
description = models.CharField(
max_length=255, blank=False, null=False, verbose_name=_('conditions for termination of consent')
)
class ConsentReceipt(models.Model):
TYPE_IMPLICIT = 'implicit'
TYPE_EXPLICIT = 'explicit'
TYPE_CHOICES = [(TYPE_IMPLICIT, _('implicit')), (TYPE_IMPLICIT, _('explicit'))]
'''
Mandatory consent receipt transaction fields
'''
version = models.CharField(max_length=31, blank=False, null=False, verbose_name=_('receipt version'))
jurisdiction = models.CharField(
max_length=255, blank=False, null=False, verbose_name=_('jurisdiction applying for the receipt')
)
consent_timestamp = models.DateTimeField(
auto_now_add=True, blank=False, null=False, verbose_name=_('consent timestamp')
)
# xxx which choices for the collection method?
collection_method = models.CharField(
max_length=127, blank=False, null=False, verbose_name=_('collection method')
)
# xxx uuid 4 default generator
receipt_id = models.CharField(
max_length=255,
blank=False,
null=False,
# default=utils.new_uuid4, # todo
verbose_name=('uuid4 identifier of the consent receipt'),
)
'''
Optional consent receipt transaction fields
'''
# xxx iso 639 choices and validator
language = models.CharField(
max_length=63, blank=True, null=True, verbose_name=_('language in which the consent was obtained')
)
'''
Mandatory consent transaction parties fields
'''
pii_principal_id = models.CharField(
max_length=255, blank=False, null=False, verbose_name=_('pii principal-provided identifier')
)
pii_controllers = models.ManyToManyField(to=PiiController, blank=True, related_name='consent_receipts')
pii_controller = models.ForeignKey(
to=PiiController,
verbose_name=_('fk to the first pii controller that collects the data'),
on_delete=models.CASCADE,
)
'''
Optional consent transaction parties fields
'''
on_behalf = models.BooleanField(
default=False, verbose_name=_('a pii processor acting on behalf of another processor or controller')
)
'''
Data, collection and use fields
'''
services = models.ManyToManyField(to=Service, blank=False, related_name='consent_receipts')
service = models.ForeignKey(
to=Service,
verbose_name=_('service or group of services for which PII is collected'),
on_delete=models.CASCADE,
)
purposes = models.ManyToManyField(to=Purpose, blank=False, related_name='consent_receipts')
purpose = models.ForeignKey(
to=Purpose, null=True, blank=True, verbose_name=_('purpose of collection'), on_delete=models.CASCADE
)
consent_type = models.CharField(
max_length=15, blank=False, null=False, default=TYPE_EXPLICIT, choices=TYPE_CHOICES
)
pii_categories = models.ManyToManyField(to=PiiCategory, blank=False, related_name='consent_receipts')
termination_policy = models.ForeignKey(
to=TerminationPolicy,
blank=False,
null=False,
verbose_name=_('termination policy'),
on_delete=models.CASCADE,
)
third_party_disclosure = models.BooleanField(
default=False, verbose_name=_('the pii controller is disclosing pii to a third party')
)
third_party_name = models.CharField(
max_length=255,
blank=False,
null=False,
verbose_name=_('name or names of the third party to which the controller may disclose the pii'),
)
@property
def sensitive_pii_categories(self):
return PiiCategory.objects.filter(consent_receipts__id=self.id, sensitivity=True)
@property
def sensitive_pii(self):
return bool(PiiCategory.objects.filter(consent_receipts__id=self.id, sensitivity=True).count())