diff --git a/examples/import_credentials.csv b/examples/import_credentials.csv deleted file mode 100644 index 4a06714..0000000 --- a/examples/import_credentials.csv +++ /dev/null @@ -1,2 +0,0 @@ -name email membershipType -Pepe user1@example.org individual diff --git a/examples/membership-card.csv b/examples/membership-card.csv deleted file mode 100644 index 1f4d08d..0000000 --- a/examples/membership-card.csv +++ /dev/null @@ -1,2 +0,0 @@ -name surnames email typeOfPerson membershipType organisation affiliatedSince -Pepe Gómez user1@example.org individual Member Pangea 01-01-2023 diff --git a/examples/membership-card.ods b/examples/membership-card.ods new file mode 100644 index 0000000..b1fe1ac Binary files /dev/null and b/examples/membership-card.ods differ diff --git a/examples/membership-card.xls b/examples/membership-card.xls new file mode 100644 index 0000000..bd10538 Binary files /dev/null and b/examples/membership-card.xls differ diff --git a/examples/signerDNIe004.pfx b/examples/signerDNIe004.pfx new file mode 100644 index 0000000..a141358 Binary files /dev/null and b/examples/signerDNIe004.pfx differ diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 478d671..4264e03 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -1,11 +1,16 @@ import csv import json +import base64 +import copy import pandas as pd +from pyhanko.sign import signers + from django import forms +from django.core.cache import cache from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError -from utils import credtools +from utils import credtools, certs from idhub.models import ( DID, File_datas, @@ -18,26 +23,69 @@ from idhub.models import ( from idhub_auth.models import User +class TermsConditionsForm(forms.Form): + accept = forms.BooleanField( + label=_("Accept terms and conditions of the service"), + required=False + ) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + + def clean(self): + data = self.cleaned_data + if data.get("accept"): + self.user.accept_gdpr = True + else: + self.user.accept_gdpr = False + return data + + def save(self, commit=True): + + if commit: + self.user.save() + return self.user + + return + + class ImportForm(forms.Form): did = forms.ChoiceField(label=_("Did"), choices=[]) + eidas1 = forms.ChoiceField( + label=_("Signature with Eidas1"), + choices=[], + required=False + ) schema = forms.ChoiceField(label=_("Schema"), choices=[]) file_import = forms.FileField(label=_("File import")) def __init__(self, *args, **kwargs): self._schema = None self._did = None + self._eidas1 = None self.rows = {} self.properties = {} + self.users = [] self.user = kwargs.pop('user', None) super().__init__(*args, **kwargs) + dids = DID.objects.filter(user=self.user) self.fields['did'].choices = [ - (x.did, x.label) for x in DID.objects.filter(user=self.user) + (x.did, x.label) for x in dids.filter(eidas1=False) ] self.fields['schema'].choices = [ (x.id, x.name) for x in Schemas.objects.filter() ] + if dids.filter(eidas1=True).exists(): + choices = [("", "")] + choices.extend([ + (x.did, x.label) for x in dids.filter(eidas1=True) + ]) + self.fields['eidas1'].choices = choices + else: + self.fields.pop('eidas1') - def clean_did(self): + def clean(self): data = self.cleaned_data["did"] did = DID.objects.filter( user=self.user, @@ -48,6 +96,14 @@ class ImportForm(forms.Form): raise ValidationError("Did is not valid!") self._did = did.first() + + eidas1 = self.cleaned_data.get('eidas1') + if eidas1: + self._eidas1 = DID.objects.filter( + user=self.user, + eidas1=True, + did=eidas1 + ).first() return data @@ -62,7 +118,8 @@ class ImportForm(forms.Form): self._schema = schema.first() try: self.json_schema = json.loads(self._schema.data) - prop = self.json_schema['properties'] + props = [x for x in self.json_schema["allOf"] if 'properties' in x.keys()] + prop = props[0]['properties'] self.properties = prop['credentialSubject']['properties'] except Exception: raise ValidationError("Schema is not valid!") @@ -70,7 +127,10 @@ class ImportForm(forms.Form): if not self.properties: raise ValidationError("Schema is not valid!") - + # TODO we need filter "$ref" of schema for can validate a csv + self.json_schema_filtered = copy.copy(self.json_schema) + allOf = [x for x in self.json_schema["allOf"] if '$ref' not in x.keys()] + self.json_schema_filtered["allOf"] = allOf return data def clean_file_import(self): @@ -79,7 +139,8 @@ class ImportForm(forms.Form): if File_datas.objects.filter(file_name=self.file_name, success=True).exists(): raise ValidationError("This file already exists!") - df = pd.read_csv (data, delimiter="\t", quotechar='"', quoting=csv.QUOTE_ALL) + # df = pd.read_csv (data, delimiter="\t", quotechar='"', quoting=csv.QUOTE_ALL) + df = pd.read_excel(data) data_pd = df.fillna('').to_dict() if not data_pd: @@ -111,18 +172,18 @@ class ImportForm(forms.Form): def validate_jsonld(self, line, row): try: - credtools.validate_json(row, self.json_schema) + check = credtools.validate_json(row, self.json_schema_filtered) + if check is not True: + raise ValidationError("Not valid row") except Exception as e: msg = "line {}: {}".format(line+1, e) self.exception(msg) - user = User.objects.filter(email=row.get('email')) - if not user: - txt = _('The user does not exist!') - msg = "line {}: {}".format(line+1, txt) - self.exception(msg) + user, new = User.objects.get_or_create(email=row.get('email')) + if new: + self.users.append(user) - return user.first() + return user def create_credential(self, user, row): return VerificableCredential( @@ -131,6 +192,7 @@ class ImportForm(forms.Form): csv_data=json.dumps(row), issuer_did=self._did, schema=self._schema, + eidas1_did=self._eidas1 ) def exception(self, msg): @@ -216,3 +278,70 @@ class UserRolForm(forms.ModelForm): raise forms.ValidationError(msg) return data['service'] + + +class ImportCertificateForm(forms.Form): + label = forms.CharField(label=_("Label")) + password = forms.CharField( + label=_("Password of certificate"), + widget=forms.PasswordInput + ) + file_import = forms.FileField(label=_("File import")) + + def __init__(self, *args, **kwargs): + self._did = None + self._s = None + self._label = None + self.user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + + def clean(self): + data = super().clean() + file_import = data.get('file_import') + self.pfx_file = file_import.read() + self.file_name = file_import.name + self._pss = data.get('password') + self._label = data.get('label') + if not self.pfx_file or not self._pss: + msg = _("Is not a valid certificate") + raise forms.ValidationError(msg) + + self.signer_init() + if not self._s: + msg = _("Is not a valid certificate") + raise forms.ValidationError(msg) + + self.new_did() + return data + + def new_did(self): + cert = self.pfx_file + keys = { + "cert": base64.b64encode(self.pfx_file).decode('utf-8'), + "passphrase": self._pss + } + key_material = json.dumps(keys) + self._did = DID( + key_material=key_material, + did=self.file_name, + label=self._label, + eidas1=True, + user=self.user, + type=DID.Types.KEY + ) + + pw = cache.get("KEY_DIDS") + self._did.set_key_material(key_material, pw) + + def save(self, commit=True): + + if commit: + self._did.save() + return self._did + + return + + def signer_init(self): + self._s = certs.load_cert( + self.pfx_file, self._pss.encode('utf-8') + ) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 7bb3f8d..2af35dc 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -10,7 +10,7 @@ from django_tables2 import SingleTableView from django.conf import settings from django.template.loader import get_template from django.utils.translation import gettext_lazy as _ -from django.views.generic.base import TemplateView +from django.views.generic.base import TemplateView, View from django.views.generic.edit import ( CreateView, DeleteView, @@ -18,6 +18,7 @@ from django.views.generic.edit import ( UpdateView, ) from django.shortcuts import get_object_or_404, redirect +from django.core.cache import cache from django.urls import reverse_lazy from django.http import HttpResponse from django.contrib import messages @@ -29,8 +30,10 @@ from idhub.email.views import NotifyActivateUserByEmail from idhub.admin.forms import ( ImportForm, MembershipForm, + TermsConditionsForm, SchemaForm, UserRolForm, + ImportCertificateForm, ) from idhub.admin.tables import ( DashboardTable, @@ -55,6 +58,41 @@ from idhub.models import ( ) +class TermsAndConditionsView(AdminView, FormView): + template_name = "idhub/admin/terms_conditions.html" + title = _("GDPR") + section = "" + subtitle = _('Accept Terms and Conditions') + icon = 'bi bi-file-earmark-medical' + form_class = TermsConditionsForm + success_url = reverse_lazy('idhub:admin_dashboard') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + kwargs['initial'] = {"accept": self.request.user.accept_gdpr} + return kwargs + + def form_valid(self, form): + user = form.save() + return super().form_valid(form) + + +class DobleFactorAuthView(AdminView, View): + url = reverse_lazy('idhub:admin_dashboard') + + def get(self, request, *args, **kwargs): + self.check_valid_user() + if not self.request.session.get("2fauth"): + return redirect(self.url) + + if self.request.session.get("2fauth") == str(kwargs.get("admin2fauth")): + self.request.session.pop("2fauth", None) + return redirect(self.url) + + return redirect(reverse_lazy("idhub:login")) + + class DashboardView(AdminView, SingleTableView): template_name = "idhub/admin/dashboard.html" table_class = DashboardTable @@ -132,6 +170,7 @@ class PeopleView(People, TemplateView): class PeopleActivateView(PeopleView): def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(self.model, pk=self.pk) @@ -153,6 +192,7 @@ class PeopleActivateView(PeopleView): class PeopleDeleteView(PeopleView): def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(self.model, pk=self.pk) @@ -317,6 +357,7 @@ class PeopleMembershipDeleteView(PeopleView): model = Membership def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(self.model, pk=self.pk) @@ -404,6 +445,7 @@ class PeopleRolDeleteView(PeopleView): model = UserRol def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(self.model, pk=self.pk) user = self.object.user @@ -470,6 +512,7 @@ class RolDeleteView(AccessControl): model = Rol def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(self.model, pk=self.pk) @@ -546,6 +589,7 @@ class ServiceDeleteView(AccessControl): model = Service def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(self.model, pk=self.pk) @@ -592,6 +636,7 @@ class CredentialView(Credentials): class CredentialJsonView(Credentials): def get(self, request, *args, **kwargs): + self.check_valid_user() pk = kwargs['pk'] self.object = get_object_or_404( VerificableCredential, @@ -606,6 +651,7 @@ class RevokeCredentialsView(Credentials): success_url = reverse_lazy('idhub:admin_credentials') def get(self, request, *args, **kwargs): + self.check_valid_user() pk = kwargs['pk'] self.object = get_object_or_404( VerificableCredential, @@ -625,6 +671,7 @@ class DeleteCredentialsView(Credentials): success_url = reverse_lazy('idhub:admin_credentials') def get(self, request, *args, **kwargs): + self.check_valid_user() pk = kwargs['pk'] self.object = get_object_or_404( VerificableCredential, @@ -669,13 +716,13 @@ class DidRegisterView(Credentials, CreateView): icon = 'bi bi-patch-check-fill' wallet = True model = DID - fields = ('label',) + fields = ('label', 'type') success_url = reverse_lazy('idhub:admin_dids') object = None def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did() + form.instance.set_did(cache.get("KEY_DIDS")) form.save() messages.success(self.request, _('DID created successfully')) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) @@ -710,6 +757,7 @@ class DidDeleteView(Credentials, DeleteView): success_url = reverse_lazy('idhub:admin_dids') def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(self.model, pk=self.pk) Event.set_EV_ORG_DID_DELETED_BY_ADMIN(self.object) @@ -725,11 +773,27 @@ class WalletCredentialsView(Credentials): wallet = True -class WalletConfigIssuesView(Credentials): +class WalletConfigIssuesView(Credentials, FormView): template_name = "idhub/admin/wallet_issues.html" subtitle = _('Configure credential issuance') icon = 'bi bi-patch-check-fill' wallet = True + form_class = ImportCertificateForm + success_url = reverse_lazy('idhub:admin_dids') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + cred = form.save() + if cred: + messages.success(self.request, _("The credential was imported successfully!")) + Event.set_EV_ORG_DID_CREATED_BY_ADMIN(cred) + else: + messages.error(self.request, _("Error importing the credential!")) + return super().form_valid(form) class SchemasView(SchemasMix, SingleTableView): @@ -750,6 +814,7 @@ class SchemasView(SchemasMix, SingleTableView): class SchemasDeleteView(SchemasMix): def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(Schemas, pk=self.pk) self.object.delete() @@ -760,6 +825,7 @@ class SchemasDeleteView(SchemasMix): class SchemasDownloadView(SchemasMix): def get(self, request, *args, **kwargs): + self.check_valid_user() self.pk = kwargs['pk'] self.object = get_object_or_404(Schemas, pk=self.pk) @@ -838,6 +904,7 @@ class SchemasImportView(SchemasMix): class SchemasImportAddView(SchemasMix): def get(self, request, *args, **kwargs): + self.check_valid_user() file_name = kwargs['file_schema'] schemas_files = os.listdir(settings.SCHEMAS_DIR) if file_name not in schemas_files: @@ -855,14 +922,18 @@ class SchemasImportAddView(SchemasMix): ldata = json.loads(data) assert credtools.validate_schema(ldata) name = ldata.get('name') + title = ldata.get('title') assert name + assert title except Exception: messages.error(self.request, _('This is not a valid schema!')) return - schema = Schemas.objects.create(file_schema=file_name, - data=data, type=name, - template_description=self.get_description() - ) + schema = Schemas.objects.create( + file_schema=file_name, + data=data, + type=name, + template_description=self.get_description() + ) schema.save() return schema @@ -917,7 +988,7 @@ class ImportStep2View(ImportExport, TemplateView): return context -class ImportAddView(ImportExport, FormView): +class ImportAddView(NotifyActivateUserByEmail, ImportExport, FormView): template_name = "idhub/admin/import_add.html" subtitle = _('Import') icon = '' @@ -938,4 +1009,11 @@ class ImportAddView(ImportExport, FormView): Event.set_EV_CREDENTIAL_CAN_BE_REQUESTED(cred) else: messages.error(self.request, _("Error importing the file!")) + + for user in form.users: + try: + self.send_email(user) + except SMTPException as e: + messages.error(self.request, e) + return super().form_valid(form) diff --git a/idhub/email/views.py b/idhub/email/views.py index 72e0daa..f14e2a5 100644 --- a/idhub/email/views.py +++ b/idhub/email/views.py @@ -13,7 +13,11 @@ logger = logging.getLogger(__name__) class NotifyActivateUserByEmail: - def get_email_context(self, user): + subject_template_name = 'idhub/admin/registration/activate_user_subject.txt' + email_template_name = 'idhub/admin/registration/activate_user_email.txt' + html_email_template_name = 'idhub/admin/registration/activate_user_email.html' + + def get_email_context(self, user, token): """ Define a new context with a token for put in a email when send a email for add a new password @@ -22,35 +26,35 @@ class NotifyActivateUserByEmail: current_site = get_current_site(self.request) site_name = current_site.name domain = current_site.domain + if not token: + token = default_token_generator.make_token(user) + context = { 'email': user.email, 'domain': domain, 'site_name': site_name, 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 'user': user, - 'token': default_token_generator.make_token(user), + 'token': token, 'protocol': protocol, } return context - def send_email(self, user): + def send_email(self, user, token=None): """ Send a email when a user is activated. """ - context = self.get_email_context(user) - subject_template_name = 'idhub/admin/registration/activate_user_subject.txt' - email_template_name = 'idhub/admin/registration/activate_user_email.txt' - html_email_template_name = 'idhub/admin/registration/activate_user_email.html' - subject = loader.render_to_string(subject_template_name, context) + context = self.get_email_context(user, token) + subject = loader.render_to_string(self.subject_template_name, context) # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) - body = loader.render_to_string(email_template_name, context) + body = loader.render_to_string(self.email_template_name, context) from_email = settings.DEFAULT_FROM_EMAIL to_email = user.email email_message = EmailMultiAlternatives( subject, body, from_email, [to_email]) - html_email = loader.render_to_string(html_email_template_name, context) + html_email = loader.render_to_string(self.html_email_template_name, context) email_message.attach_alternative(html_email, 'text/html') try: if settings.DEVELOPMENT: diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 034bc68..04c2df7 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -7,6 +7,7 @@ from utils import credtools from django.conf import settings from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model +from django.core.cache import cache from decouple import config from idhub.models import DID, Schemas from oidc4vp.models import Organization @@ -36,17 +37,25 @@ class Command(BaseCommand): self.create_organizations(r[0].strip(), r[1].strip()) self.sync_credentials_organizations("pangea.org", "somconnexio.coop") self.sync_credentials_organizations("local 8000", "local 9000") - self.create_defaults_dids() self.create_schemas() def create_admin_users(self, email, password): - User.objects.create_superuser(email=email, password=password) + su = User.objects.create_superuser(email=email, password=password) + su.set_encrypted_sensitive_data(password) + su.save() + key = su.decrypt_sensitive_data(password) + key_dids = {su.id: key} + cache.set("KEY_DIDS", key_dids, None) + self.create_defaults_dids(su, key) def create_users(self, email, password): - u= User.objects.create(email=email, password=password) + u = User.objects.create(email=email, password=password) u.set_password(password) + u.set_encrypted_sensitive_data(password) u.save() + key = u.decrypt_sensitive_data(password) + self.create_defaults_dids(u, key) def create_organizations(self, name, url): @@ -61,12 +70,10 @@ class Command(BaseCommand): org1.my_client_secret = org2.client_secret org1.save() org2.save() - - def create_defaults_dids(self): - for u in User.objects.all(): - did = DID(label="Default", user=u) - did.set_did() - did.save() + def create_defaults_dids(self, u, password): + did = DID(label="Default", user=u, type=DID.Types.KEY) + did.set_did(password) + did.save() def create_schemas(self): schemas_files = os.listdir(settings.SCHEMAS_DIR) @@ -82,11 +89,22 @@ class Command(BaseCommand): try: ldata = json.loads(data) assert credtools.validate_schema(ldata) - name = ldata.get('name') - assert name + dname = ldata.get('name') + title = ldata.get('title') + assert dname + assert title + except Exception: + title = '' + return + name = '' + try: + for x in dname: + if settings.LANGUAGE_CODE in x['lang']: + name = x.get('value', '') except Exception: return - Schemas.objects.create(file_schema=file_name, data=data, type=name) + + Schemas.objects.create(file_schema=file_name, data=data, type=title) def open_file(self, file_name): data = '' diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py deleted file mode 100644 index c0c04d3..0000000 --- a/idhub/migrations/0001_initial.py +++ /dev/null @@ -1,361 +0,0 @@ -# Generated by Django 4.2.5 on 2024-01-22 12:15 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='DID', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('created_at', models.DateTimeField(auto_now=True)), - ('label', models.CharField(max_length=50, verbose_name='Label')), - ('did', models.CharField(max_length=250)), - ('key_material', models.CharField(max_length=250)), - ( - 'user', - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name='dids', - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name='File_datas', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('file_name', models.CharField(max_length=250)), - ('success', models.BooleanField(default=True)), - ('created_at', models.DateTimeField(auto_now=True)), - ], - ), - migrations.CreateModel( - name='Rol', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('name', models.CharField(max_length=250, verbose_name='name')), - ( - 'description', - models.CharField( - max_length=250, null=True, verbose_name='Description' - ), - ), - ], - ), - migrations.CreateModel( - name='Schemas', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('type', models.CharField(max_length=250)), - ('file_schema', models.CharField(max_length=250)), - ('data', models.TextField()), - ('created_at', models.DateTimeField(auto_now=True)), - ( - '_name', - models.CharField(db_column='name', max_length=250, null=True), - ), - ( - '_description', - models.CharField( - db_column='description', max_length=250, null=True - ), - ), - ('template_description', models.TextField(null=True)), - ], - ), - migrations.CreateModel( - name='Service', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('domain', models.CharField(max_length=250, verbose_name='Domain')), - ( - 'description', - models.CharField(max_length=250, verbose_name='Description'), - ), - ('rol', models.ManyToManyField(to='idhub.rol')), - ], - ), - migrations.CreateModel( - name='VCTemplate', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('wkit_template_id', models.CharField(max_length=250)), - ('data', models.TextField()), - ], - ), - migrations.CreateModel( - name='VerificableCredential', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('id_string', models.CharField(max_length=250)), - ('verified', models.BooleanField()), - ('created_on', models.DateTimeField(auto_now=True)), - ('issued_on', models.DateTimeField(null=True)), - ('data', models.TextField()), - ('csv_data', models.TextField()), - ( - 'status', - models.PositiveSmallIntegerField( - choices=[ - (1, 'Enabled'), - (2, 'Issued'), - (3, 'Revoked'), - (4, 'Expired'), - ], - default=1, - ), - ), - ( - 'issuer_did', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='vcredentials', - to='idhub.did', - ), - ), - ( - 'schema', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='vcredentials', - to='idhub.schemas', - ), - ), - ( - 'subject_did', - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name='subject_credentials', - to='idhub.did', - ), - ), - ( - 'user', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='vcredentials', - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name='Membership', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ( - 'type', - models.PositiveSmallIntegerField( - choices=[(1, 'Beneficiary'), (2, 'Employee'), (3, 'Member')], - verbose_name='Type of membership', - ), - ), - ( - 'start_date', - models.DateField( - blank=True, - help_text='What date did the membership start?', - null=True, - verbose_name='Start date', - ), - ), - ( - 'end_date', - models.DateField( - blank=True, - help_text='What date will the membership end?', - null=True, - verbose_name='End date', - ), - ), - ( - 'user', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='memberships', - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name='Event', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('created', models.DateTimeField(auto_now=True, verbose_name='Date')), - ( - 'message', - models.CharField(max_length=350, verbose_name='Description'), - ), - ( - 'type', - models.PositiveSmallIntegerField( - choices=[ - (1, 'User registered'), - (2, 'User welcomed'), - (3, 'Data update requested by user'), - ( - 4, - 'Data update requested. Pending approval by administrator', - ), - (5, "User's data updated by admin"), - (6, 'Your data updated by admin'), - (7, 'User deactivated by admin'), - (8, 'DID created by user'), - (9, 'DID created'), - (10, 'DID deleted'), - (11, 'Credential deleted by user'), - (12, 'Credential deleted'), - (13, 'Credential issued for user'), - (14, 'Credential issued'), - (15, 'Credential presented by user'), - (16, 'Credential presented'), - (17, 'Credential enabled'), - (18, 'Credential available'), - (19, 'Credential revoked by admin'), - (20, 'Credential revoked'), - (21, 'Role created by admin'), - (22, 'Role modified by admin'), - (23, 'Role deleted by admin'), - (24, 'Service created by admin'), - (25, 'Service modified by admin'), - (26, 'Service deleted by admin'), - (27, 'Organisational DID created by admin'), - (28, 'Organisational DID deleted by admin'), - (29, 'User deactivated'), - (30, 'User activated'), - ], - verbose_name='Event', - ), - ), - ( - 'user', - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name='events', - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.CreateModel( - name='UserRol', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ( - 'service', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='users', - to='idhub.service', - verbose_name='Service', - ), - ), - ( - 'user', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='roles', - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - 'unique_together': {('user', 'service')}, - }, - ), - ] diff --git a/idhub/mixins.py b/idhub/mixins.py index 68b7344..b2b3fbe 100644 --- a/idhub/mixins.py +++ b/idhub/mixins.py @@ -1,13 +1,46 @@ from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth import views as auth_views -from django.urls import reverse_lazy, resolve from django.utils.translation import gettext_lazy as _ +from django.contrib.auth import views as auth_views +from django.core.exceptions import PermissionDenied +from django.urls import reverse_lazy, resolve from django.shortcuts import redirect +from django.core.cache import cache + + +class Http403(PermissionDenied): + status_code = 403 + default_detail = _('Permission denied. User is not authenticated') + default_code = 'forbidden' + + def __init__(self, detail=None, code=None): + if detail is not None: + self.detail = details or self.default_details + if code is not None: + self.code = code or self.default_code class UserView(LoginRequiredMixin): login_url = "/login/" wallet = False + path_terms = [ + 'admin_terms_and_conditions', + 'user_terms_and_conditions', + 'user_gdpr', + ] + + def get(self, request, *args, **kwargs): + self.admin_validated = cache.get("KEY_DIDS") + response = super().get(request, *args, **kwargs) + url = self.check_gdpr() + + return url or response + + def post(self, request, *args, **kwargs): + self.admin_validated = cache.get("KEY_DIDS") + response = super().post(request, *args, **kwargs) + url = self.check_gdpr() + + return url or response def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -19,15 +52,33 @@ class UserView(LoginRequiredMixin): 'path': resolve(self.request.path).url_name, 'user': self.request.user, 'wallet': self.wallet, + 'admin_validated': True if self.admin_validated else False }) return context + def check_gdpr(self): + if not self.request.user.accept_gdpr: + url = reverse_lazy("idhub:user_terms_and_conditions") + if self.request.user.is_admin: + url = reverse_lazy("idhub:admin_terms_and_conditions") + if resolve(self.request.path).url_name not in self.path_terms: + return redirect(url) + class AdminView(UserView): def get(self, request, *args, **kwargs): - if not request.user.is_admin: - url = reverse_lazy('idhub:user_dashboard') - return redirect(url) - + self.check_valid_user() return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.check_valid_user() + return super().post(request, *args, **kwargs) + + def check_valid_user(self): + if not self.request.user.is_admin: + raise Http403() + + if self.request.session.get("2fauth"): + raise Http403() + diff --git a/idhub/models.py b/idhub/models.py index 40b0b5d..d6f6f7a 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -1,14 +1,21 @@ import json +import ujson import pytz +import hashlib import datetime +from collections import OrderedDict from django.db import models from django.conf import settings +from django.core.cache import cache from django.template.loader import get_template from django.utils.translation import gettext_lazy as _ +from nacl import secret + from utils.idhub_ssikit import ( generate_did_controller_key, keydid_from_controller_key, sign_credential, + webdid_from_controller_key, ) from idhub_auth.models import User @@ -45,6 +52,7 @@ class Event(models.Model): EV_ORG_DID_DELETED_BY_ADMIN = 28, "Organisational DID deleted by admin" EV_USR_DEACTIVATED_BY_ADMIN = 29, "User deactivated" EV_USR_ACTIVATED_BY_ADMIN = 30, "User activated" + EV_USR_SEND_VP = 31, "User send Verificable Presentation" created = models.DateTimeField(_("Date"), auto_now=True) message = models.CharField(_("Description"), max_length=350) @@ -400,37 +408,66 @@ class Event(models.Model): type=cls.Types.EV_USR_ACTIVATED_BY_ADMIN, message=msg, ) + + @classmethod + def set_EV_USR_SEND_VP(cls, msg, user): + cls.objects.create( + type=cls.Types.EV_USR_SEND_VP, + message=msg, + user=user + ) class DID(models.Model): + class Types(models.IntegerChoices): + KEY = 1, "Key" + WEB = 2, "Web" + type = models.PositiveSmallIntegerField( + _("Type"), + choices=Types.choices, + ) created_at = models.DateTimeField(auto_now=True) label = models.CharField(_("Label"), max_length=50) did = models.CharField(max_length=250) # In JWK format. Must be stored as-is and passed whole to library functions. # Example key material: # '{"kty":"OKP","crv":"Ed25519","x":"oB2cPGFx5FX4dtS1Rtep8ac6B__61HAP_RtSzJdPxqs","d":"OJw80T1CtcqV0hUcZdcI-vYNBN1dlubrLaJa0_se_gU"}' - key_material = models.CharField(max_length=250) + key_material = models.TextField() + eidas1 = models.BooleanField(default=False) user = models.ForeignKey( User, on_delete=models.CASCADE, related_name='dids', null=True, ) + didweb_document = models.TextField() + def get_key_material(self, password): + return self.user.decrypt_data(self.key_material, password) + + def set_key_material(self, value, password): + self.key_material = self.user.encrypt_data(value, password) + @property def is_organization_did(self): if not self.user: return True return False - def set_did(self): - self.key_material = generate_did_controller_key() - self.did = keydid_from_controller_key(self.key_material) + def set_did(self, password): + new_key_material = generate_did_controller_key() + self.set_key_material(new_key_material, password) + + if self.type == self.Types.KEY: + self.did = keydid_from_controller_key(new_key_material) + elif self.type == self.Types.WEB: + didurl, document = webdid_from_controller_key(new_key_material) + self.did = didurl + self.didweb_document = document def get_key(self): return json.loads(self.key_material) - class Schemas(models.Model): type = models.CharField(max_length=250) file_schema = models.CharField(max_length=250) @@ -446,6 +483,7 @@ class Schemas(models.Model): return {} return json.loads(self.data) +#<<<<<<< HEAD def _update_and_get_field(self, field_attr, schema_key): field_value = getattr(self, field_attr) if not field_value: @@ -467,6 +505,21 @@ class Schemas(models.Model): self._name = value @property +#======= + def name(self, request=None): + names = {} + for name in self.get_schema.get('name', []): + lang = name.get('lang') + if 'ca' in lang: + lang = 'ca' + names[lang]= name.get('value') + + if request and request.LANGUAGE_CODE in names.keys(): + return names[request.LANGUAGE_CODE] + + return names[settings.LANGUAGE_CODE] + +#>>>>>>> main def description(self): return self._update_and_get_field('_description', 'description') @@ -490,6 +543,7 @@ class VerificableCredential(models.Model): issued_on = models.DateTimeField(null=True) data = models.TextField() csv_data = models.TextField() + hash = models.CharField(max_length=260) status = models.PositiveSmallIntegerField( choices=Status.choices, default=Status.ENABLED @@ -510,17 +564,54 @@ class VerificableCredential(models.Model): on_delete=models.CASCADE, related_name='vcredentials', ) + eidas1_did = models.ForeignKey( + DID, + on_delete=models.CASCADE, + null=True + ) schema = models.ForeignKey( Schemas, on_delete=models.CASCADE, related_name='vcredentials', ) + def get_data(self, password): + if not self.data: + return "" + if self.eidas1_did: + return self.data + + return self.user.decrypt_data(self.data, password) + + def set_data(self, value, password): + self.data = self.user.encrypt_data(value, password) + def type(self): return self.schema.type +#<<<<<<< HEAD def get_description(self): return self.schema.template_description +#======= + def description(self): + for des in json.loads(self.render("")).get('description', []): + if settings.LANGUAGE_CODE in des.get('lang'): + return des.get('value', '') + return '' +#>>>>>>> main + + def get_type(self, lang=None): + schema = json.loads(self.schema.data) + if not schema.get('name'): + return '' + try: + for x in schema['name']: + if lang or settings.LANGUAGE_CODE in x['lang']: + return x.get('value', '') + except: + return self.schema.type + + return '' def get_status(self): return self.Status(self.status).label @@ -529,43 +620,72 @@ class VerificableCredential(models.Model): data = json.loads(self.csv_data).items() return data - def issue(self, did): + def issue(self, did, password, domain=settings.DOMAIN.strip("/")): if self.status == self.Status.ISSUED: return self.status = self.Status.ISSUED self.subject_did = did self.issued_on = datetime.datetime.now().astimezone(pytz.utc) - self.data = sign_credential( - self.render(), - self.issuer_did.key_material - ) + issuer_pass = cache.get("KEY_DIDS") + # issuer_pass = self.user.decrypt_data( + # cache.get("KEY_DIDS"), + # settings.SECRET_KEY, + # ) - def get_context(self): + # hash of credential without sign + self.hash = hashlib.sha3_256(self.render(domain).encode()).hexdigest() + data = sign_credential( + self.render(domain), + self.issuer_did.get_key_material(issuer_pass) + ) + if self.eidas1_did: + self.data = data + else: + self.data = self.user.encrypt_data(data, password) + + def get_context(self, domain): d = json.loads(self.csv_data) issuance_date = '' if self.issued_on: format = "%Y-%m-%dT%H:%M:%SZ" issuance_date = self.issued_on.strftime(format) + cred_path = 'credentials' + sid = self.id + if self.eidas1_did: + cred_path = 'public/credentials' + sid = self.hash + + url_id = "{}/{}/{}".format( + domain, + cred_path, + sid + ) + context = { - 'vc_id': self.id, + 'vc_id': url_id, 'issuer_did': self.issuer_did.did, 'subject_did': self.subject_did and self.subject_did.did or '', 'issuance_date': issuance_date, - 'first_name': self.user.first_name, - 'last_name': self.user.last_name, + 'firstName': self.user.first_name or "", + 'lastName': self.user.last_name or "", + 'email': self.user.email, + 'organisation': settings.ORGANIZATION or '', } context.update(d) + context['firstName'] = "" return context - def render(self): - context = self.get_context() + def render(self, domain): + context = self.get_context(domain) template_name = 'credentials/{}'.format( self.schema.file_schema ) tmpl = get_template(template_name) - return tmpl.render(context) + d_ordered = ujson.loads(tmpl.render(context)) + d_minimum = self.filter_dict(d_ordered) + return ujson.dumps(d_minimum) def get_issued_on(self): if self.issued_on: @@ -573,6 +693,18 @@ class VerificableCredential(models.Model): return '' + def filter_dict(self, dic): + new_dict = OrderedDict() + for key, value in dic.items(): + if isinstance(value, dict): + new_value = self.filter_dict(value) + if new_value: + new_dict[key] = new_value + elif value: + new_dict[key] = value + return new_dict + + class VCTemplate(models.Model): wkit_template_id = models.CharField(max_length=250) data = models.TextField() diff --git a/idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg b/idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg new file mode 100644 index 0000000..2a285a9 Binary files /dev/null and b/idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg differ diff --git a/idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg b/idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg new file mode 100644 index 0000000..9b954dd Binary files /dev/null and b/idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg differ diff --git a/idhub/templates/auth/2fadmin.html b/idhub/templates/auth/2fadmin.html new file mode 100644 index 0000000..4dc2ae6 --- /dev/null +++ b/idhub/templates/auth/2fadmin.html @@ -0,0 +1,19 @@ +{% extends "auth/login_base.html" %} +{% load i18n django_bootstrap5 %} + +{% block login_content %} + +
+
+

{% trans 'Doble Factor of Authentication' %}

+
+
+ +
+
+
+ {% trans "We have sent an email with a link that you have to select in order to login." %} +
+
+
+{% endblock %} diff --git a/idhub/templates/auth/2fadmin_email.html b/idhub/templates/auth/2fadmin_email.html new file mode 100644 index 0000000..d2253c5 --- /dev/null +++ b/idhub/templates/auth/2fadmin_email.html @@ -0,0 +1,26 @@ +{% load i18n %}{% autoescape off %} +

+{% blocktrans %}You're receiving this email because you try to access in {{ site_name }}.{% endblocktrans %} +

+ +

+{% trans "Please go to the following page" %} +

+ +

+{% block reset_link %} + +{{ protocol }}://{{ domain }}{% url 'idhub:admin_2fauth' admin2fauth=token %} + +{% endblock %} +

+ +

+{% trans "Thanks for using our site!" %} +

+ +

+{% blocktrans %}The {{ site_name }} team{% endblocktrans %} +

+ +{% endautoescape %} diff --git a/idhub/templates/auth/2fadmin_email.txt b/idhub/templates/auth/2fadmin_email.txt new file mode 100644 index 0000000..a9ef3e5 --- /dev/null +++ b/idhub/templates/auth/2fadmin_email.txt @@ -0,0 +1,14 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}You're receiving this email because you try to access in {{ site_name }}.{% endblocktrans %} + +{% trans "Please go to the following page" %} +{% block reset_link %} +{{ protocol }}://{{ domain }}{% url 'idhub:admin_2fauth' admin2fauth=token %} +{% endblock %} +{% trans "Your username, in case you've forgotten:" %} {{ user.username }} + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} + +{% endautoescape %} diff --git a/idhub/templates/auth/2fadmin_email_subject.txt b/idhub/templates/auth/2fadmin_email_subject.txt new file mode 100644 index 0000000..6d3bb21 --- /dev/null +++ b/idhub/templates/auth/2fadmin_email_subject.txt @@ -0,0 +1,3 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}Authentication in {{ site_name }}{% endblocktrans %} +{% endautoescape %} \ No newline at end of file diff --git a/idhub/templates/auth/login.html b/idhub/templates/auth/login.html index 28c199e..18abfd8 100644 --- a/idhub/templates/auth/login.html +++ b/idhub/templates/auth/login.html @@ -4,8 +4,6 @@ {% block login_content %}
{% csrf_token %} - -
@@ -48,7 +46,7 @@ class="btn btn-primary form-control" id="submit-id-submit">
-
-
-
- {% trans 'View in JSON format' %} -
-
+ + +
+ {% if object.eidas1_did and admin_validated %} +
+ {% trans 'Sign credential in PDF format' %} +
+ {% endif %} +
+ {% trans 'View credential in JSON format' %}
{% endblock %} diff --git a/idhub/templates/idhub/user/gdpr.html b/idhub/templates/idhub/user/gdpr.html index 1ac25fd..16476b8 100644 --- a/idhub/templates/idhub/user/gdpr.html +++ b/idhub/templates/idhub/user/gdpr.html @@ -6,4 +6,7 @@ {{ subtitle }} +Gdpr info
+If you want accept or revoke the Gdpr go to: + Terms and conditions {% endblock %} diff --git a/idhub/templates/idhub/user/terms_conditions.html b/idhub/templates/idhub/user/terms_conditions.html new file mode 100644 index 0000000..8a02175 --- /dev/null +++ b/idhub/templates/idhub/user/terms_conditions.html @@ -0,0 +1,57 @@ +{% extends "idhub/base.html" %} +{% load i18n %} + +{% block content %} +

+ + {{ subtitle }} +

+{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +
+
+ You must read the terms and conditions of this service and accept the + Read GDPR +
+
+
+
+ {% bootstrap_form form %} +
+
+
+ {% translate "Cancel" %} + +
+ +
+ + +{% endblock %} diff --git a/idhub/templates/templates/musician/address_check_delete.html b/idhub/templates/templates/musician/address_check_delete.html deleted file mode 100644 index 27981d4..0000000 --- a/idhub/templates/templates/musician/address_check_delete.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} -
- {% csrf_token %} -

{% blocktrans with address_name=object.full_address_name %}Are you sure that you want remove the address: "{{ address_name }}"?{% endblocktrans %}

-

{% trans 'WARNING: This action cannot be undone.' %}

- - {% trans 'Cancel' %} -
-{% endblock %} diff --git a/idhub/templates/templates/musician/address_form.html b/idhub/templates/templates/musician/address_form.html deleted file mode 100644 index de21067..0000000 --- a/idhub/templates/templates/musician/address_form.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "musician/base.html" %} -{% load bootstrap4 i18n %} - -{% block content %} -

{{ service.verbose_name }}

- -
- {% csrf_token %} - {% bootstrap_form form %} - {% buttons %} - {% trans "Cancel" %} - - {% if form.instance %} -
- {% trans "Delete" %} -
- {% endif %} - {% endbuttons %} -
-{% endblock %} diff --git a/idhub/templates/templates/musician/addresses.html b/idhub/templates/templates/musician/addresses.html deleted file mode 100644 index 1ebc8b7..0000000 --- a/idhub/templates/templates/musician/addresses.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "musician/mail_base.html" %} -{% load i18n %} - -{% block tabcontent %} -
- - - - - - - - - - - - - - - - - {% for obj in object_list %} - - - - - - - {% endfor %} - - {% include "musician/components/table_paginator.html" %} -
{% trans "Email" %}{% trans "Domain" %}{% trans "Mailboxes" %}{% trans "Forward" %}
{{ obj.full_address_name }}{{ obj.domain.name }} - {% for mailbox in obj.mailboxes %} - {{ mailbox.name }} - {% if not forloop.last %}
{% endif %} - {% endfor %} -
{{ obj.forward }}
- {% trans "New mail address" %} - -
-{% endblock %} diff --git a/idhub/templates/templates/musician/billing.html b/idhub/templates/templates/musician/billing.html deleted file mode 100644 index 75f2580..0000000 --- a/idhub/templates/templates/musician/billing.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n l10n %} - -{% block content %} - -

