diff --git a/.env.example b/.env.example index 6117fe9..0897b02 100644 --- a/.env.example +++ b/.env.example @@ -31,11 +31,14 @@ IDHUB_ADMIN_USER='admin' IDHUB_ADMIN_PASSWD='admin' IDHUB_ADMIN_EMAIL='admin@example.org' +IDHUB_SUPPORTED_CREDENTIALS="['CourseCredential', 'EOperatorClaim', 'FederationMembership', 'FinancialVulnerabilityCredential', 'MembershipCard', 'Snapshot']" + # this option needs to be set to 'n' to be able to make work idhub in docker # by default it is set to 'y' to facilitate idhub dev when outside docker IDHUB_SYNC_ORG_DEV='n' -# TODO that is only for testing +# TODO that is only for testing/demo purposes IDHUB_ENABLE_EMAIL=false IDHUB_ENABLE_2FACTOR_AUTH=false IDHUB_ENABLE_DOMAIN_CHECKER=false +IDHUB_PREDEFINED_TOKEN='27f944ce-3d58-4f48-b068-e4aa95f97c95' diff --git a/docker-compose.yml b/docker-compose.yml index 2804ead..05f0764 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - ENABLE_EMAIL=${IDHUB_ENABLE_EMAIL:-true} - ENABLE_2FACTOR_AUTH=${IDHUB_ENABLE_2FACTOR_AUTH:-true} - ENABLE_DOMAIN_CHECKER=${IDHUB_ENABLE_DOMAIN_CHECKER:-true} + - PREDEFINED_TOKEN=${IDHUB_PREDEFINED_TOKEN:-} - SECRET_KEY=${IDHUB_SECRET_KEY:-publicsecretisnotsecureVtmKBfxpVV47PpBCF2Nzz2H6qnbd} - STATIC_ROOT=${IDHUB_STATIC_ROOT:-/static/} - MEDIA_ROOT=${IDHUB_MEDIA_ROOT:-/media/} @@ -26,7 +27,7 @@ services: - EMAIL_PORT=${IDHUB_EMAIL_PORT} - EMAIL_USE_TLS=${IDHUB_EMAIL_USE_TLS} - EMAIL_BACKEND=${IDHUB_EMAIL_BACKEND} - - SUPPORTED_CREDENTIALS=['CourseCredential', 'EOperatorClaim', 'FederationMembership', 'FinancialVulnerabilityCredential', 'MembershipCard', 'Snapshot'] + - SUPPORTED_CREDENTIALS=${IDHUB_SUPPORTED_CREDENTIALS:-} - SYNC_ORG_DEV=${IDHUB_SYNC_ORG_DEV} ports: - ${IDHUB_PORT:-9001}:${IDHUB_PORT:-9001} diff --git a/docker-reset.sh b/docker-reset.sh index 3a7e3ad..d31b567 100755 --- a/docker-reset.sh +++ b/docker-reset.sh @@ -15,6 +15,7 @@ main() { cp -v .env.example .env echo "WARNING: .env was not there, .env.example was copied, this only happens once" fi + . ./.env docker compose down -v if [ "${DEV_DOCKER_ALWAYS_BUILD:-}" = 'true' ]; then diff --git a/docker/idhub.entrypoint.sh b/docker/idhub.entrypoint.sh index 52b27e0..b7ebd78 100755 --- a/docker/idhub.entrypoint.sh +++ b/docker/idhub.entrypoint.sh @@ -45,8 +45,10 @@ deployment_strategy() { echo "INFO detected NEW deployment" ./manage.py migrate - printf "This is DEVELOPMENT/PILOTS_EARLY DEPLOYMENT: including demo hardcoded data\n creating initial Datas\n" >&2 - ./manage.py initial_datas + printf "This is DEVELOPMENT/PILOTS_EARLY DEPLOYMENT: including demo hardcoded data\n" >&2 + + PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}" + ./manage.py demo_data "${PREDEFINED_TOKEN}" if [ "${OIDC_ORGS:-}" ]; then config_oidc4vp diff --git a/examples/keys_did.json b/examples/keys_did.json new file mode 100644 index 0000000..070965c --- /dev/null +++ b/examples/keys_did.json @@ -0,0 +1 @@ +{"label": "DEMO", "key_material": "{\"kty\": \"OKP\", \"crv\": \"Ed25519\", \"x\": \"IRqDfIumhbKKHhqMjOngikQmGoT1cZ6LPP-JjXa8CsY\", \"d\": \"AZXUEnJYFbGcn3Ebzy3vQWYFzx6rdnoHKilaMYUWuHA\", \"kid\": \"Generated\"}"} diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 0ec7aee..24d9f17 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -36,16 +36,16 @@ class TermsConditionsForm2(forms.Form): if data.get("accept"): self.user.accept_gdpr = True else: - self.user.accept_gdpr = False + self.user.accept_gdpr = False return data - + def save(self, commit=True): if commit: self.user.save() return self.user - - return + + return class EncryptionKeyForm(forms.Form): @@ -80,8 +80,8 @@ class EncryptionKeyForm(forms.Form): did = DID.objects.create(label='Default', type=DID.Types.WEB) did.set_did() did.save() - - return + + return class TermsConditionsForm(forms.Form): @@ -131,16 +131,16 @@ class TermsConditionsForm(forms.Form): if privacy and legal and cookies: self.user.accept_gdpr = True else: - self.user.accept_gdpr = False + self.user.accept_gdpr = False return data - + def save(self, commit=True): if commit: self.user.save() return self.user - - return + + return class ImportForm(forms.Form): @@ -197,7 +197,7 @@ class ImportForm(forms.Form): eidas1=True, did=eidas1 ).first() - + return data def clean_schema(self): @@ -288,15 +288,15 @@ class ImportForm(forms.Form): def save(self, commit=True): table = [] for k, v in self.rows.items(): - table.append(self.create_credential(k, v)) + table.append(self.create_credential(k, v)) if commit: for cred in table: cred.save() File_datas.objects.create(file_name=self.file_name) return table - - return + + return def validate_jsonld(self, line, row): try: @@ -355,7 +355,7 @@ class ImportForm(forms.Form): class SchemaForm(forms.Form): file_template = forms.FileField(label=_("File template")) - + class MembershipForm(forms.ModelForm): class Meta: @@ -376,7 +376,7 @@ class MembershipForm(forms.ModelForm): if members.filter(start_date__lte=start_date, end_date=None).exists(): msg = _("This membership already exists!") raise forms.ValidationError(msg) - + if (start_date and end_date): if start_date > end_date: msg = _("The end date is less than the start date") @@ -399,8 +399,8 @@ class MembershipForm(forms.ModelForm): if members.exists(): msg = _("This membership already exists!") raise forms.ValidationError(msg) - - + + return end_date @@ -417,7 +417,7 @@ class UserRolForm(forms.ModelForm): choices = self.fields['service'].choices choices.queryset = choices.queryset.exclude(users__user=user) self.fields['service'].choices = choices - + def clean_service(self): data = super().clean() service = UserRol.objects.filter( diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/demo_data.py similarity index 60% rename from idhub/management/commands/initial_datas.py rename to idhub/management/commands/demo_data.py index 462e944..4eabe55 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/demo_data.py @@ -7,22 +7,45 @@ from utils import credtools from django.conf import settings from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model -from decouple import config -from idhub.models import Schemas +from django.core.cache import cache +from django.urls import reverse +from pyvckit.did import ( + generate_did, + gen_did_document, +) + +from idhub.models import Schemas, DID from oidc4vp.models import Organization +from webhook.models import Token User = get_user_model() class Command(BaseCommand): - help = "Insert minimum datas for the project" + help = "Insert minimum data for the project" DOMAIN = settings.DOMAIN OIDC_ORGS = settings.OIDC_ORGS + def add_arguments(self, parser): + parser.add_argument('predefined_token', nargs='?', default='', type=str, help='predefined token') + parser.add_argument('predefined_did', nargs='?', default='', type=str, help='predefined did') + def handle(self, *args, **kwargs): ADMIN_EMAIL = settings.INITIAL_ADMIN_EMAIL ADMIN_PASSWORD = settings.INITIAL_ADMIN_PASSWORD + self.predefined_token = kwargs['predefined_token'] + self.predefined_did = kwargs['predefined_did'] + # on demo situation, encrypted vault is hardcoded with password DEMO + cache.set("KEY_DIDS", "DEMO", None) + + self.org = Organization.objects.create( + name=self.DOMAIN, + domain=self.DOMAIN, + main=True + ) + self.org.set_encrypted_sensitive_data() + self.org.save() self.create_admin_users(ADMIN_EMAIL, ADMIN_PASSWORD) if settings.CREATE_TEST_USERS: @@ -30,12 +53,6 @@ class Command(BaseCommand): user = 'user{}@example.org'.format(u) self.create_users(user, '1234') - self.org = Organization.objects.create( - name=self.DOMAIN, - domain=self.DOMAIN, - main=True - ) - if self.OIDC_ORGS: self.create_organizations() @@ -45,6 +62,61 @@ class Command(BaseCommand): su = User.objects.create_superuser(email=email, password=password) su.save() + if self.predefined_token: + tk = Token.objects.filter(token=self.predefined_token).first() + if not tk: + Token.objects.create(token=self.predefined_token) + + self.create_default_did() + + def create_default_did(self): + + fdid = self.open_example_did() + if not fdid: + return + + did = DID(type=DID.Types.WEB) + new_key_material = fdid.get("key_material", "") + label = fdid.get("label", "") + if not new_key_material: + return + + did.set_key_material(new_key_material) + + if label: + did.label = label + + if did.type == did.Types.KEY: + did.did = generate_did(new_key_material) + elif did.type == did.Types.WEB: + url = "https://{}".format(settings.DOMAIN) + path = reverse("idhub:serve_did", args=["a"]) + + if path: + path = path.split("/a/did.json")[0] + url = "https://{}/{}".format(settings.DOMAIN, path) + + did.did = generate_did(new_key_material, url) + key = json.loads(new_key_material) + url, did.didweb_document = gen_did_document(did.did, key) + + did.save() + + def open_example_did(self): + BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent + didweb_path = os.path.join(BASE_DIR, "examples", "keys_did.json") + + if self.predefined_did: + didweb_path = self.predefined_did + + data = '' + with open(didweb_path) as _file: + try: + data = json.loads(_file.read()) + except Exception: + pass + + return data def create_users(self, email, password): u = User.objects.create(email=email, password=password) diff --git a/idhub/models.py b/idhub/models.py index 73ce848..86291e1 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -684,6 +684,14 @@ class VerificableCredential(models.Model): if self.status == self.Status.ISSUED: return + supported = False + for name in self.schema.get_schema.get("name"): + if name.get("value") in settings.SUPPORTED_CREDENTIALS: + supported = True + + if not supported: + return + self.subject_did = did self.issued_on = datetime.datetime.now().astimezone(pytz.utc) diff --git a/idhub/user/views.py b/idhub/user/views.py index 4f5735d..d1de190 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -179,8 +179,8 @@ class TermsAndConditionsView(UserView, FormView): class WaitingView(UserView, TemplateView): template_name = "idhub/user/waiting.html" - title = _("Comunication with admin") - subtitle = _('Service temporary close') + title = _("Comunication with admin required") + subtitle = _('Service temporarily closed') section = "" icon = 'bi bi-file-earmark-medical' success_url = reverse_lazy('idhub:user_dashboard') diff --git a/idhub_auth/models.py b/idhub_auth/models.py index 0552b35..bdd7458 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -125,7 +125,7 @@ class User(AbstractBaseUser): sb = 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): @@ -166,6 +166,6 @@ class User(AbstractBaseUser): sb = secret.SecretBox(sb_key) if not isinstance(data, bytes): data = data.encode('utf-8') - + encrypted_data = base64.b64encode(sb.encrypt(data)).decode('utf-8') self.encrypted_sensitive_data = encrypted_data diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po index b62a260..b7c67b4 100644 --- a/locale/ca/LC_MESSAGES/django.po +++ b/locale/ca/LC_MESSAGES/django.po @@ -2796,11 +2796,11 @@ msgid "Data Protection" msgstr "Protecció de dades" #: idhub/user/views.py:183 -msgid "Comunication with admin" -msgstr "Comunicació amb l'admin" +msgid "Comunication with admin required" +msgstr "Es requereix comunicació amb l'admin" #: idhub/user/views.py:184 -msgid "Service temporary close" +msgid "Service temporarily closed" msgstr "Tancament temporal del servei" #: idhub/user/views.py:407 diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index db75844..d683e63 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -2789,11 +2789,11 @@ msgid "Data Protection" msgstr "Proteccion de datos" #: idhub/user/views.py:183 -msgid "Comunication with admin" -msgstr "Comunicación con el admin" +msgid "Comunication with admin required" +msgstr "Se requiere comunicación con el admin" #: idhub/user/views.py:184 -msgid "Service temporary close" +msgid "Service temporarily closed" msgstr "Cierre temporal del servicio" #: idhub/user/views.py:407 diff --git a/webhook/views.py b/webhook/views.py index 71625d7..fd2d931 100644 --- a/webhook/views.py +++ b/webhook/views.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt from django.views.generic.edit import DeleteView from django.views.generic.base import View +from django.core.cache import cache from django.http import JsonResponse from django_tables2 import SingleTableView from pyvckit.verify import verify_vp, verify_vc @@ -20,6 +21,10 @@ from webhook.tables import TokensTable @csrf_exempt def webhook_verify(request): if request.method == 'POST': + user = User.objects.filter(is_admin=True).first() + if not cache.get("KEY_DIDS") or not user.accept_gdpr: + return JsonResponse({'error': 'Temporary out of service'}, status=400) + auth_header = request.headers.get('Authorization') if not auth_header or not auth_header.startswith('Bearer '): return JsonResponse({'error': 'Invalid or missing token'}, status=401) @@ -56,6 +61,10 @@ def webhook_verify(request): @csrf_exempt def webhook_issue(request): if request.method == 'POST': + user = User.objects.filter(is_admin=True).first() + if not cache.get("KEY_DIDS") or not user.accept_gdpr: + return JsonResponse({'error': 'Temporary out of service'}, status=400) + auth_header = request.headers.get('Authorization') if not auth_header or not auth_header.startswith('Bearer '): return JsonResponse({'error': 'Invalid or missing token'}, status=401) @@ -89,7 +98,6 @@ def webhook_issue(request): if not schema: return JsonResponse({'error': 'Invalid credential'}, status=400) - user = User.objects.filter(is_admin=True).first() cred = VerificableCredential( csv_data=vc, issuer_did=did, @@ -100,6 +108,9 @@ def webhook_issue(request): cred.set_type() vc_signed = cred.issue(did, domain=request.get_host(), save=save) + if not vc_signed: + return JsonResponse({'error': 'Invalid credential'}, status=400) + return JsonResponse({'status': 'success', "data": vc_signed}, status=200) return JsonResponse({'status': 'fail'}, status=200)