294 lines
11 KiB
Python
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())
|