{% trans "Billing" %}

-

{% trans "Billing page description." %}

- - - - - - - - - - - - - - - - - - - - {% for bill in object_list %} - - - - - - - - {% endfor %} - -{# TODO: define proper colspan #} -{% include "musician/components/table_paginator.html" %} -
{% trans "Number" %}{% trans "Bill date" %}{% trans "Type" %}{% trans "Total" %}{% trans "Download PDF" %}
{{ bill.number }}{{ bill.created_on|date:"SHORT_DATE_FORMAT" }}{{ bill.type }}{{ bill.total|floatformat:2|localize }}€
- -{% endblock %} diff --git a/idhub/templates/templates/musician/components/paginator.html b/idhub/templates/templates/musician/components/paginator.html deleted file mode 100644 index 9a7a406..0000000 --- a/idhub/templates/templates/musician/components/paginator.html +++ /dev/null @@ -1,29 +0,0 @@ -{# #} -
-
{{ page_obj.paginator.count }} items in total
-
- {% if page_obj.has_previous %} - « - - {% endif %} - Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} - {% if page_obj.has_next %} - - » - {% endif %} -
-
-
- Showing - - per page - -
-
-
diff --git a/idhub/templates/templates/musician/components/table_paginator.html b/idhub/templates/templates/musician/components/table_paginator.html deleted file mode 100644 index 913d5ae..0000000 --- a/idhub/templates/templates/musician/components/table_paginator.html +++ /dev/null @@ -1,50 +0,0 @@ -{# #} -{% load i18n %} - - - - {{ page_obj.paginator.count }} items in total - - - - -
- Showing - - per page - -
- - - diff --git a/idhub/templates/templates/musician/components/usage_progress_bar.html b/idhub/templates/templates/musician/components/usage_progress_bar.html deleted file mode 100644 index b35e84c..0000000 --- a/idhub/templates/templates/musician/components/usage_progress_bar.html +++ /dev/null @@ -1,22 +0,0 @@ -{% comment %} -Resource usage rendered as bootstrap progress bar - -Expected parameter: detail -Expected structure: dictionary or object with attributes: - - usage (int): 125 - - total (int): 200 - - unit (string): 'MB' - - percent (int: [0, 25, 50, 75, 100]: 75 -{% endcomment %} - -
- {% if detail %} - {{ detail.usage }} {{ detail.unit }} - {% else %} - N/A - {% endif %} -
-
-
-
diff --git a/idhub/templates/templates/musician/dashboard.html b/idhub/templates/templates/musician/dashboard.html deleted file mode 100644 index d359fcb..0000000 --- a/idhub/templates/templates/musician/dashboard.html +++ /dev/null @@ -1,161 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} - -

{% trans "Welcome back" %} {{ profile.username }}

-{% if profile.last_login %} -

{% blocktrans with last_login=profile.last_login|date:"SHORT_DATE_FORMAT" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}

-{% else %} -

{% trans "It's the first time you log into the system, welcome on board!" %}

-{% endif %} - -
- {% for resource, usage in resource_usage.items %} -
-
-
{{ usage.verbose_name }}
- {% include "musician/components/usage_progress_bar.html" with detail=usage.data %} - {% if usage.data.alert %} -
- {{ usage.data.alert }} -
- {% endif %} -
-
- {% endfor %} -
-
-
{% trans "Notifications" %}
- {% for message in notifications %} -

{{ message }}

- {% empty %} -

{% trans "There is no notifications at this time." %}

- {% endfor %} -
-
-
- - -

{% trans "Your domains and websites" %}

-

{% trans "Dashboard page description." %}

- -{% for domain in domains %} -
-
-
-
- {{ domain.name }} -
-
- {% with domain.websites.0 as website %} - {% with website.contents.0 as content %} - - {% endwith %} - {% endwith %} -
-
- {% comment "@slamora: orchestra doesn't have this information [won't fix] See issue #2" %} - {% trans "Expiration date" %}: {{ domain.expiration_date|date:"SHORT_DATE_FORMAT" }} - {% endcomment %} -
-
-
-
-
-

{% trans "Mail" %}

-

-

- {{ domain.addresses|length }} {% trans "mail addresses created" %} -

- -
-
-

{% trans "Mail list" %}

-

- -
-
-

{% trans "Software as a Service" %}

-

-

{% trans "Nothing installed" %}

- -
-
-
-

{% trans "Disk usage" %}

-

-
- {% include "musician/components/usage_progress_bar.html" with detail=domain.usage %} -
-
-
-
-
- -{% endfor %} - - - -{% endblock %} -{% block extrascript %} - -{% endblock %} diff --git a/idhub/templates/templates/musician/databases.html b/idhub/templates/templates/musician/databases.html deleted file mode 100644 index cf71d1f..0000000 --- a/idhub/templates/templates/musician/databases.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} - -

{{ service.verbose_name }}

-

{{ service.description }}

- -{% for database in object_list %} -
-
-
-
- {{ database.name }} -
-
- {% trans "Type" %}: {{ database.type }} -
-
- {% comment "@slamora: orchestra doesn't provide this information [won't fix] See issue #3" %} - {% trans "associated to" %}: {{ database.domain|default:"-" }} - {% endcomment %} -
-
-
-
-
-

Database users

-
    - {% for user in database.users %} - {# TODO(@slamora) render in two columns #} -
  • {{ user.username }}
  • - {% empty %} -
  • {% trans "No users for this database." %}
  • - {% endfor %} -
-
-
-

Database usage

-

- {% include "musician/components/usage_progress_bar.html" with detail=database.usage %} -
- -
-
- -{% empty %} -
-
-
-
-

- {# Translators: database page when there isn't any database. #} -
{% trans "Ooops! Looks like there is nothing here!" %}
-
-
-
-
-{% endfor %} - - {% if object_list|length > 0 %} - {% include "musician/components/paginator.html" %} - {% endif %} -{% endblock %} diff --git a/idhub/templates/templates/musician/domain_detail.html b/idhub/templates/templates/musician/domain_detail.html deleted file mode 100644 index 761c331..0000000 --- a/idhub/templates/templates/musician/domain_detail.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} -{% trans "Go back" %} - -

{% trans "DNS settings for" %} {{ object.name }}

-

{% trans "DNS settings page description." %}

- - - - - - - - - - - - - - {% for record in object.records %} - - - - - {% endfor %} - -
{% trans "Type" %}{% trans "Value" %}
{{ record.type }}{{ record.value }}
-{% endblock %} diff --git a/idhub/templates/templates/musician/mail_base.html b/idhub/templates/templates/musician/mail_base.html deleted file mode 100644 index 9445f7f..0000000 --- a/idhub/templates/templates/musician/mail_base.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} -{% if active_domain %} -{% trans "Go to global" %} -{% endif %} - -

{{ service.verbose_name }} - {% if active_domain %}{% trans "for" %} {{ active_domain.name }}{% endif %} -

-

{{ service.description }}

- -{% with request.resolver_match.url_name as url_name %} - - -{% endwith %} - -
- {% block tabcontent %} - {% endblock %} - -{% endblock %} diff --git a/idhub/templates/templates/musician/mailbox_change_password.html b/idhub/templates/templates/musician/mailbox_change_password.html deleted file mode 100644 index e18b95a..0000000 --- a/idhub/templates/templates/musician/mailbox_change_password.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "musician/base.html" %} -{% load bootstrap4 i18n %} - -{% block content %} -

