diff --git a/docker-platform/settings.py b/docker-platform/settings.py index 297e4f3570..0739f7e929 100644 --- a/docker-platform/settings.py +++ b/docker-platform/settings.py @@ -333,4 +333,16 @@ from wirecloud.glogger import config as LOGGING -# MIDDLEWARE = ('django.middleware.gzip.GZipMiddleware',) + MIDDLEWARE \ No newline at end of file +# MIDDLEWARE = ('django.middleware.gzip.GZipMiddleware',) + MIDDLEWARE + +VC_LOGIN_CONFIG = { + 'enabled': os.environ.get('VC_LOGIN_ENABLED', 'False').lower() in ('true', 'yes', 't'), + 'verifier_host': os.environ.get('VC_VERIFIER_HOST'), + 'verifier_qr_path': os.environ.get('VC_VERIFIER_QR_PATH', '/api/v2/loginQR'), + 'verifier_token_path': os.environ.get('VC_VERIFIER_TOKEN_PATH', '/token'), + 'verifier_jwks_path': os.environ.get('VC_VERIFIER_JWKS_PATH', '/.well-known/jwks'), + 'client_id': os.environ.get('VC_CLIENT_ID'), + 'scope': os.environ.get('VC_SCOPE', 'openid learcredential'), + 'role_target': os.environ.get('VC_ROLE_TARGET'), + 'credential_type': os.environ.get('VC_CREDENTIAL_TYPE', 'LegalPersonCredential'), +} \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt index 60a244ccd6..c3ff5ea653 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,4 +1,5 @@ Django>=2.0,<2.3 +django-nose lxml>=2.3 django_compressor>=2.0,<2.3 rdflib>=3.2.0 @@ -15,3 +16,4 @@ pyScss>=1.3.4,<2.0 Pygments pillow jsonpatch +python-jose>=3.3.0 \ No newline at end of file diff --git a/src/settings.py b/src/settings.py index 1ece504c45..6a4716408e 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Django settings used as base for developing wirecloud. -from os import path +from os import path, environ from wirecloud.commons.utils.conf import load_default_wirecloud_conf from django.urls import reverse_lazy @@ -116,7 +116,7 @@ # Authentication AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', + 'django.contrib.auth.backends.ModelBackend' ) # WGT deployment dirs @@ -169,3 +169,19 @@ # 'CLASS': 'selenium.webdriver.Safari', # }, # } + + +VC_LOGIN_CONFIG = { + 'enabled': environ.get('VC_LOGIN_ENABLED', 'False').lower() in ('true', 'yes', 't'), + 'verifier_host': environ.get('VC_VERIFIER_HOST'), + 'verifier_qr_path': environ.get('VC_VERIFIER_QR_PATH', '/api/v2/loginQR'), + 'verifier_token_path': environ.get('VC_VERIFIER_TOKEN_PATH', '/token'), + 'verifier_jwks_path': environ.get('VC_VERIFIER_JWKS_PATH', '/.well-known/jwks'), + 'client_id': environ.get('VC_CLIENT_ID'), + 'scope': environ.get('VC_SCOPE', 'openid learcredential'), + 'role_target': environ.get('VC_ROLE_TARGET'), + 'credential_type': environ.get('VC_CREDENTIAL_TYPE', 'LegalPersonCredential'), +} + +if VC_LOGIN_CONFIG['enabled']: + AUTHENTICATION_BACKENDS += ('wirecloud.backends.vc_backend.VCBackend') \ No newline at end of file diff --git a/src/urls.py b/src/urls.py index 5196e6bebb..64ffe09498 100644 --- a/src/urls.py +++ b/src/urls.py @@ -5,10 +5,16 @@ from django.contrib import admin from django.contrib.auth import views as django_auth from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.conf import settings from wirecloud.commons import authentication as wc_auth import wirecloud.platform.urls + +vc_login_enabled = settings.VC_LOGIN_CONFIG['enabled'] +if vc_login_enabled: + from wirecloud.vc_login import views as vc_login_views + admin.autodiscover() urlpatterns = ( @@ -23,6 +29,11 @@ url(r'^login/?$', django_auth.LoginView.as_view(), name="login"), url(r'^logout/?$', wc_auth.logout, name="logout"), url(r'^admin/logout/?$', wc_auth.logout), + # VC login when enabled + *([ + url(r'^vc/login/?$', vc_login_views.vc_sso_login, name='vc_sso_login'), + url(r'^vc/callback/?$', vc_login_views.vc_sso_callback, name='vc_sso_callback'), + ] if vc_login_enabled else []), # Admin interface url(r'^admin/', admin.site.urls), diff --git a/src/wirecloud/backends/vc_backend.py b/src/wirecloud/backends/vc_backend.py new file mode 100644 index 0000000000..0d4a689930 --- /dev/null +++ b/src/wirecloud/backends/vc_backend.py @@ -0,0 +1,99 @@ +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth import get_user_model +from django.db import IntegrityError +import logging +from wirecloud.vc_login.vc_payload import VCPayload +from django.contrib.auth.models import Group, AbstractUser +from django.conf import settings + +# Get the active User model (handles custom user models) +User = get_user_model() +logger = logging.getLogger(__name__) + +class VCBackend(ModelBackend): + """ + Custom authentication backend that provisions (creates) a new user or + logs in and updates an existing user based on a validated + Verifiable Credential (VC) payload. + """ + + def authenticate(self, request, vc_payload: VCPayload = None, **kwargs): + """ + Retrieves user data from the VC payload and handles the login/creation process. + """ + if vc_payload is None: + return None + + email = vc_payload.email + first_name = vc_payload.first_name + last_name = vc_payload.last_name + user = None + roles = self._get_roles(vc_payload) + if not email: + logger.error("VC Payload missing required 'email' field for authentication.") + return None + + try: + user = User.objects.get(email=email) + + is_updated = False + + if user.first_name != first_name: + user.first_name = first_name + is_updated = True + if user.last_name != last_name: + user.last_name = last_name + is_updated = True + if is_updated: + user.save() + + logger.info(f"Existing user logged in successfully: {email}") + + except User.DoesNotExist: + try: + logger.info(f"User not found. Creating new user: {email}") + user = User.objects.create_user( + username=email, + email=email, + first_name=first_name, + last_name=last_name + ) + logger.info(f"New user provisioned: {user.username}") + + except IntegrityError: + logger.warning(f"Integrity conflict during user creation for {email}.") + return None + except Exception as e: + logger.error(f"Unexpected error during user creation: {e}") + return None + logger.info("User logged. Assign roles") + self._assign_roles(roles, user) + return user + + def get_user(self, user_id): + """Required method for Django session management.""" + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None + + def get_groups(self, groups): + return Group.objects.filter(name__in=groups) + + def _assign_roles(self, user_roles, user: AbstractUser): + logger.info(f"User roles: {user_roles}") + allowed_groups = self.get_groups(user_roles) + if not allowed_groups: + logger.info("No allowed groups") + return + groups_to_add = allowed_groups.exclude(id__in=user.groups.values_list('id', flat=True)) + if groups_to_add.exists(): + groups = ", ".join([g.name for g in groups_to_add]) + logger.info(f"Add user '{user.username}' to groups: {groups}") + user.groups.add(*groups_to_add) + + def _get_roles(self, vc_payload: VCPayload): + client_id = settings.VC_LOGIN_CONFIG['role_target'] + if not vc_payload.roles: + return [] + return next((role['names'] for role in vc_payload.roles if role['target'] == client_id), []) \ No newline at end of file diff --git a/src/wirecloud/commons/utils/conf.py b/src/wirecloud/commons/utils/conf.py index 53d3bec5c1..82a3a325af 100644 --- a/src/wirecloud/commons/utils/conf.py +++ b/src/wirecloud/commons/utils/conf.py @@ -106,6 +106,7 @@ def load_default_wirecloud_conf(settings, instance_type='platform'): 'django.contrib.messages.context_processors.messages', 'wirecloud.platform.context_processors.plugins', 'wirecloud.platform.context_processors.active_theme', + 'wirecloud.platform.context_processors.vc_login_context', ), 'debug': settings['DEBUG'], 'loaders': ( diff --git a/src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po b/src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po index 601805e01b..1c7a0bbf8c 100644 --- a/src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po +++ b/src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po @@ -119,6 +119,22 @@ msgstr "Contraseña" msgid "Sign in" msgstr "Iniciar sessión" +#: templates/registration/login.html:61 +msgid "Sign In with Verifiable Credentials" +msgstr "Iniciar sesión con Credenciales Verificables" + +#: templates/registration/login.html:67 +msgid "Invalid Verifiable Credential" +msgstr "Credenciales Verificables invalidas" + +#: templates/registration/login.html:69 +msgid "Unable to login using Verifiable Credentials" +msgstr "No se pudo loguear usando Credenciales Verificables" + +#: templates/registration/login.html:71 +msgid "An unexpected error has occurred. Please try again later" +msgstr "Error inexperado. Por favor, intentelo más tarde" + #: templates/wirecloud/catalogue/main_resource_details.html:3 msgid "Description" msgstr "Descripción" @@ -389,3 +405,11 @@ msgstr "" "