From d62c1419dfd6656e16b11d5648815bc49eeb06e5 Mon Sep 17 00:00:00 2001 From: Walter Lorenzetti Date: Wed, 18 Dec 2024 16:24:55 +0100 Subject: [PATCH 1/4] Add social account login capability (#994) * Add settings and urls * Login template * Update requirements.txt * Add setting fo socialaccount * Add pre_social_login signal receiver * Fix connect user socialaccount * Update base settings * Update settings * Management fo role and backend of user logged by socialaccount * Update requirements.txt * Typo * Add ACCOUNT_EMAIL_VERIFICATION = 'none' * Change login backend order --------- Co-authored-by: wlorenzetti --- g3w-admin/base/settings/base.py | 40 +++++++---- .../base/settings/local_settings_example.py | 4 ++ g3w-admin/base/urls.py | 3 + g3w-admin/templates/login.html | 72 ++++++++++--------- g3w-admin/usersmanage/receivers.py | 2 + .../socialaccount/provider_list.html | 22 ++++++ g3w-admin/usersmanage/vendors/__init__.py | 0 .../usersmanage/vendors/allauth/__init__.py | 0 .../usersmanage/vendors/allauth/adapter.py | 58 +++++++++++++++ requirements.txt | 6 +- 10 files changed, 159 insertions(+), 48 deletions(-) create mode 100644 g3w-admin/usersmanage/templates/socialaccount/provider_list.html create mode 100644 g3w-admin/usersmanage/vendors/__init__.py create mode 100644 g3w-admin/usersmanage/vendors/allauth/__init__.py create mode 100644 g3w-admin/usersmanage/vendors/allauth/adapter.py diff --git a/g3w-admin/base/settings/base.py b/g3w-admin/base/settings/base.py index aa8a105f0..554afc9a9 100644 --- a/g3w-admin/base/settings/base.py +++ b/g3w-admin/base/settings/base.py @@ -48,7 +48,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.gis', - #'django.contrib.sites', + 'django.contrib.sites', ] THIRD_PARTY_APPS = [ @@ -75,7 +75,13 @@ 'about', 'django_bleach', 'django_registration', - 'captcha' + 'captcha', + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.microsoft", + "allauth.socialaccount.providers.google", + "allauth.usersessions" ] G3WADMIN_APPS = [ @@ -104,6 +110,10 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware', 'qdjango.process_events_middleware.process_events_middleware', + + # Allauth middleware + 'allauth.account.middleware.AccountMiddleware', + 'allauth.usersessions.middleware.UserSessionsMiddleware' ] @@ -127,10 +137,6 @@ 'loaders': [ 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader' - #('django.template.loaders.cached.Loader', [ - # 'django.template.loaders.filesystem.Loader', - # 'django.template.loaders.app_directories.Loader' - #]), ], }, }, @@ -142,7 +148,6 @@ ATOMIC_REQUESTS = True # Password validation -# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -162,6 +167,7 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'guardian.backends.ObjectPermissionBackend', + 'allauth.account.auth_backends.AuthenticationBackend' ) GUARDIAN_RAISE_403 = True @@ -241,8 +247,6 @@ # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.9/howto/static-files/ - STATIC_URL = '/static/' SITE_TITLE = 'g3w-admin' @@ -252,7 +256,7 @@ QDJANGO_PRJ_CACHE = True QDJANGO_PRJ_CACHE_KEY = 'qdjango_prj_' -# data for proxy server +# Data for proxy server PROXY_SERVER = False # LOGGING_CONFIG = None @@ -303,7 +307,6 @@ SITE_ID = 1 - INTERNAL_IPS = [ '127.0.0.1', ] @@ -344,7 +347,6 @@ # QPLOTLY DEFAULT SETTINGS # ------------------------ - LOAD_QPLOTLY_FROM_PROJECT = False # VENDOR KEYS SETTINGS @@ -387,4 +389,16 @@ # Settings for ReCaptcha v2. Default checkbox RECAPTCHA_VERSION2_TYPE = 'checkbox' # or 'invisible' -SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error'] \ No newline at end of file +SILENCED_SYSTEM_CHECKS = ['captcha.recaptcha_test_key_error'] + +# SOCIAL ACCOUNT LOGIN SETTINGS +# ----------------------------- +SOCIALACCOUNT_LOGIN_ON_GET = True +SOCIALACCOUNT_ADAPTER = 'usersmanage.vendors.allauth.adapter.G3WSocialAccountAdapter' + +# Activate/deactivate user login session tracking +USERSESSIONS_TRACK_ACTIVITY = False + +SOCIALACCOUNT_ONLY = True +SOCIALACCOUNT_USER_ROLE = 'Viewer Level 1' +ACCOUNT_EMAIL_VERIFICATION = 'none' \ No newline at end of file diff --git a/g3w-admin/base/settings/local_settings_example.py b/g3w-admin/base/settings/local_settings_example.py index c4589d510..4b124f9b3 100644 --- a/g3w-admin/base/settings/local_settings_example.py +++ b/g3w-admin/base/settings/local_settings_example.py @@ -163,3 +163,7 @@ 'worker_type': 'process', }, } + +# For social login +# Activate/deactivate user login session tracking +USERSESSIONS_TRACK_ACTIVITY = False \ No newline at end of file diff --git a/g3w-admin/base/urls.py b/g3w-admin/base/urls.py index 4bea8cf2c..541c84fba 100644 --- a/g3w-admin/base/urls.py +++ b/g3w-admin/base/urls.py @@ -168,8 +168,11 @@ ), name="django_registration_disallowed", ), + path('allauthg/', include('allauth.urls')), ] + + ############################################################# # PASSWORD RESET (user password reset by email) # USERNAME RECOVERY (username recovery by email) diff --git a/g3w-admin/templates/login.html b/g3w-admin/templates/login.html index f66ccc7de..78c0fc87f 100644 --- a/g3w-admin/templates/login.html +++ b/g3w-admin/templates/login.html @@ -1,4 +1,5 @@ {% extends "base_login.html" %} +{% load socialaccount %} {% load static %} {% load i18n %} @@ -22,42 +23,47 @@

{% trans 'LOGIN ERROR' %}!

{% trans 'Username and/or password uncorrect' %} {% endif %} -
- {% csrf_token %} -
- - -
-
- - -
-
{{ form.captcha }}
-
-
- -
-
- {% if SETTINGS.RESET_USER_PASSWORD %} -
-
- {% trans 'Forgot your password?' %} + + {% csrf_token %} +
+ +
-
-
- - {% endif %} - {% if SETTINGS.REGISTRATION_OPEN %} -
-
- {% trans 'Sign up' %} +
{{ form.captcha }}
+
+
+ +
-
- {% endif %} - + +
+ {% include "socialaccount/provider_list.html" with process="login" %} +
+ + {% if SETTINGS.RESET_USER_PASSWORD %} + + + {% endif %} + {% if SETTINGS.REGISTRATION_OPEN %} + + {% endif %} +
{% include 'include/login_page_version.html' %}
diff --git a/g3w-admin/usersmanage/receivers.py b/g3w-admin/usersmanage/receivers.py index a07ffc399..b528716c3 100644 --- a/g3w-admin/usersmanage/receivers.py +++ b/g3w-admin/usersmanage/receivers.py @@ -104,3 +104,5 @@ def send_email_to_user(sender, **kwargs): ) user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL, fail_silently=True) + + diff --git a/g3w-admin/usersmanage/templates/socialaccount/provider_list.html b/g3w-admin/usersmanage/templates/socialaccount/provider_list.html new file mode 100644 index 000000000..b3b60307a --- /dev/null +++ b/g3w-admin/usersmanage/templates/socialaccount/provider_list.html @@ -0,0 +1,22 @@ +{% load allauth socialaccount %} +{% load i18n %} +{% get_providers as socialaccount_providers %} +{% if socialaccount_providers %} +
+

