initial commit, draft support for oauth and rest sources
This commit is contained in:
commit
a0391b2bdd
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
black --target-version py37 --skip-string-normalization --line-length 110 .
|
||||
|
||||
isort --profile black --line-length 110 .
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Get venv site-packages path
|
||||
DSTDIR=`python3 -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())'`
|
||||
|
||||
# Get not venv site-packages path
|
||||
# Remove first path (assuming that is the venv path)
|
||||
NONPATH=`echo $PATH | sed 's/^[^:]*://'`
|
||||
SRCDIR=`PATH=$NONPATH python3 -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())'`
|
||||
|
||||
# Clean up
|
||||
rm -f $DSTDIR/lasso.*
|
||||
rm -f $DSTDIR/_lasso.*
|
||||
|
||||
# Link
|
||||
ln -sv /usr/lib/python3/dist-packages/lasso.py $DSTDIR/
|
||||
for SOFILE in /usr/lib/python3/dist-packages/_lasso.cpython-*.so
|
||||
do
|
||||
ln -sv $SOFILE $DSTDIR/
|
||||
done
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,281 @@
|
|||
# pii manager poc french l10n
|
||||
# Copyright (C) 2021
|
||||
# This file is distributed under the same license as the pii-manager-poc package.
|
||||
# Paul Marillonnet <pmarillonnet@entrouvert.com>, 2021.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: pii manager poc\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-29 15:14+0000\n"
|
||||
"PO-Revision-Date: 2021-04-29 17:02+0200\n"
|
||||
"Last-Translator: Paul Marillonnet <pmarillonnet@entrouvert.com>\n"
|
||||
"Language-Team: None\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: manager/core/models.py:29
|
||||
msgid "OAuth"
|
||||
msgstr "OAuth"
|
||||
|
||||
#: manager/core/models.py:30
|
||||
msgid "SAML"
|
||||
msgstr "SAML"
|
||||
|
||||
#: manager/core/models.py:31
|
||||
msgid "REST"
|
||||
msgstr "REST"
|
||||
|
||||
#: manager/core/models.py:32
|
||||
msgid "Kerberos"
|
||||
msgstr "Kerberos"
|
||||
|
||||
#: manager/core/models.py:41
|
||||
msgid "source name"
|
||||
msgstr "nom de la source"
|
||||
|
||||
#: manager/core/models.py:48
|
||||
msgid "source type"
|
||||
msgstr "type de source"
|
||||
|
||||
#: manager/core/models.py:53
|
||||
msgid "source url"
|
||||
msgstr "url de la source"
|
||||
|
||||
#: manager/core/models.py:64
|
||||
msgid "provider name"
|
||||
msgstr "nom du fournisseur"
|
||||
|
||||
#: manager/core/models.py:69
|
||||
msgid "provider url"
|
||||
msgstr "url du fournisseur"
|
||||
|
||||
#: manager/core/models.py:108
|
||||
msgid "street number"
|
||||
msgstr "numéro de rue"
|
||||
|
||||
#: manager/core/models.py:113
|
||||
msgid "street name"
|
||||
msgstr "nom de rue"
|
||||
|
||||
#: manager/core/models.py:118
|
||||
msgid "zip code"
|
||||
msgstr "code postal"
|
||||
|
||||
#: manager/core/models.py:123
|
||||
msgid "city"
|
||||
msgstr "ville"
|
||||
|
||||
#: manager/core/models.py:128
|
||||
msgid "region"
|
||||
msgstr "région"
|
||||
|
||||
#: manager/core/models.py:133
|
||||
msgid "country"
|
||||
msgstr "pays"
|
||||
|
||||
#: manager/core/models.py:142
|
||||
msgid "public key"
|
||||
msgstr "clé publique"
|
||||
|
||||
#: manager/core/models.py:147
|
||||
msgid "contact name of the pii controller"
|
||||
msgstr "nom du contact du responsable de traitement"
|
||||
|
||||
#: manager/core/models.py:151
|
||||
msgid "address of the pii controller"
|
||||
msgstr "adresse du responsable de traitement"
|
||||
|
||||
#: manager/core/models.py:155
|
||||
msgid "pii controller contact email address"
|
||||
msgstr "adresse courriel de contact du responsable de traitement"
|
||||
|
||||
#: manager/core/models.py:159
|
||||
msgid "pii controller contact phone number"
|
||||
msgstr "numéro de téléphone de contact du responsable de traitement"
|
||||
|
||||
#: manager/core/models.py:163
|
||||
msgid "pii controller contact url"
|
||||
msgstr "url de contact du responsable de traitement"
|
||||
|
||||
#: manager/core/models.py:167
|
||||
msgid "link to the pii controller's privacy statement"
|
||||
msgstr "lien vers la politique de gestion des données personnelles du responsable de traitement"
|
||||
|
||||
#: manager/core/models.py:175
|
||||
msgid "name of the service"
|
||||
msgstr "nom du service"
|
||||
|
||||
#: manager/core/models.py:180
|
||||
msgid "client identifier of the service"
|
||||
msgstr "identifiant du service (client id)"
|
||||
|
||||
#: manager/core/models.py:185
|
||||
msgid "client secret of the service"
|
||||
msgstr "secret du service (client secret)"
|
||||
|
||||
#: manager/core/models.py:197
|
||||
msgid "authorization code for the service"
|
||||
msgstr "code d’autorisation pour le service"
|
||||
|
||||
#: manager/core/models.py:203
|
||||
msgid "value of the authorization code"
|
||||
msgstr "valeur du code d’autorisation"
|
||||
|
||||
#: manager/core/models.py:207
|
||||
msgid "authorization code expiry timestamp"
|
||||
msgstr "horodate d’expiration du code d’autorisation"
|
||||
|
||||
#: manager/core/models.py:217
|
||||
msgid "access token"
|
||||
msgstr "jeton d’accès"
|
||||
|
||||
#: manager/core/models.py:221
|
||||
msgid "scopes for the access token"
|
||||
msgstr "portées du jeton d’accès"
|
||||
|
||||
#: manager/core/models.py:225
|
||||
msgid "expiration timestamp"
|
||||
msgstr "horodate d’expiration"
|
||||
|
||||
#: manager/core/models.py:228
|
||||
msgid "audience of the token"
|
||||
msgstr "audience du jeton"
|
||||
|
||||
#: manager/core/models.py:232
|
||||
msgid "token not usable before"
|
||||
msgstr "jeton inutilisable avant le"
|
||||
|
||||
#: manager/core/models.py:251
|
||||
msgid "core function"
|
||||
msgstr "fonction cœur"
|
||||
|
||||
#: manager/core/models.py:252
|
||||
msgid "contracted service"
|
||||
msgstr "service en contrat"
|
||||
|
||||
#: manager/core/models.py:253
|
||||
msgid "delivery"
|
||||
msgstr "livraison"
|
||||
|
||||
#: manager/core/models.py:254
|
||||
msgid "contact requested"
|
||||
msgstr "contact demandé"
|
||||
|
||||
#: manager/core/models.py:255
|
||||
msgid "personalized experience"
|
||||
msgstr "expérience personnalisée"
|
||||
|
||||
#: manager/core/models.py:256
|
||||
msgid "marketing"
|
||||
msgstr "marketing"
|
||||
|
||||
#: manager/core/models.py:263
|
||||
msgid "short description of the pii collection purpose"
|
||||
msgstr "description brève de la finalité de collecte des données"
|
||||
|
||||
#: manager/core/models.py:278
|
||||
msgid "target category of pii"
|
||||
msgstr "catégorie cible de données"
|
||||
|
||||
#: manager/core/models.py:281
|
||||
msgid "sensitivity of pii"
|
||||
msgstr "niveau de sensibilité des données"
|
||||
|
||||
#: manager/core/models.py:290
|
||||
msgid "conditions for termination of consent"
|
||||
msgstr "conditions de terminaison du consentement"
|
||||
|
||||
#: manager/core/models.py:298
|
||||
msgid "implicit"
|
||||
msgstr "implicite"
|
||||
|
||||
#: manager/core/models.py:299
|
||||
msgid "explicit"
|
||||
msgstr "explicite"
|
||||
|
||||
#: manager/core/models.py:309
|
||||
msgid "receipt version"
|
||||
msgstr "version de reçu"
|
||||
|
||||
#: manager/core/models.py:314
|
||||
msgid "jurisdiction applying for the receipt"
|
||||
msgstr "juridiction d’application du reçu"
|
||||
|
||||
#: manager/core/models.py:319
|
||||
msgid "consent timestamp"
|
||||
msgstr "horodate du consentement"
|
||||
|
||||
#: manager/core/models.py:325
|
||||
msgid "collection method"
|
||||
msgstr "méthode de collecte"
|
||||
|
||||
#: manager/core/models.py:342
|
||||
msgid "language in which the consent was obtained"
|
||||
msgstr "langue dans laquelle le consentement a été obtenu"
|
||||
|
||||
#: manager/core/models.py:351
|
||||
msgid "pii principal-provided identifier"
|
||||
msgstr "identifiant des données fournies par le sujet"
|
||||
|
||||
#: manager/core/models.py:358
|
||||
msgid "fk to the first pii controller that collects the data"
|
||||
msgstr "clé étrangère vers le premier responsable de traitement ayant collecté les donées"
|
||||
|
||||
#: manager/core/models.py:366
|
||||
msgid "a pii processor acting on behalf of another processor or controller"
|
||||
msgstr "au moins un des responsables de traitement agit pour le compte d’un autre responsable de traitement"
|
||||
|
||||
#: manager/core/models.py:377
|
||||
msgid "service or group of services for which PII is collected"
|
||||
msgstr "service ou groupe de services pour le(s)quel(s) les données sont collectées"
|
||||
|
||||
#: manager/core/models.py:387
|
||||
msgid "purpose of collection"
|
||||
msgstr "finalité de la collecte"
|
||||
|
||||
#: manager/core/models.py:403
|
||||
msgid "termination policy"
|
||||
msgstr "politique de terminaison"
|
||||
|
||||
#: manager/core/models.py:407
|
||||
msgid "the pii controller is disclosing pii to a third party"
|
||||
msgstr "le responsable de traitement divulgue les données à un tiers-parti"
|
||||
|
||||
#: manager/core/models.py:412
|
||||
msgid ""
|
||||
"name or names of the third party to which the controller may disclose the pii"
|
||||
msgstr ""
|
||||
"nom ou noms du tiers-parti auquel le responsable de traitement divulgue les données"
|
||||
|
||||
#: manager/core/templates/manager/core/authorize.html:7
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" <p>The service %(service)s is requesting access to scopes %(scopes)s "
|
||||
"on your personal information.</p>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" <p> Le service %(service)s demande l’accès aux portées %(scopes)s "
|
||||
"concernant vos données personnelles.</p>\n"
|
||||
" "
|
||||
|
||||
#: manager/core/templates/manager/core/authorize.html:16
|
||||
msgid "Choose"
|
||||
msgstr "Effectuer ce choix"
|
||||
|
||||
#: manager/core/templates/manager/core/authorize.html:17
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: manager/templates/manager/root.html:28
|
||||
msgid "Logout"
|
||||
msgstr "Déconnexion"
|
||||
|
||||
#: manager/templates/manager/root.html:49
|
||||
msgid "Homepage"
|
||||
msgstr "Page d’accueil"
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manager.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
raise
|
||||
execute_from_command_line(sys.argv)
|
|
@ -0,0 +1,58 @@
|
|||
# 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.contrib import admin
|
||||
|
||||
from manager.core import models
|
||||
|
||||
|
||||
class OAuthSourceAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'url']
|
||||
list_filter = ['name']
|
||||
|
||||
|
||||
class RestSourceAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'url']
|
||||
list_filter = ['name']
|
||||
|
||||
|
||||
class KerberosSourceAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'url']
|
||||
list_filter = ['name']
|
||||
|
||||
|
||||
class AddressAdmin(admin.ModelAdmin):
|
||||
list_display = ['street_number', 'street_name', 'zip_code', 'city', 'region', 'country']
|
||||
|
||||
|
||||
class PiiControllerAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'public_key',
|
||||
'contact',
|
||||
'address',
|
||||
'email',
|
||||
'phone_number',
|
||||
'contact_uri',
|
||||
'privacy_policy_url',
|
||||
]
|
||||
list_filter = ['contact']
|
||||
|
||||
|
||||
admin.site.register(models.OAuthSource, OAuthSourceAdmin)
|
||||
admin.site.register(models.RestSource, RestSourceAdmin)
|
||||
admin.site.register(models.KerberosSource, KerberosSourceAdmin)
|
||||
admin.site.register(models.Address, AddressAdmin)
|
||||
admin.site.register(models.PiiController, PiiControllerAdmin)
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = 'core'
|
|
@ -0,0 +1,23 @@
|
|||
# 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/>.
|
||||
|
||||
|
||||
class AuthzError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PiiRetrievalError(Exception):
|
||||
pass
|
|
@ -0,0 +1,15 @@
|
|||
# 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/>.
|
|
@ -0,0 +1,397 @@
|
|||
# Generated by Django 2.2.20 on 2021-04-29 13:40
|
||||
|
||||
import django.db.models.deletion
|
||||
import phonenumber_field.modelfields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Address',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('street_number', models.CharField(max_length=31, verbose_name='street number')),
|
||||
('street_name', models.CharField(max_length=255, verbose_name='street name')),
|
||||
('zip_code', models.CharField(max_length=15, verbose_name='zip code')),
|
||||
('city', models.CharField(max_length=127, verbose_name='city')),
|
||||
('region', models.CharField(max_length=127, verbose_name='region')),
|
||||
('country', models.CharField(max_length=127, verbose_name='country')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AuthorizationCode',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('value', models.CharField(max_length=255, verbose_name='value of the authorization code')),
|
||||
('expires', models.DateTimeField(verbose_name='authorization code expiry timestamp')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='KerberosSource',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('name', models.CharField(max_length=127, verbose_name='source name')),
|
||||
(
|
||||
'source_type',
|
||||
models.CharField(
|
||||
choices=[
|
||||
('oauth', 'OAuth'),
|
||||
('saml', 'SAML'),
|
||||
('rest', 'REST'),
|
||||
('kerberos', 'Kerberos'),
|
||||
],
|
||||
default='oauth',
|
||||
max_length=15,
|
||||
verbose_name='source type',
|
||||
),
|
||||
),
|
||||
('url', models.URLField(unique=True, verbose_name='source url')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OAuthServer',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('name', models.CharField(max_length=127, verbose_name='provider name')),
|
||||
('url', models.URLField(unique=True, verbose_name='provider url')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OAuthSource',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('name', models.CharField(max_length=127, verbose_name='source name')),
|
||||
(
|
||||
'source_type',
|
||||
models.CharField(
|
||||
choices=[
|
||||
('oauth', 'OAuth'),
|
||||
('saml', 'SAML'),
|
||||
('rest', 'REST'),
|
||||
('kerberos', 'Kerberos'),
|
||||
],
|
||||
default='oauth',
|
||||
max_length=15,
|
||||
verbose_name='source type',
|
||||
),
|
||||
),
|
||||
('url', models.URLField(unique=True, verbose_name='source url')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PiiCategory',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
(
|
||||
'name',
|
||||
models.CharField(max_length=127, unique=True, verbose_name='target category of pii'),
|
||||
),
|
||||
('is_sensitive', models.BooleanField(default=False, verbose_name='sensitivity of pii')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Purpose',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
(
|
||||
'description',
|
||||
models.CharField(
|
||||
max_length=255, verbose_name='short description of the pii collection purpose'
|
||||
),
|
||||
),
|
||||
(
|
||||
'category',
|
||||
models.CharField(
|
||||
choices=[
|
||||
('core function', 'core function'),
|
||||
('contracted service', 'contracted service'),
|
||||
('delivery', 'delivery'),
|
||||
('contact requested', 'contact requested'),
|
||||
('personalized experience', 'personalized experience'),
|
||||
('marketing', 'marketing'),
|
||||
],
|
||||
default='core function',
|
||||
max_length=127,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RestSource',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('name', models.CharField(max_length=127, verbose_name='source name')),
|
||||
(
|
||||
'source_type',
|
||||
models.CharField(
|
||||
choices=[
|
||||
('oauth', 'OAuth'),
|
||||
('saml', 'SAML'),
|
||||
('rest', 'REST'),
|
||||
('kerberos', 'Kerberos'),
|
||||
],
|
||||
default='oauth',
|
||||
max_length=15,
|
||||
verbose_name='source type',
|
||||
),
|
||||
),
|
||||
('url', models.URLField(unique=True, verbose_name='source url')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Service',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('name', models.CharField(max_length=255, verbose_name='name of the service')),
|
||||
(
|
||||
'client_id',
|
||||
models.CharField(max_length=255, verbose_name='client identifier of the service'),
|
||||
),
|
||||
(
|
||||
'client_secret',
|
||||
models.CharField(max_length=255, verbose_name='client secret of the service'),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TerminationPolicy',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
(
|
||||
'description',
|
||||
models.CharField(max_length=255, verbose_name='conditions for termination of consent'),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Token',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('scopes', models.CharField(max_length=255, verbose_name='scopes for the access token')),
|
||||
('expires', models.DateTimeField(verbose_name='expiration timestamp')),
|
||||
('audience', models.CharField(max_length=255, verbose_name='audience of the token')),
|
||||
('not_before', models.DateTimeField(verbose_name='token not usable before')),
|
||||
(
|
||||
'code',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='core.AuthorizationCode',
|
||||
verbose_name='access token',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PiiController',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
(
|
||||
'public_key',
|
||||
models.CharField(blank=True, max_length=8191, null=True, verbose_name='public key'),
|
||||
),
|
||||
(
|
||||
'contact',
|
||||
models.CharField(max_length=255, verbose_name='contact name of the pii controller'),
|
||||
),
|
||||
(
|
||||
'email',
|
||||
models.EmailField(max_length=254, verbose_name='pii controller contact email address'),
|
||||
),
|
||||
(
|
||||
'phone_number',
|
||||
phonenumber_field.modelfields.PhoneNumberField(
|
||||
max_length=128, region=None, 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(verbose_name="link to the pii controller's privacy statement"),
|
||||
),
|
||||
(
|
||||
'address',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='core.Address',
|
||||
verbose_name='address of the pii controller',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConsentReceipt',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('version', models.CharField(max_length=31, verbose_name='receipt version')),
|
||||
(
|
||||
'jurisdiction',
|
||||
models.CharField(max_length=255, verbose_name='jurisdiction applying for the receipt'),
|
||||
),
|
||||
(
|
||||
'consent_timestamp',
|
||||
models.DateTimeField(auto_now_add=True, verbose_name='consent timestamp'),
|
||||
),
|
||||
('collection_method', models.CharField(max_length=127, verbose_name='collection method')),
|
||||
(
|
||||
'receipt_id',
|
||||
models.CharField(max_length=255, verbose_name='uuid4 identifier of the consent receipt'),
|
||||
),
|
||||
(
|
||||
'language',
|
||||
models.CharField(
|
||||
blank=True,
|
||||
max_length=63,
|
||||
null=True,
|
||||
verbose_name='language in which the consent was obtained',
|
||||
),
|
||||
),
|
||||
(
|
||||
'pii_principal_id',
|
||||
models.CharField(max_length=255, verbose_name='pii principal-provided identifier'),
|
||||
),
|
||||
(
|
||||
'on_behalf',
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
verbose_name='a pii processor acting on behalf of another processor or controller',
|
||||
),
|
||||
),
|
||||
(
|
||||
'consent_type',
|
||||
models.CharField(
|
||||
choices=[('implicit', 'implicit'), ('implicit', 'explicit')],
|
||||
default='explicit',
|
||||
max_length=15,
|
||||
),
|
||||
),
|
||||
(
|
||||
'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,
|
||||
verbose_name='name or names of the third party to which the controller may disclose the pii',
|
||||
),
|
||||
),
|
||||
(
|
||||
'pii_categories',
|
||||
models.ManyToManyField(related_name='consent_receipts', to='core.PiiCategory'),
|
||||
),
|
||||
(
|
||||
'pii_controller',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='core.PiiController',
|
||||
verbose_name='fk to the first pii controller that collects the data',
|
||||
),
|
||||
),
|
||||
(
|
||||
'pii_controllers',
|
||||
models.ManyToManyField(
|
||||
blank=True, related_name='consent_receipts', to='core.PiiController'
|
||||
),
|
||||
),
|
||||
(
|
||||
'purpose',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='core.Purpose',
|
||||
verbose_name='purpose of collection',
|
||||
),
|
||||
),
|
||||
('purposes', models.ManyToManyField(related_name='consent_receipts', to='core.Purpose')),
|
||||
(
|
||||
'service',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='core.Service',
|
||||
verbose_name='service or group of services for which PII is collected',
|
||||
),
|
||||
),
|
||||
('services', models.ManyToManyField(related_name='consent_receipts', to='core.Service')),
|
||||
(
|
||||
'termination_policy',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='core.TerminationPolicy',
|
||||
verbose_name='termination policy',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='authorizationcode',
|
||||
name='service',
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='core.Service',
|
||||
verbose_name='authorization code for the service',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,293 @@
|
|||
# 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())
|
|
@ -0,0 +1,85 @@
|
|||
# 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/>.
|
||||
|
||||
|
||||
class BaseBackend(object):
|
||||
'''
|
||||
Base class for the PII Manager's Source Backend
|
||||
'''
|
||||
|
||||
interactive = False
|
||||
|
||||
def __init__(self, source, *args, **kwargs):
|
||||
self.source = source
|
||||
|
||||
def get_resource(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class OAuthBackend(BaseBackend):
|
||||
'''
|
||||
Minimalistic OAuth 2.0 client for the PII Manager's Source Backend
|
||||
Supported grant types are:
|
||||
- authorization code (https://tools.ietf.org/html/rfc6749#section-4.1)
|
||||
- implicit (https://tools.ietf.org/html/rfc6749#section-4.2)
|
||||
'''
|
||||
|
||||
# user interactions are required
|
||||
interactive = True
|
||||
|
||||
def __init__(self, source, *args, **kwargs):
|
||||
super().__init__(source, *args, **kwargs)
|
||||
self.get_authorization()
|
||||
|
||||
def get_authorization(self):
|
||||
# required in authorization code grant only
|
||||
pass
|
||||
|
||||
def get_token(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class RestBackend(BaseBackend):
|
||||
'''
|
||||
REST support class for the PII Manager's Source Backend
|
||||
'''
|
||||
|
||||
def __init__(self, source, *args, **kwargs):
|
||||
super().__init__(source, *args, **kwargs)
|
||||
self.authenticate()
|
||||
|
||||
def authenticate(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def get_credentials(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class SamlBackend(BaseBackend):
|
||||
'''
|
||||
TODO Not supported yet
|
||||
'''
|
||||
|
||||
# user interactions are required, no backchannel pii retrieval
|
||||
interactive = True
|
||||
|
||||
|
||||
class KerberosBackend(BaseBackend):
|
||||
'''
|
||||
TODO Not supported yet
|
||||
'''
|
||||
|
||||
pass
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "manager/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div id="manager-core-authorize">
|
||||
{% block form-intro %}
|
||||
{% blocktrans %}
|
||||
<p>The service {{ service }} is requesting access to scopes {{ scopes }} on your personal information.</p>
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
{% block form %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="buttons">
|
||||
<button name="submit">{% trans "Choose" %}</button>
|
||||
<button name="cancel">{% trans "Cancel" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,28 @@
|
|||
# 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.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
|
||||
from manager.core import views
|
||||
|
||||
# from manager.core.api_views import router # todo
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^resource/(?P<resource_id>[A-Za-z0-9_ -]+)/$', views.resource, name='resource_endpoint'),
|
||||
# url(r'^api/', include(router.urls)), # todo
|
||||
# url(r'^api/', include('manager.core.api_urls')), # todo
|
||||
]
|
|
@ -0,0 +1,40 @@
|
|||
# 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/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from manager.core.exceptions import AuthzError
|
||||
from manager.core.models import AuthorizationCode, Token
|
||||
|
||||
|
||||
def generate_uuid(value=None):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def is_token_valid(authz_code, token):
|
||||
try:
|
||||
if not isinstance(authz_code, AuthorizationCode):
|
||||
raise AuthzError('code must be an AuthorizationCode object')
|
||||
if not isinstance(token, (str, Token)):
|
||||
raise AuthzError('token must either be str or Token object')
|
||||
if isinstance(token, str):
|
||||
token = Token.deserialize(token)
|
||||
# todo check scopes
|
||||
# todo check audience
|
||||
# todo check expiry and not_before
|
||||
# todo check code
|
||||
except AuthzError:
|
||||
return False
|
|
@ -0,0 +1,186 @@
|
|||
# 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.core.exceptions import MultipleObjectsReturned
|
||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.encoding import force_text
|
||||
from django.views.generic import FormView, TemplateView, View
|
||||
from jwcrypto.common import JWException
|
||||
from jwcrypto.jwk import JWK
|
||||
from jwcrypto.jwt import JWT
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from manager.core.models import AuthorizationCode, GenericProvider, GenericSource
|
||||
|
||||
|
||||
class ResourceView(APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
resource = {}
|
||||
# 1. retrieve access token
|
||||
token = request.GET.get('token', '')
|
||||
header, payload, sig, *_ = token.split('.')
|
||||
# 2. local oken verification
|
||||
try:
|
||||
token = JWT() # todo
|
||||
token.verify(sig)
|
||||
except JWException as e:
|
||||
return Response(request, {'err': 'invalid_token'}, status=403)
|
||||
|
||||
try:
|
||||
source = GenericProvider.objects.get(url=JWT.fields.provider_uri)
|
||||
except GenericProvider.DoesNotExist as e:
|
||||
return Response(request, {'err': 'unknown_provider'}, status=403)
|
||||
except MultipleObjectsReturned as e:
|
||||
return Response(request, {'err': 'invalid_provider'}, status=403)
|
||||
|
||||
resource_id = request.GET.get('resource_id')
|
||||
user_id = request.GET.get('pii_principal_id')
|
||||
if not resource_id:
|
||||
return Response(request, {'err': 'unknown_resource'}, status=404)
|
||||
if not user_id:
|
||||
return Response(request, {'err': 'invalid_pii_principal_id'}, status=403)
|
||||
consent_receipts = ConsentReceipt.objects.filter(pii_identifier=resource_id, pii_principal_id=user_id)
|
||||
requested_scopes = request.GET.get('scopes', '').split()
|
||||
|
||||
if not consent_receipts:
|
||||
# authorization required, user interaction needed
|
||||
# xxx not a redirect, fail and mention authorization url
|
||||
return Response(
|
||||
request,
|
||||
{'err': 'user_authorization_required'},
|
||||
url=authorization_url,
|
||||
scopes=requested_scopes,
|
||||
status=403,
|
||||
)
|
||||
|
||||
# if not request.GET.get('user_id') -> error
|
||||
previously_granted_scopes = []
|
||||
for receipt in consent_receipts:
|
||||
previously_granted_scopes.append(consent_receipts.scopes_set.split())
|
||||
|
||||
# 3. online verification?
|
||||
if is_online_verifiable(service):
|
||||
is_verified = perform_online_verification(token, service)
|
||||
if not is_verified:
|
||||
return Response(
|
||||
request,
|
||||
{'err': 'invalid_token'}, # xxx mention that online verification has been performed
|
||||
status=403,
|
||||
)
|
||||
|
||||
if diff_scopes := set(requested_scopes) - set(previously_granted_scopes):
|
||||
# todo broken here
|
||||
# uma logic and scope reduction algo
|
||||
# 3.a. Scope translation
|
||||
# We need to define a new mapping object, preferably model
|
||||
# 3.b. Set inclusion in service's authorized scopes in stored CRs
|
||||
# Check if compatible with our current CR model
|
||||
# 3.c. Perform No/maybe decision
|
||||
if not perform_uma_decision(service):
|
||||
return redirect(request, authorization_url, scopes=diff_scopes)
|
||||
else:
|
||||
requested_scopes = m
|
||||
# 4. reverse scope reduction
|
||||
# 4.a. retrieve source(s) hosting pii
|
||||
# 4.b. call each target source's backend
|
||||
# 4.c. return available pii to the caller
|
||||
|
||||
return Response(
|
||||
{
|
||||
'err': 0,
|
||||
'data': resources,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
resource = ResourceView.as_view()
|
||||
|
||||
|
||||
def AuthorizeView(TemplateView):
|
||||
template = 'manager/core/authorize.html'
|
||||
success_url = '/'
|
||||
|
||||
def redirect(self, **kwargs):
|
||||
return HttpResponseRedirect(make_url(self.redirect_uri, **kwargs))
|
||||
|
||||
def dispatch(self, request):
|
||||
# mandatory request parameters
|
||||
self.redirect_uri = request.GET.get('redirect_uri')
|
||||
if not self.redirect_uri:
|
||||
return HttpResponseBadRequest('missing redirect_uri parameter')
|
||||
|
||||
client_id = request.GET.get('client_id')
|
||||
if not client_id:
|
||||
return HttpResponseBadRequest('missing client_id parameter')
|
||||
|
||||
response_type = request.GET.get('response_type')
|
||||
if not response_type:
|
||||
return HttpResponseBadRequest('missing response_type parameter')
|
||||
|
||||
try:
|
||||
self.service = Service.objects.get(client_id=client_id)
|
||||
# todo check redirect uri against client's uris
|
||||
except Service.DoesNotExist as e:
|
||||
return self.redirect(error='unauthorized_client')
|
||||
self.state = request.GET.get('state')
|
||||
|
||||
return super().dispatch(request)
|
||||
|
||||
def form_valid(self, request):
|
||||
scopes = request.POST.get('scopes')
|
||||
# xxx required claims from scopes?
|
||||
# 1. create consent receipt
|
||||
# 2. generate authorization code
|
||||
return HttpResponseRedirect(self.redirect_uri, code=authorization_code, state=self.state)
|
||||
|
||||
def post(self, request):
|
||||
if 'cancel' in request.POST:
|
||||
return self.redirect(error='access_denied')
|
||||
return super().post(request)
|
||||
|
||||
# todo get_form_kwargs, user details?
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['service'] = self.service
|
||||
# todo display scopes?
|
||||
return ctx
|
||||
|
||||
|
||||
authorize = ResourceView.as_view()
|
||||
|
||||
|
||||
class TokenView(APIView):
|
||||
def post(self, request, *args, **kwarsg):
|
||||
if request.data['grant_type'] != 'authorization_code': # todo uma grant
|
||||
return Response({'error': 'unsupported_grant_type'}, status=400)
|
||||
|
||||
# validate authorization code
|
||||
try:
|
||||
authorization_code = AuthorizationCode.objects.get(value=request.data['code'])
|
||||
except AuthorizationCode.DoesNotExist:
|
||||
return Response({'error': 'invalid_grant'}, status=400)
|
||||
|
||||
# todo check token lifetime
|
||||
|
||||
# issue access token
|
||||
token = authorization_code.generate_token()
|
||||
return Response({'token': force_text(token)}) # todo 'expires' param
|
||||
|
||||
|
||||
token = TokenView.as_view()
|
|
@ -0,0 +1,138 @@
|
|||
"""
|
||||
Django settings for manager project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.11.18.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '*v%=be_ic=5xivp#63l#ii%ax0%fvty_q=an0app#9z^1(zv9%'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Hey Entr'ouvert is in France !!
|
||||
TIME_ZONE = 'Europe/Paris'
|
||||
LANGUAGE_CODE = 'fr'
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'mellon',
|
||||
'manager',
|
||||
'manager.core',
|
||||
'rest_framework',
|
||||
'xstatic.pkg.jquery',
|
||||
'xstatic.pkg.jquery_ui',
|
||||
'xstatic.pkg.select2',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
LOCALE_PATHS = (os.path.join(BASE_DIR, 'locale'),)
|
||||
|
||||
ROOT_URLCONF = 'manager.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.template.context_processors.media',
|
||||
'django.template.context_processors.request',
|
||||
'django.template.context_processors.static',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'manager.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
|
@ -0,0 +1,9 @@
|
|||
{% extends "manager/root.html" %}
|
||||
{% load i18n static %}
|
||||
{% block page-title %}PII Manager{% endblock %}
|
||||
{% block site-title %}PII Manager{% endblock %}
|
||||
|
||||
{% block logout-url %}{% url "auth_logout" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,102 @@
|
|||
{% load i18n staticfiles %}<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{% block page-title %}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
|
||||
<script src="{% xstatic 'jquery' 'jquery.min.js' %}"></script>
|
||||
<script src="{% xstatic 'jquery-ui' 'jquery-ui.min.js' %}"></script>
|
||||
{{ media }}
|
||||
{% block extrascripts %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body {% block bodyargs %}{% endblock %}>
|
||||
<div id="top">
|
||||
{% block sidepage %}
|
||||
{% endblock %}
|
||||
{% block user-links %}
|
||||
<ul class="user-info">
|
||||
{% if global_title %}
|
||||
<li class="ui-platform-name">{% if portal_url %}<a href="{{portal_url}}">{{ global_title }}</a>{% else %}{{ global_title }}{% endif %}</li>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
<li class="ui-avatar">{{ user.get_full_name|slice:":1" }}</li>
|
||||
<li class="ui-name">{% block user-name %}{{ user.get_full_name }}{% endblock %}</li>
|
||||
<li class="ui-logout"><a href="{% block logout-url %}index.html{% endblock %}"
|
||||
title="{% trans "Logout" %}"></a></li>
|
||||
{% endif %}
|
||||
{% block help-link %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div id="header">
|
||||
{% block site-header %}
|
||||
<h1>{% block site-title %}{% endblock %}</h1>
|
||||
{% block subheader %}{% endblock %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div id="main">
|
||||
|
||||
<div id="main-content" {% block main-content-attributes %}{% endblock %}>
|
||||
{% block main-content %}
|
||||
<div id="more-user-links">
|
||||
{% block more-user-links %}
|
||||
<span id="breadcrumb">
|
||||
{% block breadcrumb %}
|
||||
<a href="{% block homepage-url %}/{% endblock %}">{% block homepage-title %}{% trans "Homepage" %}{% endblock %}</a>
|
||||
{% endblock %}
|
||||
</span>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div id="content">
|
||||
|
||||
<div id="appbar">
|
||||
{% block appbar %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block messages %}
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block beforecontent %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<br style="clear: both;"/>
|
||||
|
||||
{% block aftercontent %}
|
||||
{% endblock %}
|
||||
|
||||
</div> <!-- #content -->
|
||||
{% endblock %}
|
||||
</div> <!-- #main-content -->
|
||||
|
||||
{% block after-main-content %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
||||
</div> <!-- #main -->
|
||||
|
||||
<div id="footer">
|
||||
{% block footer %}
|
||||
Copyright © 2021 Entr'ouvert
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block page-end %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,59 @@
|
|||
import pytest
|
||||
from httmock import HTTMock, urlmatch
|
||||
|
||||
from manager.core.models import (
|
||||
ConsentReceipt,
|
||||
OAuthServer,
|
||||
OAuthSource,
|
||||
PiiCategory,
|
||||
PiiController,
|
||||
Purpose,
|
||||
Service,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_source():
|
||||
# todo mock responses
|
||||
return OAuthSource.object.create(name='test oauth source', url='https://www.myoauthsource.org/resources/')
|
||||
|
||||
|
||||
def oauth_source_mock(oauth_source):
|
||||
# @urlmatch todo
|
||||
def resource_endpoint_mock():
|
||||
pass
|
||||
|
||||
return HTTMock() # todo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_server():
|
||||
# todo mock responses
|
||||
return OAuthServer.objects.create(
|
||||
name='test oauth server', url='https://www.myoauthserver.org/authorize/'
|
||||
)
|
||||
|
||||
|
||||
def oauth_server_mock(oauth_server):
|
||||
# @urlmatch todo
|
||||
def authz_endpoint_mock():
|
||||
pass
|
||||
|
||||
# @urlmatch todo
|
||||
def token_endpoint_mock():
|
||||
pass
|
||||
|
||||
return HTTMock() # todo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_urm_client():
|
||||
pass
|
||||
|
||||
|
||||
def test_simple_oauth(oauth_source, oauth_server):
|
||||
with oauth_source_mock(oauth_source):
|
||||
with oauth_server_mock(oauth_server):
|
||||
pass
|
|
@ -0,0 +1,40 @@
|
|||
# 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.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
|
||||
from manager import views
|
||||
from manager.core import urls as core_urls
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^login/$', views.login, name='auth_login'),
|
||||
url(r'^logout/$', views.logout, name='auth_logout'),
|
||||
url(r'^core/', include(core_urls)),
|
||||
]
|
||||
|
||||
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
|
||||
|
||||
if 'mellon' in settings.INSTALLED_APPS:
|
||||
urlpatterns.append(url(r'^accounts/mellon/', include('mellon.urls')))
|
|
@ -0,0 +1,45 @@
|
|||
# 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.contrib.auth import views as auth_views
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
|
||||
class LoginView(auth_views.LoginView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if any(get_idps()):
|
||||
if not 'next' in request.GET:
|
||||
return HttpResponseRedirect(resolve_url('mellon_login'))
|
||||
return HttpResponseRedirect(
|
||||
resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next'))
|
||||
)
|
||||
return super(LoginView, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
login = LoginView.as_view()
|
||||
|
||||
|
||||
class LogoutView(auth_views.LogoutView):
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if any(get_idps()):
|
||||
return HttpResponseRedirect(resolve_url('mellon_logout'))
|
||||
return super(LogoutView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
logout = LogoutView.as_view()
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for manager project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manager.settings")
|
||||
|
||||
application = get_wsgi_application()
|
|
@ -0,0 +1,31 @@
|
|||
h1. Quickstart:
|
||||
|
||||
On Debian stable/testing, install within a python3 virtual environment:
|
||||
|
||||
<pre>
|
||||
sudo apt install virtualenv python3-xstatic
|
||||
mkdir ~/pii-manager-venv/
|
||||
virtualenv -p python3 ~/pii-manager-venv/
|
||||
source ~/pii-manager-venv/bin/activate
|
||||
python3 ./setup.py develop
|
||||
</pre>
|
||||
|
||||
h1. Local testing:
|
||||
|
||||
<pre>
|
||||
python3 ./manage.py makemigrations
|
||||
python3 ./manage.py migrate
|
||||
python3 ./manage.py runserver 127.0.0.1:8080
|
||||
</pre>
|
||||
|
||||
h1. Todolist:
|
||||
|
||||
* PostgreSQL integration
|
||||
* uwsgi/nginx configuration
|
||||
* gadjo support
|
||||
* crud api views on main models
|
||||
|
||||
h1. Optional todos:
|
||||
|
||||
* hobo integration (?) -- see https://git.entrouvert.org/hobo.git/
|
||||
* publik devinst integration (?) -- see https://git.entrouvert.org/publik-devinst.git/
|
|
@ -0,0 +1,93 @@
|
|||
#! /usr/bin/env python
|
||||
#
|
||||
'''
|
||||
Setup script for PII Manager proof-of-concept implementations
|
||||
'''
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
def get_version():
|
||||
"""Use the VERSION, if absent generates a version with git describe, if not
|
||||
tag exists, take 0.0- and add the length of the commit log.
|
||||
"""
|
||||
if os.path.exists('VERSION'):
|
||||
with open('VERSION', 'r') as v:
|
||||
return v.read()
|
||||
if os.path.exists('.git'):
|
||||
p = subprocess.Popen(
|
||||
['git', 'describe', '--dirty=.dirty', '--match=v*'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
result = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v
|
||||
if '-' in result: # not a tagged version
|
||||
real_number, commit_count, commit_hash = result.split('-', 2)
|
||||
version = '%s.post%s+%s' % (real_number, commit_count, commit_hash)
|
||||
else:
|
||||
version = result
|
||||
return version
|
||||
else:
|
||||
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0'
|
||||
|
||||
|
||||
setup(
|
||||
name="pii-manager-poc",
|
||||
version=get_version(),
|
||||
license="AGPLv3+",
|
||||
description="PII Manager proof-of-concept implementation",
|
||||
url="http://git.entrouvert.org/pii-manager-poc.git/",
|
||||
author="Entr'ouvert",
|
||||
author_email="pmarillonnet@entrouvert.com",
|
||||
maintainer="Paul Marillonnet",
|
||||
maintainer_email="pmarillonnet@entrouvert.com",
|
||||
scripts=('manage.py',),
|
||||
packages=find_packages('manager'),
|
||||
package_dir={
|
||||
'': 'manager',
|
||||
},
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'django>=1.11,<2.3',
|
||||
'requests>=2.3',
|
||||
'requests-oauthlib',
|
||||
'Django-Select2>5,<6',
|
||||
'django-ratelimit',
|
||||
'djangorestframework>=3.3,<3.10',
|
||||
'pycryptodomex',
|
||||
'django-mellon>=1.22',
|
||||
'jwcrypto>=0.3.1,<1',
|
||||
'cryptography',
|
||||
'XStatic-jQuery<2',
|
||||
'XStatic-jquery-ui',
|
||||
'xstatic-select2',
|
||||
'pytz',
|
||||
'sqlparse',
|
||||
'django-phonenumber-field',
|
||||
'phonenumbers',
|
||||
],
|
||||
zip_safe=False,
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Django",
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'Intended Audience :: Information Technology',
|
||||
'Intended Audience :: Legal Industry',
|
||||
'Intended Audience :: Science/Research',
|
||||
'Intended Audience :: Telecommunications Industry',
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: System :: Systems Administration :: Authentication/Directory",
|
||||
],
|
||||
cmdclass={},
|
||||
)
|
Reference in New Issue