{% trans "Change password" %}: {{ object.name }}

- -
- {% csrf_token %} - {% bootstrap_form form %} - {% buttons %} - {% trans "Cancel" %} - - {% endbuttons %} -
-{% endblock %} diff --git a/idhub/templates/templates/musician/mailbox_check_delete.html b/idhub/templates/templates/musician/mailbox_check_delete.html deleted file mode 100644 index 18b9249..0000000 --- a/idhub/templates/templates/musician/mailbox_check_delete.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} -
- {% csrf_token %} -

{% blocktrans with name=object.name %}Are you sure that you want remove the mailbox: "{{ name }}"?{% endblocktrans %}

- -

{% trans 'WARNING: This action cannot be undone.' %}

- - {% trans 'Cancel' %} -
-{% endblock %} diff --git a/idhub/templates/templates/musician/mailbox_form.html b/idhub/templates/templates/musician/mailbox_form.html deleted file mode 100644 index 5fb9465..0000000 --- a/idhub/templates/templates/musician/mailbox_form.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "musician/base.html" %} -{% load bootstrap4 i18n %} - -{% block content %} -

{{ service.verbose_name }}

- -{% if extra_mailbox %} - -{% endif %} - -
- {% csrf_token %} - {% bootstrap_form form %} - {% buttons %} - {% trans "Cancel" %} - - {% if form.instance %} - - {% endif %} - {% endbuttons %} -
-{% endblock %} diff --git a/idhub/templates/templates/musician/mailboxes.html b/idhub/templates/templates/musician/mailboxes.html deleted file mode 100644 index a9f3700..0000000 --- a/idhub/templates/templates/musician/mailboxes.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "musician/mail_base.html" %} -{% load i18n %} - -{% block tabcontent %} -
- - - - - - - - - - - - - - - {% for mailbox in object_list %} - {# #} - {% if mailbox.is_active %} - - - - - - {% endif %}{# #} - {% endfor %} - - {% include "musician/components/table_paginator.html" %} -
{% trans "Name" %}{% trans "Filtering" %}{% trans "Addresses" %}
- {{ mailbox.name }} - - {% trans "Update password" %} - {{ mailbox.filtering }} - {% for addr in mailbox.addresses %} - - {{ addr.full_address_name }} -
- {% endfor %} -
- {% trans "New mailbox" %} - -
-{% endblock %} diff --git a/idhub/templates/templates/musician/mailinglists.html b/idhub/templates/templates/musician/mailinglists.html deleted file mode 100644 index 6ff509e..0000000 --- a/idhub/templates/templates/musician/mailinglists.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} -{% if active_domain %} -{% trans "Go to global" %} -{% endif %} - -