- {% trans 'OR' %} -

+
+ {% for provider in socialaccount_providers %} + {% if provider.id == "openid" %} + {% for brand in provider.get_brands %} + {% provider_login_url provider openid=brand.openid_url process=process as href %} + {% element provider name=brand.name provider_id=provider.id href=href %} + {% endelement %} + {% endfor %} + {% endif %} + {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} + + {% endfor %} +
+{% endif %} diff --git a/g3w-admin/usersmanage/vendors/__init__.py b/g3w-admin/usersmanage/vendors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/g3w-admin/usersmanage/vendors/allauth/__init__.py b/g3w-admin/usersmanage/vendors/allauth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/g3w-admin/usersmanage/vendors/allauth/adapter.py b/g3w-admin/usersmanage/vendors/allauth/adapter.py new file mode 100644 index 000000000..343d169e6 --- /dev/null +++ b/g3w-admin/usersmanage/vendors/allauth/adapter.py @@ -0,0 +1,58 @@ +# coding=utf-8 +"""" +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the Mozilla Public License 2.0. + +""" + +__author__ = 'lorenzetti@gis3w.it' +__date__ = '2024-12-17' +__copyright__ = 'Copyright 2015 - 2024, Gis3w' +__license__ = 'MPL 2.0' + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from allauth.account.models import EmailAddress +from usersmanage.models import User, Group as AuthGroup, Userbackend, USER_BACKEND_DEFAULT +from usersmanage.configs import G3W_EDITOR1, G3W_EDITOR2, G3W_VIEWER1 + +class G3WSocialAccountAdapter(DefaultSocialAccountAdapter): + + def _set_user_role_backend(self, user): + """ + Set the role and the backend for the user login by social + """ + + # Role to se from settings + role = settings.SOCIALACCOUNT_USER_ROLE \ + if settings.SOCIALACCOUNT_USER_ROLE in (G3W_EDITOR1, G3W_EDITOR2, G3W_VIEWER1) else G3W_VIEWER1 + + AuthGroup.objects.get(name=role).user_set.add(user) + + # Backend + if not hasattr(user, 'userbackend'): + Userbackend(user=user, backend=USER_BACKEND_DEFAULT).save() + + def pre_social_login(self, request, sociallogin): + + # Social account already exists, so this is just a login + if sociallogin.is_existing: + return + + # some social logins don't have an email address + if not sociallogin.email_addresses: + return + try: + existing_user = User.objects.get(email=sociallogin.email_addresses[0].email) + self._set_user_role_backend(existing_user) + except ObjectDoesNotExist: + return + + # if it does, connect this new social login to the existing user + sociallogin.connect(request, existing_user) + + def save_user(self, request, sociallogin, form=None): + user = super(G3WSocialAccountAdapter, self).save_user(request, sociallogin, form=form) + self._set_user_role_backend(user) + return user \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2ab928343..66015f121 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ django-ordered-model==3.3.0 django-ajax-selects==2.2.0 django-modeltranslation==0.17.5 django-filter -django-bleach +django-bleach==3.0.1 pathlib2 lxml psycopg2-binary @@ -30,7 +30,7 @@ django-import-export==3.2.0 coverage urllib3 python-magic -celery==4.4.7 +celery #==5.0.2 deprecation huey==2.4.3 redis==4.5.4 @@ -40,3 +40,5 @@ django-recaptcha==3.0.0 pydantic==2.6.3 weasyprint==63.0 distro==1.7.0 +django-allauth[socialaccount]==0.63.6 + From a03b7c0ec725440289d6b5c3d7ce1540d01625cc Mon Sep 17 00:00:00 2001 From: Walter Lorenzetti Date: Thu, 19 Dec 2024 08:49:41 +0100 Subject: [PATCH 2/4] Fix PyQt object translate (#997) Co-authored-by: wlorenzetti --- g3w-admin/qplotly/utils/qplotly_factory.py | 28 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/g3w-admin/qplotly/utils/qplotly_factory.py b/g3w-admin/qplotly/utils/qplotly_factory.py index 6ba862235..971433ecf 100644 --- a/g3w-admin/qplotly/utils/qplotly_factory.py +++ b/g3w-admin/qplotly/utils/qplotly_factory.py @@ -44,6 +44,12 @@ GeoConstraintsFilter, ) +from qgis.PyQt.QtCore import ( + QDate, + QDateTime, + QTime +) + class QplotlyFactoring(PlotFactory): @@ -92,6 +98,22 @@ def build_layout(self): self.layout = self._build_layout() + def _pyqt2py(self, value): + """ + Check if the value is a PyQT object and translate it in native python object + """ + + tp = type(value) + if tp in (QDate, QDateTime, QTime): + if tp == QDate: + value = value.toPyDate() + if tp == QDateTime: + value = value.toPyDateTime() + if tp == QTime: + value = value.toPyTime() + + return value + def fetch_values_from_layer(self): """ (Re)fetches plot values from the source layer. @@ -238,7 +260,7 @@ def add_source_field_or_expression(field_or_expression): if x == NULL or x is None: continue elif self.settings.properties['x_name']: - x = f[self.settings.properties['x_name']] + x = self._pyqt2py(f[self.settings.properties['x_name']]) if x == NULL or x is None: continue @@ -248,7 +270,7 @@ def add_source_field_or_expression(field_or_expression): if y == NULL or y is None: continue elif self.settings.properties['y_name']: - y = f[self.settings.properties['y_name']] + y = self._pyqt2py(f[self.settings.properties['y_name']]) if y == NULL or y is None: continue @@ -258,7 +280,7 @@ def add_source_field_or_expression(field_or_expression): if z == NULL or z is None: continue elif self.settings.properties['z_name']: - z = f[self.settings.properties['z_name']] + z = self._pyqt2py(f[self.settings.properties['z_name']]) if z == NULL or z is None: continue From a38eabd16f56091cf8835804325219bcc0b94d7f Mon Sep 17 00:00:00 2001 From: Walter Lorenzetti Date: Mon, 23 Dec 2024 08:55:21 +0100 Subject: [PATCH 3/4] Add management for setting SOCIALACCOUNT_USER_GROUP (#1001) Co-authored-by: wlorenzetti --- .../usersmanage/vendors/allauth/adapter.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/g3w-admin/usersmanage/vendors/allauth/adapter.py b/g3w-admin/usersmanage/vendors/allauth/adapter.py index 343d169e6..241159225 100644 --- a/g3w-admin/usersmanage/vendors/allauth/adapter.py +++ b/g3w-admin/usersmanage/vendors/allauth/adapter.py @@ -17,11 +17,16 @@ from usersmanage.models import User, Group as AuthGroup, Userbackend, USER_BACKEND_DEFAULT from usersmanage.configs import G3W_EDITOR1, G3W_EDITOR2, G3W_VIEWER1 +import logging + +logging = logging.getLogger('g3wadmin.debug') + class G3WSocialAccountAdapter(DefaultSocialAccountAdapter): def _set_user_role_backend(self, user): """ Set the role and the backend for the user login by social + Set up alse the group of user if the settings is set """ # Role to se from settings @@ -34,6 +39,22 @@ def _set_user_role_backend(self, user): if not hasattr(user, 'userbackend'): Userbackend(user=user, backend=USER_BACKEND_DEFAULT).save() + # Add User group if is set + if hasattr(settings, 'SOCIALACCOUNT_USER_GROUP'): + try: + ugroup = AuthGroup.objects.get(name=settings.SOCIALACCOUNT_USER_GROUP) + + # Test user role group compatibility + if role in (G3W_VIEWER1, ) and ugroup.grouprole.role != 'viewer': + raise Exception(f"User role {G3W_VIEWER1} not compatible with user group {ugroup}") + + # Assign user to group + ugroup.user_set.add(user) + except Exception as e: + logging.error("[SOCIAL AUTH] Error setting user group: {}".format(e)) + + + def pre_social_login(self, request, sociallogin): # Social account already exists, so this is just a login From 4af4f36a970a504fbe68b8af3dbc12e4b0775f5e Mon Sep 17 00:00:00 2001 From: Walter Lorenzetti Date: Mon, 23 Dec 2024 14:48:37 +0100 Subject: [PATCH 4/4] Fix media update (#1000) * Fix media update * Try to restore the correct media path --------- Co-authored-by: wlorenzetti --- g3w-admin/core/utils/vector.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/g3w-admin/core/utils/vector.py b/g3w-admin/core/utils/vector.py index 2d6a02318..44cc340b6 100644 --- a/g3w-admin/core/utils/vector.py +++ b/g3w-admin/core/utils/vector.py @@ -89,6 +89,15 @@ def get_domain(self): else: return '{}://{}'.format(schema, self.request.get_host()) + def _new_path(self, file_name): + """ Build new path to save media file """ + + return reverse('user-media', kwargs={ + 'project_type': self.type, + 'layer_id': self.layer.pk, + 'file_name': file_name + }) + def new_value(self, change=False): """ Build and save media value from client """ @@ -162,11 +171,17 @@ def new_value(self, change=False): # build new value self.feature_properties[field] = '{}{}'.format(self.get_domain(), - reverse('user-media', kwargs={ - 'project_type': self.type, - 'layer_id': self.layer.pk, - 'file_name': file_name - })) + self._new_path(file_name)) + else: + if is_media_view: + + value = current_field_value + # Try to fix old record saved with temp_uploads path + if os.path.exists(path_file_to_save): + value = self._new_path(file_name) + + # Restore to current_feature value + self.feature_properties[field] = value if delete_old: to_delete = '{}/{}'.format(path_to_save, current_file_name)