diff --git a/examples/membership-card.csv b/examples/membership-card.csv index 1f4d08d..7447dcc 100644 --- a/examples/membership-card.csv +++ b/examples/membership-card.csv @@ -1,2 +1,2 @@ -name surnames email typeOfPerson membershipType organisation affiliatedSince -Pepe Gómez user1@example.org individual Member Pangea 01-01-2023 +firstName lastName email membershipType membershipId affiliatedUntil affiliatedSince typeOfPerson identityDocType identityNumber +Pepe Gómez user1@example.org individual 123456 2024-01-01T00:00:00Z 2023-01-01T00:00:00Z natural DNI 12345678A diff --git a/examples/membership-card.ods b/examples/membership-card.ods index adf3cfc..b1fe1ac 100644 Binary files a/examples/membership-card.ods and b/examples/membership-card.ods differ diff --git a/examples/membership-card.xls b/examples/membership-card.xls index 857fb28..bd10538 100644 Binary files a/examples/membership-card.xls and b/examples/membership-card.xls differ diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 8174a32..aec6605 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -1,11 +1,13 @@ 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, certs @@ -23,24 +25,39 @@ from idhub_auth.models import User 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.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, @@ -51,6 +68,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 @@ -65,7 +90,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!") @@ -73,7 +99,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): @@ -115,7 +144,9 @@ 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) @@ -135,6 +166,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): @@ -268,9 +300,13 @@ class ImportCertificateForm(forms.Form): did=self.file_name, label=self._label, eidas1=True, - user=self.user + 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: diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 7f385ab..eb150e1 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -17,6 +17,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 @@ -640,19 +641,20 @@ 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) return super().form_valid(form) + class DidEditView(Credentials, UpdateView): template_name = "idhub/admin/did_register.html" subtitle = _('Organization Identities (DID)') diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 034bc68..2dd6ad6 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,10 +89,18 @@ class Command(BaseCommand): try: ldata = json.loads(data) assert credtools.validate_schema(ldata) - name = ldata.get('name') - assert name + dname = ldata.get('name') + assert dname except Exception: 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) def open_file(self, file_name): diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 5aeabdb..bc9f011 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-01-11 10:17 +# Generated by Django 4.2.5 on 2024-01-18 11:32 from django.conf import settings from django.db import migrations, models @@ -25,11 +25,18 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), + ( + 'type', + models.PositiveSmallIntegerField( + choices=[(1, 'Key'), (2, 'Web')], verbose_name='Type' + ), + ), ('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.TextField()), ('eidas1', models.BooleanField(default=False)), + ('didweb_document', models.TextField()), ( 'user', models.ForeignKey( @@ -151,7 +158,6 @@ class Migration(migrations.Migration): ('issued_on', models.DateTimeField(null=True)), ('data', models.TextField()), ('csv_data', models.TextField()), - ('public', models.BooleanField(default=True)), ('hash', models.CharField(max_length=260)), ( 'status', @@ -165,6 +171,14 @@ class Migration(migrations.Migration): default=1, ), ), + ( + 'eidas1_did', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='idhub.did', + ), + ), ( 'issuer_did', models.ForeignKey( diff --git a/idhub/models.py b/idhub/models.py index 7d48543..10e0ceb 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -1,15 +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 @@ -404,6 +410,13 @@ class Event(models.Model): 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) @@ -418,21 +431,34 @@ class DID(models.Model): 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) @@ -445,9 +471,19 @@ class Schemas(models.Model): return {} return json.loads(self.data) - def name(self): - return self.get_schema.get('name', '') + 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] + def description(self): return self.get_schema.get('description', '') @@ -468,7 +504,6 @@ class VerificableCredential(models.Model): issued_on = models.DateTimeField(null=True) data = models.TextField() csv_data = models.TextField() - public = models.BooleanField(default=settings.DEFAULT_PUBLIC_CREDENTIALS) hash = models.CharField(max_length=260) status = models.PositiveSmallIntegerField( choices=Status.choices, @@ -490,21 +525,50 @@ 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 def description(self): - for des in json.loads(self.render()).get('description', []): - if settings.LANGUAGE_CODE == des.get('lang'): + for des in json.loads(self.render("")).get('description', []): + if settings.LANGUAGE_CODE in des.get('lang'): return des.get('value', '') return '' + 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 @@ -512,21 +576,29 @@ 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, + # ) + data = sign_credential( + self.render(domain), + self.issuer_did.get_key_material(issuer_pass) ) - if self.public: + if self.eidas1_did: + self.data = data self.hash = hashlib.sha3_256(self.data.encode()).hexdigest() + else: + self.data = self.user.encrypt_data(data, password) - def get_context(self): + def get_context(self, domain): d = json.loads(self.csv_data) issuance_date = '' if self.issued_on: @@ -534,31 +606,38 @@ class VerificableCredential(models.Model): issuance_date = self.issued_on.strftime(format) cred_path = 'credentials' - if self.public: + if self.eidas1_did: cred_path = 'public/credentials' + url_id = "{}/{}/{}".format( - settings.DOMAIN.strip("/"), + domain, cred_path, self.id ) + context = { '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): @@ -567,6 +646,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/templates/credentials/membership-card.json b/idhub/templates/credentials/membership-card.json index bc315bb..5ecda0a 100644 --- a/idhub/templates/credentials/membership-card.json +++ b/idhub/templates/credentials/membership-card.json @@ -1,31 +1,8 @@ { "@context": [ "https://www.w3.org/2018/credentials/v1", - { - "individual": "https://schema.org/Person", - "Member": "https://schema.org/Member", - "startDate": "https://schema.org/startDate", - "jsonSchema": "https://schema.org/jsonSchema", - "$ref": "https://schema.org/jsonSchemaRef", - "credentialSchema": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#credentialSchema", - "organisation": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#organisation", - "membershipType": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#membershipType", - "membershipId": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#membershipId", - "typeOfPerson": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#typeOfPerson", - "identityDocType": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#identityDocType", - "identityNumber": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#identityNumber", - "name": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#name", - "description": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#description", - "value": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#value", - "lang": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#lang", - "surnames": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#surnames", - "email": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#email", - "affiliatedSince": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#affiliatedSince", - "affiliatedUntil": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#affiliatedUntil", - "issued": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#issued", - "validFrom": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#validFrom", - "validUntil": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#validUntil" - } + "https://idhub.pangea.org/credentials/base/v1", + "https://idhub.pangea.org/credentials/membership-card/v1" ], "type": [ "VerifiableCredential", @@ -35,22 +12,7 @@ "id": "{{ vc_id }}", "issuer": { "id": "{{ issuer_did }}", - "name": "Pangea", - "description": [ - { - "value": "Pangea.org is a service provider leveraging open-source technologies to provide affordable and accessible solutions for social enterprises and solidarity organisations.", - "lang": "en" - }, - { - "value": "Pangea.org és un proveïdor de serveis que aprofita les tecnologies de codi obert per oferir solucions assequibles i accessibles per a empreses socials i organitzacions solidàries.", - "lang": "ca_ES" - }, - { - "value": "Pangea.org es un proveedor de servicios que aprovecha tecnologías de código abierto para proporcionar soluciones asequibles y accesibles para empresas sociales y organizaciones solidarias.", - "lang": "es" - } - - ] + "name": "{{ organisation }}" }, "issuanceDate": "{{ issuance_date }}", "issued": "{{ issuance_date }}", @@ -86,20 +48,20 @@ ], "credentialSubject": { "id": "{{ subject_did }}", - "organisation": "Pangea", - "membershipType": "{{ membershipType }}", - "membershipId": "{{ vc_id }}", - "affiliatedSince": "{{ affiliatedSince }}", - "affiliatedUntil": "{{ affiliatedUntil }}", + "firstName": "{{ firstName }}", + "lastName": "{{ lastName }}", + "email": "{{ email }}", "typeOfPerson": "{{ typeOfPerson }}", "identityDocType": "{{ identityDocType }}", "identityNumber": "{{ identityNumber }}", - "name": "{{ first_name }}", - "surnames": "{{ last_name }}", - "email": "{{ email }}", - "credentialSchema": { - "id": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/membership-card-schema.json", - "type": "JsonSchema" - } + "organisation": "{{ organisation }}", + "membershipType": "{{ membershipType }}", + "membershipId": "{{ vc_id }}", + "affiliatedSince": "{{ affiliatedSince }}", + "affiliatedUntil": "{{ affiliatedUntil }}" + }, + "credentialSchema": { + "id": "https://idhub.pangea.org/vc_schemas/membership-card.json", + "type": "FullJsonSchemaValidator2021" } } \ No newline at end of file diff --git a/idhub/templates/idhub/admin/credentials.html b/idhub/templates/idhub/admin/credentials.html index cab6a97..caee5fc 100644 --- a/idhub/templates/idhub/admin/credentials.html +++ b/idhub/templates/idhub/admin/credentials.html @@ -23,7 +23,7 @@
{% for f in credentials.all %}