{{ service.verbose_name }}{% if active_domain %} {% trans "for" %} {{ active_domain.name }}{% endif %}

-

{{ service.description }}

- - - - - - - - - - - - - - - - - - - - {% for resource in object_list %} - - - {% if resource.is_active %} - - {% else %} - - {% endif %} - - - - - {% endfor %} - - {% include "musician/components/table_paginator.html" %} -
NameStatusAddressAdmin emailConfigure
{{ resource.name }}{% trans "Active" %}{% trans "Inactive" %}{{ resource.address_name}}{{ resource.admin_email }}Mailtrain
-{% endblock %} diff --git a/idhub/templates/templates/musician/profile.html b/idhub/templates/templates/musician/profile.html deleted file mode 100644 index f6bdbce..0000000 --- a/idhub/templates/templates/musician/profile.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} - -

{% trans "Profile" %}

-

{% trans "Little description on profile page." %}

- -
-
-
{% trans "User information" %}
-
-
-
- user-profile-picture -
-
-
-

{{ profile.username }}

-

{{ profile.type }}

-

{% trans "Preferred language:" %} {{ profile.language|language_name_local }}

-
- {% comment %} - - - {% endcomment %} -
-
- - {% with profile.billing as contact %} -
-
{% trans "Billing information" %}
-
-
{{ contact.name }}
-
{{ contact.address }}
-
- {{ contact.zipcode }} - {{ contact.city }} - {{ contact.country }} -
-
- {{ contact.vat }} -
- -
- {% trans "payment method:" %} {{ payment.method }} -
-
- {% if payment.method == 'SEPADirectDebit' %} - IBAN {{ payment.data.iban }} - {% else %} - {# #} - Details: {{ payment.data }} - {% endif %} -
- -
-
-
-{% endwith %} -{% endblock %} diff --git a/idhub/templates/templates/musician/saas.html b/idhub/templates/templates/musician/saas.html deleted file mode 100644 index 4da034f..0000000 --- a/idhub/templates/templates/musician/saas.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n %} - -{% block content %} - -

{{ service.verbose_name }}

-

{{ service.description }}

- -{% for saas in object_list %} -
-
-
-
- {{ saas.name }} -
- {% comment "Hidden until API provides this information" %} -
- {% trans "Installed on" %}: {{ saas.domain|default:"-" }} -
- {% endcomment %} -
-
-
-
-

{{ saas.service|capfirst }}

-

-
-
-

{% trans "Service info" %}

- {{ saas.is_active|yesno }}
- {% for key, value in saas.data.items %} - {{ value }}
- {% endfor %} -
- -
-
- {% empty %} -
-
-
-
-

- {# Translators: saas page when there isn't any saas. #} -
{% trans "Ooops! Looks like there is nothing here!" %}
-
-
-
-
-{% endfor %} - -{% endblock %} diff --git a/idhub/templates/templates/musician/service_list.html b/idhub/templates/templates/musician/service_list.html deleted file mode 100644 index d413fc8..0000000 --- a/idhub/templates/templates/musician/service_list.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "musician/base.html" %} -{% load i18n musician %} - -{% block content %} - -

{{ service.verbose_name }}

-

{{ service.description }}

- - - - - {% for field_name in service.fields %} - - {% endfor %} - - - - {% for resource in object_list %} - - {% for field_name in service.fields %} - - {% endfor %} - - {% endfor %} - - {% include "musician/components/table_paginator.html" %} -
{{ field_name }}
{{ resource|get_item:field_name }}
-{% endblock %} diff --git a/idhub/urls.py b/idhub/urls.py index d139c32..d107b3f 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -17,7 +17,12 @@ Including another URLconf from django.contrib.auth import views as auth_views from django.views.generic import RedirectView from django.urls import path, reverse_lazy -from .views import LoginView +from .views import ( + LoginView, + PasswordResetConfirmView, + serve_did, + DobleFactorSendView, +) from .admin import views as views_admin from .user import views as views_user # from .verification_portal import views as views_verification_portal @@ -45,13 +50,16 @@ urlpatterns = [ ), name='password_reset_done' ), - path('auth/reset///', - auth_views.PasswordResetConfirmView.as_view( - template_name='auth/password_reset_confirm.html', - success_url=reverse_lazy('idhub:password_reset_complete') - ), + path('auth/reset///', PasswordResetConfirmView.as_view(), name='password_reset_confirm' ), + # path('auth/reset///', + # auth_views.PasswordResetConfirmView.as_view( + # template_name='auth/password_reset_confirm.html', + # success_url=reverse_lazy('idhub:password_reset_complete') + # ), + # name='password_reset_confirm' + # ), path('auth/reset/done/', auth_views.PasswordResetCompleteView.as_view( template_name='auth/password_reset_complete.html' @@ -80,14 +88,20 @@ urlpatterns = [ name='user_credentials'), path('user/credentials/', views_user.CredentialView.as_view(), name='user_credential'), - path('user/credentials//json', views_user.CredentialJsonView.as_view(), + path('user/credentials//pdf', views_user.CredentialPdfView.as_view(), + name='user_credential_pdf'), + path('credentials//', views_user.CredentialJsonView.as_view(), name='user_credential_json'), + path('public/credentials//', views_user.PublicCredentialJsonView.as_view(), + name='public_credential_json'), path('user/credentials/request/', views_user.CredentialsRequestView.as_view(), name='user_credentials_request'), path('user/credentials_presentation/demand', views_user.DemandAuthorizationView.as_view(), name='user_demand_authorization'), + path('user/terms/', views_user.TermsAndConditionsView.as_view(), + name='user_terms_and_conditions'), # Admin path('admin/dashboard/', views_admin.DashboardView.as_view(), @@ -170,8 +184,15 @@ urlpatterns = [ name='admin_schemas_import_add'), path('admin/import', views_admin.ImportView.as_view(), name='admin_import'), + path('admin/terms/', views_admin.TermsAndConditionsView.as_view(), + name='admin_terms_and_conditions'), path('admin/import/new', views_admin.ImportAddView.as_view(), name='admin_import_add'), + path('admin/auth/', views_admin.DobleFactorAuthView.as_view(), + name='admin_2fauth'), + path('admin/auth/2f/', DobleFactorSendView.as_view(), name='confirm_send_2f'), + + path('did-registry//did.json', serve_did) # path('verification_portal/verify/', views_verification_portal.verify, # name="verification_portal_verify") diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 07b134d..9c68ff3 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -5,18 +5,56 @@ from idhub.models import DID, VerificableCredential from oidc4vp.models import Organization +class ProfileForm(forms.ModelForm): + MANDATORY_FIELDS = ['first_name', 'last_name', 'email'] + + class Meta: + model = User + fields = ('first_name', 'last_name', 'email') + + +class TermsConditionsForm(forms.Form): + accept = forms.BooleanField( + label=_("Accept terms and conditions of the service"), + required=False + ) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + + def clean(self): + data = self.cleaned_data + if data.get("accept"): + self.user.accept_gdpr = True + else: + self.user.accept_gdpr = False + return data + + def save(self, commit=True): + + if commit: + self.user.save() + return self.user + + return + + class RequestCredentialForm(forms.Form): did = forms.ChoiceField(label=_("Did"), choices=[]) credential = forms.ChoiceField(label=_("Credential"), choices=[]) def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) + self.lang = kwargs.pop('lang', None) + self._domain = kwargs.pop('domain', None) + self.password = kwargs.pop('password', None) super().__init__(*args, **kwargs) self.fields['did'].choices = [ (x.did, x.label) for x in DID.objects.filter(user=self.user) ] self.fields['credential'].choices = [ - (x.id, x.type()) for x in VerificableCredential.objects.filter( + (x.id, x.get_type(lang=self.lang)) for x in VerificableCredential.objects.filter( user=self.user, status=VerificableCredential.Status.ENABLED ) @@ -38,7 +76,8 @@ class RequestCredentialForm(forms.Form): did = did[0] cred = cred[0] try: - cred.issue(did) + if self.password: + cred.issue(did, self.password, domain=self._domain) except Exception: return diff --git a/idhub/user/views.py b/idhub/user/views.py index 07430f5..82c5337 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -1,6 +1,20 @@ +import os +import json +import base64 +import qrcode import logging +import datetime +import weasyprint +import qrcode.image.svg +from io import BytesIO +from pathlib import Path +from pyhanko.sign import fields, signers +from pyhanko import stamp +from pyhanko.pdf_utils import text +from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter from django.utils.translation import gettext_lazy as _ +from django.views.generic import View from django.views.generic.edit import ( UpdateView, CreateView, @@ -20,10 +34,15 @@ from idhub.user.tables import ( DIDTable, CredentialsTable ) +from django.core.cache import cache +from django.conf import settings from idhub.user.forms import ( - RequestCredentialForm, - DemandAuthorizationForm + ProfileForm, + RequestCredentialForm, + DemandAuthorizationForm, + TermsConditionsForm ) +from utils import certs from idhub.mixins import UserView from idhub.models import DID, VerificableCredential, Event, Membership from idhub_auth.models import User @@ -113,6 +132,26 @@ class CredentialsView(MyWallet, SingleTableView): return queryset + +class TermsAndConditionsView(UserView, FormView): + template_name = "idhub/user/terms_conditions.html" + title = _("GDPR") + section = "" + subtitle = _('Accept Terms and Conditions') + icon = 'bi bi-file-earmark-medical' + form_class = TermsConditionsForm + success_url = reverse_lazy('idhub:user_dashboard') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + kwargs['initial'] = {"accept": self.request.user.accept_gdpr} + return kwargs + + def form_valid(self, form): + user = form.save() + return super().form_valid(form) + class CredentialView(MyWallet, TemplateView): template_name = "idhub/user/credential.html" @@ -136,6 +175,147 @@ class CredentialView(MyWallet, TemplateView): return context +class CredentialPdfView(MyWallet, TemplateView): + template_name = "certificates/4_Model_Certificat.html" + subtitle = _('Credential management') + icon = 'bi bi-patch-check-fill' + file_name = "certificate.pdf" + + def get(self, request, *args, **kwargs): + self.admin_validated = cache.get("KEY_DIDS") + pk = kwargs['pk'] + self.user = self.request.user + self.object = get_object_or_404( + VerificableCredential, + pk=pk, + eidas1_did__isnull=False, + user=self.request.user + ) + self.url_id = "{}://{}/public/credentials/{}".format( + self.request.scheme, + self.request.get_host(), + self.object.hash + ) + + data = self.build_certificate() + if self.object.eidas1_did: + doc = self.insert_signature(data) + else: + doc = data + response = HttpResponse(doc, content_type="application/pdf") + response['Content-Disposition'] = 'attachment; filename={}'.format(self.file_name) + return response + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + this_folder = str(Path.cwd()) + path_img_sig = "idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg" + img_signature = next(Path.cwd().glob(path_img_sig)) + with open(img_signature, 'rb') as _f: + img_sig = base64.b64encode(_f.read()).decode('utf-8') + + path_img_head = "idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg" + img_header= next(Path.cwd().glob(path_img_head)) + with open(img_header, 'rb') as _f: + img_head = base64.b64encode(_f.read()).decode('utf-8') + + qr = self.generate_qr_code(self.url_id) + + first_name = self.user.first_name and self.user.first_name.upper() or "" + last_name = self.user.first_name and self.user.last_name.upper() or "" + document_id = "0000000-L" + course = "COURSE 1" + address = "ADDRESS" + date_course = datetime.datetime.now() + n_hours = 40 + n_lections = 5 + issue_date = datetime.datetime.now() + context.update({ + 'object': self.object, + "image_signature": img_sig, + "image_header": img_head, + "first_name": first_name, + "last_name": last_name, + "document_id": document_id, + "course": course, + "address": address, + "date_course": date_course, + "n_hours": n_hours, + "n_lections": n_lections, + "issue_date": issue_date, + "qr": qr + }) + return context + + def build_certificate(self): + doc = self.render_to_response(context=self.get_context_data()) + doc.render() + pdf = weasyprint.HTML(string=doc.content) + return pdf.write_pdf() + + def generate_qr_code(self, data): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(data) + qr.make(fit=True) + img_buffer = BytesIO() + img = qr.make_image(fill_color="black", back_color="white") + img.save(img_buffer, format="PNG") + + return base64.b64encode(img_buffer.getvalue()).decode('utf-8') + + def get_pfx_data(self): + did = self.object.eidas1_did + pw = self.admin_validated + if not did or not pw: + return None, None + key_material = json.loads(did.get_key_material(pw)) + cert = key_material.get("cert") + passphrase = key_material.get("passphrase") + if cert and passphrase: + return base64.b64decode(cert), passphrase.encode('utf-8') + return None, None + + + def signer_init(self): + pfx_data, passphrase = self.get_pfx_data() + if not pfx_data or not passphrase: + return + s = certs.load_cert( + pfx_data, passphrase + ) + return s + + def insert_signature(self, doc): + sig = self.signer_init() + if not sig: + return + + _buffer = BytesIO() + _buffer.write(doc) + w = IncrementalPdfFileWriter(_buffer) + fields.append_signature_field( + w, sig_field_spec=fields.SigFieldSpec( + 'Signature', box=(150, 100, 450, 150) + ) + ) + + meta = signers.PdfSignatureMetadata(field_name='Signature') + pdf_signer = signers.PdfSigner( + meta, signer=sig, stamp_style=stamp.TextStampStyle( + stamp_text='Signed by: %(signer)s\nTime: %(ts)s\nURL: %(url)s', + text_box_style=text.TextBoxStyle() + ) + ) + _bf_out = BytesIO() + pdf_signer.sign_pdf(w, output=_bf_out, appearance_text_params={'url': self.url_id}) + return _bf_out.read() + + class CredentialJsonView(MyWallet, TemplateView): def get(self, request, *args, **kwargs): @@ -145,6 +325,28 @@ class CredentialJsonView(MyWallet, TemplateView): pk=pk, user=self.request.user ) + pass_enc = self.request.session.get("key_did") + data = "" + if pass_enc: + user_pass = self.request.user.decrypt_data( + pass_enc, + self.request.user.password+self.request.session._session_key + ) + data = self.object.get_data(user_pass) + response = HttpResponse(data, content_type="application/json") + response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json") + return response + + +class PublicCredentialJsonView(View): + + def get(self, request, *args, **kwargs): + pk = kwargs['pk'] + self.object = get_object_or_404( + VerificableCredential, + hash=pk, + eidas1_did__isnull=False, + ) response = HttpResponse(self.object.data, content_type="application/json") response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json") return response @@ -157,9 +359,27 @@ class CredentialsRequestView(MyWallet, FormView): form_class = RequestCredentialForm success_url = reverse_lazy('idhub:user_credentials') + def get(self, request, *args, **kwargs): + response = super().get(request, *args, **kwargs) + if not self.admin_validated: + return redirect(reverse_lazy('idhub:user_dashboard')) + return response + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user + kwargs['lang'] = self.request.LANGUAGE_CODE + domain = "{}://{}".format(self.request.scheme, self.request.get_host()) + kwargs['domain'] = domain + pass_enc = self.request.session.get("key_did") + if pass_enc: + user_pass = self.request.user.decrypt_data( + pass_enc, + self.request.user.password+self.request.session._session_key + ) + else: + pass_enc = None + kwargs['password'] = user_pass return kwargs def form_valid(self, form): @@ -230,13 +450,17 @@ class DidRegisterView(MyWallet, CreateView): icon = 'bi bi-patch-check-fill' wallet = True model = DID - fields = ('label',) + fields = ('label', 'type') success_url = reverse_lazy('idhub:user_dids') object = None def form_valid(self, form): form.instance.user = self.request.user - form.instance.set_did() + pw = self.request.user.decrypt_data( + self.request.session.get("key_did"), + self.request.user.password+self.request.session._session_key + ) + form.instance.set_did(pw) form.save() messages.success(self.request, _('DID created successfully')) diff --git a/idhub/verification_portal/__init__.py b/idhub/verification_portal/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/idhub/verification_portal/models.py b/idhub/verification_portal/models.py deleted file mode 100644 index 0bd203a..0000000 --- a/idhub/verification_portal/models.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.db import models - - -class VPVerifyRequest(models.Model): - """ - `nonce` is an opaque random string used to lookup verification requests. URL-safe. - Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ" - `expected_credentials` is a JSON list of credential types that must be present in this VP. - Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] - `expected_contents` is a JSON object that places optional constraints on the contents of the - returned VP. - Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}] - `action` is (for now) a JSON object describing the next steps to take if this verification - is successful. For example "send mail to with and " - Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} - `response` is a URL that the user's wallet will redirect the user to. - `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification - """ - nonce = models.CharField(max_length=50) - expected_credentials = models.CharField(max_length=255) - expected_contents = models.TextField() - action = models.TextField() - response_or_redirect = models.CharField(max_length=255) - submitted_on = models.DateTimeField(auto_now=True) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py deleted file mode 100644 index 486f4f7..0000000 --- a/idhub/verification_portal/views.py +++ /dev/null @@ -1,49 +0,0 @@ -import json - -from django.core.mail import send_mail -from django.http import HttpResponse, HttpResponseRedirect - -from utils.idhub_ssikit import verify_presentation -from .models import VPVerifyRequest -from django.shortcuts import get_object_or_404 -from more_itertools import flatten, unique_everseen - - -def verify(request): - assert request.method == "POST" - # TODO: incorporate request.POST["presentation_submission"] as schema definition - (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) - if not presentation_valid: - raise Exception("Failed to verify signature on the given Verifiable Presentation.") - vp = json.loads(request.POST["vp_token"]) - nonce = vp["nonce"] - # "vr" = verification_request - vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 - # Get a list of all included verifiable credential types - included_credential_types = unique_everseen(flatten([ - vc["type"] for vc in vp["verifiableCredential"] - ])) - # Check that it matches what we requested - for requested_vc_type in json.loads(vr.expected_credentials): - if requested_vc_type not in included_credential_types: - raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error - # Perform whatever action we have to do - action = json.loads(vr.action) - if action["action"] == "send_mail": - subject = action["params"]["subject"] - to_email = action["params"]["to"] - from_email = "noreply@verifier-portal" - body = request.POST["vp-token"] - send_mail( - subject, - body, - from_email, - [to_email] - ) - elif action["action"] == "something-else": - pass - else: - raise Exception("Unknown action!") - # OK! Your verifiable presentation was successfully presented. - return HttpResponseRedirect(vr.response_or_redirect) - diff --git a/idhub/views.py b/idhub/views.py index 53db736..7a525b1 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -1,8 +1,19 @@ +import uuid + +from django.conf import settings +from django.core.cache import cache from django.urls import reverse_lazy -from django.utils.translation import gettext_lazy as _ +from django.views.generic.base import TemplateView from django.contrib.auth import views as auth_views from django.contrib.auth import login as auth_login -from django.http import HttpResponseRedirect +from django.utils.translation import gettext_lazy as _ +from django.shortcuts import get_object_or_404, redirect +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponseRedirect, HttpResponse, Http404 + +from idhub.models import DID +from idhub.email.views import NotifyActivateUserByEmail +from trustchain_idhub import settings class LoginView(auth_views.LoginView): @@ -13,16 +24,76 @@ class LoginView(auth_views.LoginView): } def get(self, request, *args, **kwargs): - if request.GET.get('next'): - self.extra_context['success_url'] = request.GET.get('next') + self.extra_context['success_url'] = request.GET.get( + 'next', + reverse_lazy('idhub:user_dashboard') + ) return super().get(request, *args, **kwargs) def form_valid(self, form): user = form.get_user() - if not user.is_anonymous and user.is_admin: - user_dashboard = reverse_lazy('idhub:user_dashboard') - admin_dashboard = reverse_lazy('idhub:admin_dashboard') - if self.extra_context['success_url'] == user_dashboard: - self.extra_context['success_url'] = admin_dashboard + password = form.cleaned_data.get("password") auth_login(self.request, user) + + sensitive_data_encryption_key = user.decrypt_sensitive_data(password) + + if not user.is_anonymous and user.is_admin: + admin_dashboard = reverse_lazy('idhub:admin_dashboard') + self.extra_context['success_url'] = admin_dashboard + # encryption_key = user.encrypt_data( + # sensitive_data_encryption_key, + # settings.SECRET_KEY + # ) + # cache.set("KEY_DIDS", encryption_key, None) + cache.set("KEY_DIDS", sensitive_data_encryption_key, None) + if not settings.DEVELOPMENT: + self.request.session["2fauth"] = str(uuid.uuid4()) + return redirect(reverse_lazy('idhub:confirm_send_2f')) + + self.request.session["key_did"] = user.encrypt_data( + sensitive_data_encryption_key, + user.password+self.request.session._session_key + ) + return HttpResponseRedirect(self.extra_context['success_url']) + + +class PasswordResetConfirmView(auth_views.PasswordResetConfirmView): + template_name = 'auth/password_reset_confirm.html' + success_url = reverse_lazy('idhub:password_reset_complete') + + def form_valid(self, form): + password = form.cleaned_data.get("password") + user = form.get_user() + user.set_encrypted_sensitive_data(password) + user.save() + return HttpResponseRedirect(self.success_url) + + +def serve_did(request, did_id): + id_did = f'did:web:{settings.DOMAIN}:did-registry:{did_id}' + did = get_object_or_404(DID, did=id_did) + document = did.didweb_document + retval = HttpResponse(document) + retval.headers["Content-Type"] = "application/json" + return retval + + +class DobleFactorSendView(LoginRequiredMixin, NotifyActivateUserByEmail, TemplateView): + template_name = 'auth/2fadmin.html' + subject_template_name = 'auth/2fadmin_email_subject.txt' + email_template_name = 'auth/2fadmin_email.txt' + html_email_template_name = 'auth/2fadmin_email.html' + + def get(self, request, *args, **kwargs): + if not request.user.is_admin: + raise Http404 + + f2auth = self.request.session.get("2fauth") + if not f2auth: + raise Http404 + + self.send_email(self.request.user, token=f2auth) + return super().get(request, *args, **kwargs) + + diff --git a/idhub_auth/forms.py b/idhub_auth/forms.py index f4279b7..d9ff2f7 100644 --- a/idhub_auth/forms.py +++ b/idhub_auth/forms.py @@ -31,4 +31,3 @@ class ProfileForm(forms.ModelForm): return last_name - diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py deleted file mode 100644 index bd9b3eb..0000000 --- a/idhub_auth/migrations/0001_initial.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 4.2.5 on 2024-01-22 12:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('password', models.CharField(max_length=128, verbose_name='password')), - ( - 'last_login', - models.DateTimeField( - blank=True, null=True, verbose_name='last login' - ), - ), - ( - 'email', - models.EmailField( - max_length=255, unique=True, verbose_name='Email address' - ), - ), - ('is_active', models.BooleanField(default=True)), - ('is_admin', models.BooleanField(default=False)), - ( - 'first_name', - models.CharField( - blank=True, max_length=255, null=True, verbose_name='First name' - ), - ), - ( - 'last_name', - models.CharField( - blank=True, max_length=255, null=True, verbose_name='Last name' - ), - ), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/idhub_auth/models.py b/idhub_auth/models.py index 07a7896..e89bc35 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -1,4 +1,9 @@ +import nacl +import base64 + +from nacl import pwhash from django.db import models +from django.core.cache import cache from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import BaseUserManager, AbstractBaseUser @@ -44,6 +49,9 @@ class User(AbstractBaseUser): is_admin = models.BooleanField(default=False) first_name = models.CharField(_("First name"), max_length=255, blank=True, null=True) last_name = models.CharField(_("Last name"), max_length=255, blank=True, null=True) + encrypted_sensitive_data = models.CharField(max_length=255) + salt = models.CharField(max_length=255) + accept_gdpr = models.BooleanField(default=False) objects = UserManager() @@ -86,3 +94,64 @@ class User(AbstractBaseUser): for r in s.service.rol.all(): roles.append(r.name) return ", ".join(set(roles)) + + def derive_key_from_password(self, password): + kdf = pwhash.argon2i.kdf + ops = pwhash.argon2i.OPSLIMIT_INTERACTIVE + mem = pwhash.argon2i.MEMLIMIT_INTERACTIVE + return kdf( + nacl.secret.SecretBox.KEY_SIZE, + password, + self.get_salt(), + opslimit=ops, + memlimit=mem + ) + + def decrypt_sensitive_data(self, password, data=None): + sb_key = self.derive_key_from_password(password.encode('utf-8')) + sb = nacl.secret.SecretBox(sb_key) + if not data: + data = self.get_encrypted_sensitive_data() + if not isinstance(data, bytes): + data = data.encode('utf-8') + + return sb.decrypt(data).decode('utf-8') + + def encrypt_sensitive_data(self, password, data): + sb_key = self.derive_key_from_password(password.encode('utf-8')) + sb = nacl.secret.SecretBox(sb_key) + if not isinstance(data, bytes): + data = data.encode('utf-8') + + return base64.b64encode(sb.encrypt(data)).decode('utf-8') + + def get_salt(self): + return base64.b64decode(self.salt.encode('utf-8')) + + def set_salt(self): + self.salt = base64.b64encode(nacl.utils.random(16)).decode('utf-8') + + def get_encrypted_sensitive_data(self): + return base64.b64decode(self.encrypted_sensitive_data.encode('utf-8')) + + def set_encrypted_sensitive_data(self, password): + key = base64.b64encode(nacl.utils.random(64)) + self.set_salt() + + key_crypted = self.encrypt_sensitive_data(password, key) + self.encrypted_sensitive_data = key_crypted + + def encrypt_data(self, data, password): + sb = self.get_secret_box(password) + value_enc = sb.encrypt(data.encode('utf-8')) + return base64.b64encode(value_enc).decode('utf-8') + + def decrypt_data(self, data, password): + sb = self.get_secret_box(password) + value = base64.b64decode(data.encode('utf-8')) + return sb.decrypt(value).decode('utf-8') + + def get_secret_box(self, password): + pw = base64.b64decode(password.encode('utf-8')*4) + sb_key = self.derive_key_from_password(pw) + return nacl.secret.SecretBox(sb_key) diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index b3c4c9e..d40c6a1 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -19,7 +19,9 @@ class AuthorizeForm(forms.Form): self.user = kwargs.pop('user', None) self.org = kwargs.pop('org', None) self.code = kwargs.pop('code', None) + self.pw = kwargs.pop('pw', None) self.presentation_definition = kwargs.pop('presentation_definition', []) + self.subject_did = None reg = r'({})'.format('|'.join(self.presentation_definition)) @@ -49,7 +51,12 @@ class AuthorizeForm(forms.Form): txt = _('There are some problems with this credentials') raise ValidationError(txt) - self.list_credentials.append(c) + cred = self.user.decrypt_data( + c.data, + self.pw + ) + self.subject_did = c.subject_did + self.list_credentials.append(cred) if not self.code: txt = _("There isn't code in request") @@ -69,13 +76,14 @@ class AuthorizeForm(forms.Form): return def get_verificable_presentation(self): - did = self.list_credentials[0].subject_did + did = self.subject_did vp_template = get_template('credentials/verifiable_presentation.json') - vc_list = json.dumps([json.loads(x.data) for x in self.list_credentials]) + vc_list = json.dumps([json.loads(x) for x in self.list_credentials]) context = { "holder_did": did.did, "verifiable_credential_list": vc_list } unsigned_vp = vp_template.render(context) - self.vp = create_verifiable_presentation(did.key_material, unsigned_vp) + key_material = did.get_key_material(self.pw) + self.vp = create_verifiable_presentation(key_material, unsigned_vp) diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py deleted file mode 100644 index 2f67109..0000000 --- a/oidc4vp/migrations/0001_initial.py +++ /dev/null @@ -1,137 +0,0 @@ -# Generated by Django 4.2.5 on 2024-01-22 12:16 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import oidc4vp.models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Authorization', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ( - 'code', - models.CharField(default=oidc4vp.models.set_code, max_length=24), - ), - ('code_used', models.BooleanField(default=False)), - ('created', models.DateTimeField(auto_now=True)), - ('presentation_definition', models.CharField(max_length=250)), - ], - ), - migrations.CreateModel( - name='Organization', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('name', models.CharField(max_length=250)), - ( - 'client_id', - models.CharField( - default=oidc4vp.models.set_client_id, max_length=24, unique=True - ), - ), - ( - 'client_secret', - models.CharField( - default=oidc4vp.models.set_client_secret, max_length=48 - ), - ), - ('my_client_id', models.CharField(max_length=24)), - ('my_client_secret', models.CharField(max_length=48)), - ( - 'response_uri', - models.URLField( - help_text='Url where to send the verificable presentation', - max_length=250, - ), - ), - ], - ), - migrations.CreateModel( - name='OAuth2VPToken', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('created', models.DateTimeField(auto_now=True)), - ('result_verify', models.CharField(max_length=255)), - ('vp_token', models.TextField()), - ( - 'authorization', - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name='vp_tokens', - to='oidc4vp.authorization', - ), - ), - ( - 'organization', - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name='vp_tokens', - to='oidc4vp.organization', - ), - ), - ( - 'user', - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name='vp_tokens', - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), - migrations.AddField( - model_name='authorization', - name='organization', - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name='authorizations', - to='oidc4vp.organization', - ), - ), - migrations.AddField( - model_name='authorization', - name='user', - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 66ceff1..42b0d93 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -193,10 +193,15 @@ class OAuth2VPToken(models.Model): return response response["verify"] = "Ok, Verification correct" - response["redirect_uri"] = self.get_redirect_url() + url = self.get_redirect_url() + if url: + response["redirect_uri"] = url return response def get_redirect_url(self): + if not settings.ALLOW_CODE_URI: + return + data = { "code": self.authorization.code, } diff --git a/oidc4vp/templates/credentials_presentation.html b/oidc4vp/templates/credentials_presentation.html index 63f54ba..41629c8 100644 --- a/oidc4vp/templates/credentials_presentation.html +++ b/oidc4vp/templates/credentials_presentation.html @@ -87,7 +87,7 @@
diff --git a/oidc4vp/views.py b/oidc4vp/views.py index a62e462..d6eec81 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -13,6 +13,7 @@ from django.contrib import messages from oidc4vp.models import Authorization, Organization, OAuth2VPToken from idhub.mixins import UserView +from idhub.models import Event from oidc4vp.forms import AuthorizeForm from utils.idhub_ssikit import verify_presentation @@ -43,6 +44,11 @@ class AuthorizeView(UserView, FormView): kwargs['presentation_definition'] = vps kwargs["org"] = self.get_org() kwargs["code"] = self.request.GET.get('code') + enc_pw = self.request.session["key_did"] + kwargs['pw'] = self.request.user.decrypt_data( + enc_pw, + self.request.user.password+self.request.session._session_key + ) return kwargs def get_form(self, form_class=None): @@ -55,12 +61,12 @@ class AuthorizeView(UserView, FormView): authorization = form.save() if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) - return super().form_valid(form) + return redirect(self.success_url) try: authorization = authorization.json() except: messages.error(self.request, _("Error sending credential!")) - return super().form_valid(form) + return redirect(self.success_url) verify = authorization.get('verify') result, msg = verify.split(",") @@ -69,13 +75,22 @@ class AuthorizeView(UserView, FormView): if 'ok' in result.lower(): messages.success(self.request, msg) + cred = form.credentials.first() + verifier = form.org.name + if cred and verifier: + Event.set_EV_CREDENTIAL_PRESENTED(cred, verifier) + if authorization.get('redirect_uri'): return redirect(authorization.get('redirect_uri')) elif authorization.get('response'): txt = authorization.get('response') messages.success(self.request, txt) + txt2 = f"Verifier {verifier} send: " + txt + Event.set_EV_USR_SEND_VP(txt2, self.request.user) + url = reverse_lazy('idhub:user_dashboard') + return redirect(url) - return super().form_valid(form) + return redirect(self.success_url) def get_org(self): client_id = self.request.GET.get("client_id") @@ -123,7 +138,6 @@ class VerifyView(View): response = vp_token.get_response_verify() vp_token.save() if not vp_token.authorization.promotions.exists(): - response["redirect_uri"] = "" response["response"] = "Validation Code {}".format(code) return JsonResponse(response) @@ -157,9 +171,10 @@ class AllowCodeView(View): code=code, code_used=False ) - if not self.authorization.promotions.exists(): + + promotion = self.authorization.promotions.first() + if not promotion: raise Http404("Page not Found!") - promotion = self.authorization.promotions.all()[0] return redirect(promotion.get_url(code)) diff --git a/promotion/migrations/0001_initial.py b/promotion/migrations/0001_initial.py deleted file mode 100644 index b994f2f..0000000 --- a/promotion/migrations/0001_initial.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 4.2.5 on 2024-01-22 12:16 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ('oidc4vp', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Promotion', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('name', models.CharField(max_length=250)), - ( - 'discount', - models.PositiveSmallIntegerField( - choices=[(1, 'Financial vulnerability')] - ), - ), - ( - 'authorize', - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name='promotions', - to='oidc4vp.authorization', - ), - ), - ], - ), - ] diff --git a/requirements.txt b/requirements.txt index ae069eb..27dd8c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,11 +6,25 @@ black==23.9.1 python-decouple==3.8 jsonschema==4.19.1 pandas==2.1.1 +xlrd==2.0.1 +odfpy==1.4.1 requests==2.31.0 -didkit==0.3.2 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 +pynacl==1.5.0 more-itertools==10.1.0 dj-database-url==2.1.0 faker==21.0.0 +PyPDF2 +svg2rlg +svglib +cairosvg +pypdf +pyhanko +qrcode +uharfbuzz==0.38.0 +fontTools==4.47.0 +weasyprint==60.2 +ujson==5.9.0 +./didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl diff --git a/schemas/course-credential.json b/schemas/course-credential.json new file mode 100644 index 0000000..eae8f21 --- /dev/null +++ b/schemas/course-credential.json @@ -0,0 +1,130 @@ +{ + "$id": "https://idhub.pangea.org/vc_schemas/courseCredential", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "NGO Course Credential Schema", + "description": "A NGO Course Credential Schema awarded by a NGO federation and their NGO members, as proposed by Lafede.cat", + "name": [ + { + "value": "NGO Course Credential for participants", + "lang": "en" + }, + { + "value": "Credencial per participants d'un curs impartit per una ONG", + "lang": "ca_ES" + }, + { + "value": "Credencial para participantes de un curso impartido por una ONG", + "lang": "es" + } + ], + "type": "object", + "allOf": [ + { + "$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json" + }, + { + "properties": { + "credentialSubject": { + "description": "Defines additional properties on credentialSubject: the given course followed by a student", + "type": "object", + "properties": { + "id": { + "description": "Defines a unique identifier (DID) of the credential subject: the credential of a completed course by a student", + "type": "string" + }, + "firstName": { + "type": "string", + "description": "The first name of the student" + }, + "lastName": { + "type": "string", + "description": "The family name of the student" + }, + "personalIdentifier": { + "type": "string", + "description": "The personal identifier of the student, such as ID number" + }, + "issuedDate": { + "type": "string", + "description": "The date the credential was issued", + "format": "date" + }, + "modeOfInstruction": { + "type": "string", + "description": "The mode of instruction: online, in-person, etc." + }, + "courseDuration": { + "type": "integer", + "description": "The duration of the course in hours" + }, + "courseDays": { + "type": "integer", + "description": "The number of days the course lasts" + }, + "courseName": { + "type": "string", + "description": "The name of the course" + }, + "courseDescription": { + "type": "string", + "description": "The description of the course" + }, + "gradingScheme": { + "type": "string", + "description": "The grading scheme used for the course" + }, + "scoreAwarded": { + "type": "integer", + "description": "The score awarded to the student", + "minimum": 0, + "maximum": 10 + }, + "qualificationAwarded": { + "type": "string", + "description": "The qualification awarded to the student", + "enum": [ + "A+", + "A", + "B", + "C", + "D" + ] + }, + "courseLevel": { + "type": "string", + "description": "The level of the course" + }, + "courseFramework": { + "type": "string", + "description": "The framework in which the course belongs to" + }, + "courseCredits": { + "type": "integer", + "description": "The number of (ECTS) credits awarded for the course" + }, + "dateOfAssessment": { + "type": "string", + "description": "The date of assessment", + "format": "date" + }, + "evidenceAssessment": { + "type": "string", + "description": "The evidence of the assessment: final exam, presence, participation" + } + }, + "required": [ + "id", + "firstName", + "lastName", + "personalIdentifier", + "issuedDate", + "modeOfInstruction", + "courseDuration", + "courseDays", + "courseName" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/schemas/device-purchase.json b/schemas/device-purchase.json new file mode 100644 index 0000000..6267ae9 --- /dev/null +++ b/schemas/device-purchase.json @@ -0,0 +1,176 @@ +{ + "$id": "https://idhub.pangea.org/vc_schemas/devicePurchase.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Purchase of an eReuse device", + "description": "A device purchase credential is a proof of purchase of a device from a seller by a buyer", + "name": [ + { + "value": "Device purchase credential", + "lang": "en" + }, + { + "value": "Credencial d'adquisició d'un dispositiu", + "lang": "ca_ES" + }, + { + "value": "Credencial de adquisición de un dispositivo", + "lang": "es" + } + ], + "type": "object", + "allOf": [ + { + "$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json" + }, + { + "properties": { + "credentialSubject": { + "description": "Defines additional properties on credentialSubject: the purchase act, to qualify as simplified invoice (ES)", + "type": "object", + "properties": { + "id": { + "description": "Defines a unique identifier (DID) of the credential subject: the purchase act/transaction", + "type": "string" + }, + "invoiceNumber": { + "description": "The invoice number of the purchase act/transaction", + "type": "string" + }, + "totalAmount": { + "description": "The total amount of the transaction in local currency units: Euro by default", + "type": "string" + }, + "sellerId": { + "description": "Defines a unique identifier (DID) of the seller actor", + "type": "string" + }, + "sellerBusinessName": { + "description": "Business name of the credential subject in the seller role", + "type": "string" + }, + "sellerName": { + "description": "Name of the credential subject in the seller role", + "type": "string" + }, + "sellerSurname": { + "description": "Surname of the credential subject in the seller role, if natural person", + "type": "string" + }, + "sellerEmail": { + "type": "string", + "format": "email" + }, + "sellerPhoneNumber": { + "type": "string" + }, + "sellerIdentityDocType": { + "description": "Type of the Identity Document of the credential subject in the seller role", + "type": "string" + }, + "sellerIdentityNumber": { + "description": "Number of the Identity Document of the credential subject in the seller role", + "type": "string" + }, + "buyerId": { + "description": "Defines a unique identifier (DID) of the credential subject: the buyer actor", + "type": "string" + }, + "buyerBusinessName": { + "description": "Business name of the credential subject in the buyer role", + "type": "string" + }, + "buyerName": { + "description": "Name of the credential subject in the buyer role", + "type": "string" + }, + "buyerSurname": { + "description": "Surname of the credential subject in the buyer role, if natural person", + "type": "string" + }, + "buyerEmail": { + "type": "string", + "format": "email" + }, + "buyerPhoneNumber": { + "type": "string" + }, + "buyerIdentityDocType": { + "description": "Type of the Identity Document of the credential subject in the buyer role", + "type": "string" + }, + "buyerIdentityNumber": { + "description": "Number of the Identity Document of the credential subject in the buyer role", + "type": "string" + }, + "deliveryStreetAddress": { + "description": "Postal address of the credential Subject in the buyer role", + "type": "string" + }, + "deliveryPostCode": { + "description": "Postal code of the credential Subject in the buyer role", + "type": "string" + }, + "deliveryCity": { + "description": "City of the credential Subject in the buyer role", + "type": "string" + }, + "supplyDescription": { + "description": "Description of the product/device supplied, needed in a simplified invoice", + "type": "string" + }, + "taxRate": { + "description": "Description of Tax rate (VAT) and optionally also the expression VAT included, or special circumstances such as REBU, needed in a simplified invoice", + "type": "string" + }, + "deviceChassisId": { + "description": "Chassis identifier of the device", + "type": "string" + }, + "devicePreciseHardwareId": { + "description": "Chassis precise hardware configuration identifier of the device", + "type": "string" + }, + "depositId": { + "description": "Identifier of an economic deposit left on loan to be returned under conditions", + "type": "string" + }, + "sponsorId": { + "description": "Identifier of the sponsor of this purchase that paid the economic cost of the purchase", + "type": "string" + }, + "sponsorName": { + "description": "Name of the sponsor of this purchase that paid the economic cost of the purchase", + "type": "string" + }, + "purchaseDate": { + "type": "string", + "format": "date-time" + }, + "invoiceDate": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "invoiceNumber", + "totalAmount", + "sellerId", + "sellerName", + "sellerBusinessName", + "sellerSurname", + "sellerEmail", + "sellerIdentityDocType", + "sellerIdentityNumber", + "buyerId", + "buyerEmail", + "supplyDescription", + "taxRate", + "deviceChassisId", + "purchaseDate" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/schemas/federation-membership.json b/schemas/federation-membership.json new file mode 100644 index 0000000..b1f5a9d --- /dev/null +++ b/schemas/federation-membership.json @@ -0,0 +1,122 @@ +{ + "$id": "https://idhub.pangea.org/vc_schemas/federationMembership.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Federation membership", + "description": "The federation membership specifies participation of a NGO into a NGO federation, as proposed by Lafede.cat", + "name": [ + { + "value": "NGO federation membership", + "lang": "en" + }, + { + "value": "Membre de federació ONGs", + "lang": "ca_ES" + }, + { + "value": "Miembro de federación de ONGs", + "lang": "es" + } + ], + "type": "object", + "allOf": [ + { + "$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json" + }, + { + "properties": { + "credentialSubject": { + "description": "Defines additional properties on credentialSubject", + "type": "object", + "properties": { + "id": { + "description": "Defines a unique identifier of the credential subject", + "type": "string" + }, + "federation": { + "description": "Federation the credential subject is affiliated with", + "type": "string" + }, + "legalName": { + "description": "Legal name of the affiliated organisation", + "type": "string" + }, + "shortName": { + "description": "Short name of the organisation of the affiliated organisation", + "type": "string" + }, + "registrationIdentifier": { + "description": "Registration identifier of the affiliated organisation", + "type": "string" + }, + "publicRegistry": { + "description": "Registry where the affiliated organisation is registered: 'Generalitat de Catalunya', 'Ministerio del interior de España'", + "type": "string" + }, + "streetAddress": { + "description": "Postal address of the member organisation: legal address", + "type": "string" + }, + "postCode": { + "description": "Postal code of the member organisation", + "type": "string" + }, + "city": { + "description": "City of the member organisation", + "type": "string" + }, + "taxReference": { + "description": "Tax reference as VAT registration of the member organisation", + "type": "string" + }, + "membershipType": { + "description": "Type of membership: full / observer", + "type": "string" + }, + "membershipStatus": { + "description": "Type of membership: active / suspended, etc.", + "type": "string" + }, + "membershipId": { + "description": "Membership identifier: an internal unique number or code", + "type": "string" + }, + "membershipSince": { + "type": "string", + "format": "date" + }, + "email": { + "type": "string", + "format": "email" + }, + "phone": { + "type": "string" + }, + "website": { + "type": "string", + "format": "uri" + }, + "evidence": { + "description": "Type of evidence used for attestation", + "type": "string" + }, + "certificationDate": { + "type": "string", + "format": "date" + } + }, + "required": [ + "id", + "legalName", + "postCode", + "city", + "membershipType", + "membershipStatus", + "federation", + "membershipSince", + "certificationDate" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/schemas/financial-vulnerability.json b/schemas/financial-vulnerability.json new file mode 100644 index 0000000..e1960d5 --- /dev/null +++ b/schemas/financial-vulnerability.json @@ -0,0 +1,103 @@ +{ + "$id": "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Financial Vulnerability Credential", + "description": "A Financial Vulnerability Credential is issued to individuals or families to prove their financial vulnerability based on various factors, with the objective of presenting it to a third party to receive benefits or services.", + "name": [ + { + "value": "Financial Vulnerability Credential", + "lang": "en" + }, + { + "value": "Credencial de Vulnerabilitat Financera", + "lang": "ca_ES" + }, + { + "value": "Credencial de Vulnerabilidad Financiera", + "lang": "es" + } + ], + "type": "object", + "allOf": [ + { + "$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json" + }, + { + "properties": { + "credentialSubject": { + "description": "Defines additional properties on credentialSubject", + "type": "object", + "properties": { + "id": { + "description": "Defines a unique identifier (DID) of the credential subject", + "type": "string" + }, + "firstName": { + "description": "Name of the credential subject", + "type": "string" + }, + "lastName": { + "description": "Surname of the credential subject", + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "phoneNumber": { + "type": "string" + }, + "identityDocType": { + "description": "Type of the Identity Document of the credential subject", + "type": "string" + }, + "identityNumber": { + "description": "Number of the Identity Document of the credential subject", + "type": "string" + }, + "streetAddress": { + "description": "Postal address of the credential Subject", + "type": "string" + }, + "socialWorkerName": { + "description": "Name of the social worker that support the vulnerable person/family", + "type": "string" + }, + "socialWorkerSurname": { + "description": "Surname of the social worker that support the vulnerable person/family", + "type": "string" + }, + "financialVulnerabilityScore": { + "description": "Measure of an individual's susceptibility to financial hardship", + "type": "string" + }, + "amountCoveredByOtherAids": { + "type": "string" + }, + "connectivityOptionList": { + "type": "string" + }, + "assessmentDate": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "firstName", + "lastName", + "email", + "identityDocType", + "identityNumber", + "streetAddress", + "socialWorkerName", + "socialWorkerSurname", + "financialVulnerabilityScore", + "amountCoveredByOtherAids", + "assessmentDate" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/schemas/membership-card.json b/schemas/membership-card.json index 0d5ff9a..20a5f9b 100644 --- a/schemas/membership-card.json +++ b/schemas/membership-card.json @@ -1,65 +1,94 @@ { - "$id": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/membership-card-schema.json", + "$id": "https://idhub.pangea.org/vc_schemas/membership-card.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "name": "MembershipCard", - "description": "MembershipCard credential using JsonSchema", - "type": "object", - "properties": { - "credentialSubject": { - "type": "object", - "properties": { - "organisation": { - "type": "string" - }, - "membershipType": { - "type": "string" - }, - "affiliatedSince": { - "type": "string", - "format": "date-time" - }, - "affiliatedUntil": { - "type": "string", - "format": "date-time" - }, - "typeOfPerson": { - "type": "string", - "enum": [ - "individual", - "org" - ] - }, - "identityDocType": { - "type": "string", - "enum": [ - "DNI", - "NIF", - "NIE", - "PASSPORT" - ] - }, - "identityNumber": { - "type": "string" - }, - "name": { - "type": "string" - }, - "surnames": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - } - }, - "required": [ - "organisation", - "affiliatedSince", - "typeOfPerson", - "name", - "surnames", - "email" - ] + "title": "Membership Card", + "description": "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.", + "name": [ + { + "value": "Membership Card", + "lang": "en" + }, + { + "value": "Carnet de soci/a", + "lang": "ca_ES" + }, + { + "value": "Carnet de socio/a", + "lang": "es" } - } + ], + "type": "object", + "allOf": [ + { + "$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json" + }, + { + "properties": { + "credentialSubject": { + "description": "Defines additional properties on credentialSubject", + "type": "object", + "properties": { + "id": { + "description": "Defines a unique identifier of the credential subject", + "type": "string" + }, + "organisation": { + "description": "Organisation the credential subject is affiliated with", + "type": "string" + }, + "membershipType": { + "description": "Type of membership", + "type": "string" + }, + "membershipId": { + "description": "Membership identifier", + "type": "string" + }, + "affiliatedSince": { + "type": "string", + "format": "date-time" + }, + "affiliatedUntil": { + "type": "string", + "format": "date-time" + }, + "typeOfPerson": { + "type": "string", + "enum": [ + "natural", + "legal" + ] + }, + "identityDocType": { + "description": "Type of the Identity Document of the credential subject", + "type": "string" + }, + "identityNumber": { + "description": "Number of the Identity Document of the credential subject", + "type": "string" + }, + "firstName": { + "description": "Name of the natural person or name of the legal person (organisation)", + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "id", + "organisation", + "affiliatedSince", + "typeOfPerson", + "firstName", + "email" + ] + } + } + } + ] } \ No newline at end of file diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 290ff63..1a3675d 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -149,6 +149,7 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ @@ -222,3 +223,4 @@ LOGGING = { } } +ORGANIZATION = config('ORGANIZATION', 'Pangea') diff --git a/urls_provisional b/urls_provisional deleted file mode 100644 index 43f0677..0000000 --- a/urls_provisional +++ /dev/null @@ -1,15 +0,0 @@ -/user/event-log [GET] -> vista d'esdeveniments - sense enllaços rapids a les accions -/user/dashboard [GET, POST] -> vista de dades personals -/user/roles [GET] -> vista de rols (????) -/user/gdpr [GET] -> info de la gdpr - -/user/wallet/dids [GET, POST] -/user/wallet/dids/ [GET, DELETE] -/user/credentials [GET] -/user/credentials/ [GET, DELETE] -/user/credentials/request [GET, POST] - *** falta "present credentials" ??? *** - - -/admin/ diff --git a/utils/certs.py b/utils/certs.py new file mode 100644 index 0000000..ff2c241 --- /dev/null +++ b/utils/certs.py @@ -0,0 +1,36 @@ +from pyhanko.sign.signers import SimpleSigner +from cryptography.hazmat.primitives.serialization import pkcs12 +from pyhanko_certvalidator.registry import SimpleCertificateStore +from pyhanko.keys import _translate_pyca_cryptography_cert_to_asn1 +from pyhanko.keys import _translate_pyca_cryptography_key_to_asn1 + + +def load_cert(pfx_bytes, passphrase): + try: + ( + private_key, + cert, + other_certs_pkcs12, + ) = pkcs12.load_key_and_certificates(pfx_bytes, passphrase) + except (IOError, ValueError, TypeError) as e: + # logger.error( + # 'Could not load key material from PKCS#12 file', exc_info=e + # ) + return None + + kinfo = _translate_pyca_cryptography_key_to_asn1(private_key) + cert = _translate_pyca_cryptography_cert_to_asn1(cert) + other_certs_pkcs12 = set( + map(_translate_pyca_cryptography_cert_to_asn1, other_certs_pkcs12) + ) + + cs = SimpleCertificateStore() + certs_to_register = set(other_certs_pkcs12) + cs.register_multiple(certs_to_register) + return SimpleSigner( + signing_key=kinfo, + signing_cert=cert, + cert_registry=cs, + signature_mechanism=None, + prefer_pss=False, + ) diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index cc3e9b4..85e6e2f 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -2,10 +2,13 @@ import asyncio import datetime import didkit import json +import urllib import jinja2 from django.template.backends.django import Template from django.template.loader import get_template +from trustchain_idhub import settings + def generate_did_controller_key(): return didkit.generate_ed25519_key() @@ -15,6 +18,31 @@ def keydid_from_controller_key(key): return didkit.key_to_did("key", key) +async def resolve_keydid(keydid): + return await didkit.resolve_did(keydid, "{}") + + +def webdid_from_controller_key(key): + """ + Se siguen los pasos para generar un webdid a partir de un keydid. + Documentado en la docu de spruceid. + """ + keydid = keydid_from_controller_key(key) # "did:key:<...>" + pubkeyid = keydid.rsplit(":")[-1] # <...> + document = json.loads(asyncio.run(resolve_keydid(keydid))) # Documento DID en terminos "key" + domain = urllib.parse.urlencode({"domain": settings.DOMAIN})[7:] + webdid_url = f"did:web:{domain}:did-registry:{pubkeyid}" # nueva URL: "did:web:idhub.pangea.org:<...>" + webdid_url_owner = webdid_url + "#owner" + # Reemplazamos los campos del documento DID necesarios: + document["id"] = webdid_url + document["verificationMethod"][0]["id"] = webdid_url_owner + document["verificationMethod"][0]["controller"] = webdid_url + document["authentication"][0] = webdid_url_owner + document["assertionMethod"][0] = webdid_url_owner + document_fixed_serialized = json.dumps(document) + return webdid_url, document_fixed_serialized + + def generate_generic_vc_id(): # TODO agree on a system for Verifiable Credential IDs return "https://pangea.org/credentials/42"