diff --git a/.devcontainer/.env b/.devcontainer/.env index 98923e8ed11..a3c8523037a 100644 --- a/.devcontainer/.env +++ b/.devcontainer/.env @@ -195,4 +195,4 @@ MODIFY_TOPICCATEGORY=True AVATAR_GRAVATAR_SSL=True EXIF_ENABLED=True CREATE_LAYER=True -FAVORITE_ENABLED=True \ No newline at end of file +FAVORITE_ENABLED=True diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8ae2150784a..aedbf448d49 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -60,4 +60,4 @@ // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. // "remoteUser": "vscode" -} +} \ No newline at end of file diff --git a/geonode/base/apps.py b/geonode/base/apps.py index 4b24947e52a..e633659110e 100644 --- a/geonode/base/apps.py +++ b/geonode/base/apps.py @@ -24,6 +24,7 @@ class BaseAppConfig(NotificationsAppConfigBase, AppConfig): name = "geonode.base" + default_auto_field = "django.db.models.BigAutoField" NOTIFICATIONS = ( ( "request_download_resourcebase", diff --git a/geonode/base/models.py b/geonode/base/models.py index b7805e75adf..0c89173f632 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -31,7 +31,6 @@ from django.db import transaction from django.db import models -from django.db.models import Max from django.conf import settings from django.utils.html import escape from django.utils.timezone import now @@ -989,16 +988,16 @@ def save(self, notify=False, *args, **kwargs): _notification_sent = False _group_status_changed = False _approval_status_changed = False - + send_create_notification = False if hasattr(self, "class_name") and (self.pk is None or notify): - if self.pk is None and (self.title or getattr(self, "name", None)): - # Resource Created - if not self.title and getattr(self, "name", None): - self.title = getattr(self, "name", None) - notice_type_label = f"{self.class_name.lower()}_created" - recipients = get_notification_recipients(notice_type_label, resource=self) - send_notification(recipients, notice_type_label, {"resource": self}) - elif self.pk: + # if self.pk is None and (self.title or getattr(self, "name", None)): + # # Resource Created + # if not self.title and getattr(self, "name", None): + # self.title = getattr(self, "name", None) + # notice_type_label = f"{self.class_name.lower()}_created" + # recipients = get_notification_recipients(notice_type_label, resource=self) + # send_notification(recipients, notice_type_label, {"resource": self}) + if self.pk: # Group has changed _group_status_changed = self.group != ResourceBase.objects.get(pk=self.get_self_resource().pk).group @@ -1031,15 +1030,14 @@ def save(self, notify=False, *args, **kwargs): send_notification(recipients, notice_type_label, {"resource": self}) if self.pk is None: - _initial_value = ResourceBase.objects.aggregate(Max("pk"))["pk__max"] - if not _initial_value: - _initial_value = 1 - else: - _initial_value += 1 + # behaviour changed with Djagno 5.2 + base = ResourceBase.objects + _initial_value = 1 if not base.exists() else base.last().id + 1 _next_value = get_next_value("ResourceBase", initial_value=_initial_value) # type(self).__name__, if _initial_value > _next_value: Sequence.objects.filter(name="ResourceBase").update(last=_initial_value) _next_value = _initial_value + send_create_notification = True self.pk = self.id = _next_value @@ -1049,6 +1047,13 @@ def save(self, notify=False, *args, **kwargs): self.uuid = str(uuid.uuid4()) super().save(*args, **kwargs) + if send_create_notification: + # changed in Django 5.2, the we can get the title via the assets only if the resource is saved + if not self.title and hasattr(self, "name") and getattr(self, "name", None): + self.title = getattr(self, "name", None) + notice_type_label = f"{self.__class__.__name__.lower()}_created" + recipients = get_notification_recipients(notice_type_label, resource=self) + send_notification(recipients, notice_type_label, {"resource": self}) # Update workflow permissions if _approval_status_changed or _group_status_changed: self.set_permissions( diff --git a/geonode/documents/apps.py b/geonode/documents/apps.py index 72e2ad96375..27707aed711 100644 --- a/geonode/documents/apps.py +++ b/geonode/documents/apps.py @@ -24,6 +24,7 @@ class DocumentsAppConfig(NotificationsAppConfigBase, AppConfig): name = "geonode.documents" + default_auto_field = "django.db.models.BigAutoField" NOTIFICATIONS = ( ( "document_created", diff --git a/geonode/geoapps/apps.py b/geonode/geoapps/apps.py index 3985e342070..01d59734453 100644 --- a/geonode/geoapps/apps.py +++ b/geonode/geoapps/apps.py @@ -24,7 +24,7 @@ class GeoNodeAppsConfig(NotificationsAppConfigBase, AppConfig): name = "geonode.geoapps" type = "GEONODE_APP" - + default_auto_field = "django.db.models.BigAutoField" NOTIFICATIONS = ( ( "geoapp_created", diff --git a/geonode/layers/admin.py b/geonode/layers/admin.py index cfa40c547bf..96c17b5be17 100644 --- a/geonode/layers/admin.py +++ b/geonode/layers/admin.py @@ -66,7 +66,7 @@ class DatasetAdmin(TabbedTranslationAdmin): "dirty_state", ) search_fields = ("alternate", "title", "abstract", "purpose", "is_approved", "is_published", "state") - filter_horizontal = ("contacts",) + # filter_horizontal = ("contacts",) date_hierarchy = "date" readonly_fields = ("uuid", "alternate", "workspace", "geographic_bounding_box") inlines = ( diff --git a/geonode/layers/apps.py b/geonode/layers/apps.py index f7c503179a0..789dd690693 100644 --- a/geonode/layers/apps.py +++ b/geonode/layers/apps.py @@ -26,6 +26,7 @@ class DatasetAppConfig(NotificationsAppConfigBase, AppConfig): name = "geonode.layers" verbose_name = "Dataset" verbose_name_plural = "Datasets" + default_auto_field = "django.db.models.BigAutoField" NOTIFICATIONS = ( ( "dataset_created", diff --git a/geonode/maps/apps.py b/geonode/maps/apps.py index 762c6743ec4..b8f6e573a4d 100644 --- a/geonode/maps/apps.py +++ b/geonode/maps/apps.py @@ -25,6 +25,7 @@ class MapsAppConfig(NotificationsAppConfigBase, AppConfig): name = "geonode.maps" + default_auto_field = "django.db.models.BigAutoField" NOTIFICATIONS = ( ( "map_created", diff --git a/geonode/metadata/tests/test_handlers.py b/geonode/metadata/tests/test_handlers.py index 82f3faf98d5..499468698e0 100644 --- a/geonode/metadata/tests/test_handlers.py +++ b/geonode/metadata/tests/test_handlers.py @@ -1533,7 +1533,7 @@ def test_tkeywords_handler_update_resource(self): about__in=["http://example.com/keyword1", "http://example.com/keyword2"] ) - self.assertQuerysetEqual( + self.assertQuerySetEqual( updated_keywords.order_by("id"), expected_keywords.order_by("id"), transform=lambda x: x ) diff --git a/geonode/people/hashers.py b/geonode/people/hashers.py new file mode 100644 index 00000000000..a183e592a55 --- /dev/null +++ b/geonode/people/hashers.py @@ -0,0 +1,83 @@ +import hashlib +import math +from django.utils.translation import gettext_noop as _ +from django.utils.crypto import ( + RANDOM_STRING_CHARS, + constant_time_compare, +) +from django.contrib.auth.hashers import PBKDF2SHA1PasswordHasher, BasePasswordHasher + + +def mask_hash(hash, show=6, char="*"): + """ + Return the given hash, with only the first ``show`` number shown. The + rest are masked with ``char`` for security reasons. + """ + masked = hash[:show] + masked += char * len(hash[show:]) + return masked + + +def must_update_salt(salt, expected_entropy): + # Each character in the salt provides log_2(len(alphabet)) bits of entropy. + return len(salt) * math.log2(len(RANDOM_STRING_CHARS)) < expected_entropy + + +class SHA1PasswordHasher(BasePasswordHasher): + """ + This is the legecy SHA1 password hasher which will be removed in future releases. + """ + + algorithm = "sha1" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def encode(self, password, salt): + self._check_encode_args(password, salt) + hash = hashlib.sha1((salt + password).encode()).hexdigest() + return "%s$%s$%s" % (self.algorithm, salt, hash) + + def decode(self, encoded): + algorithm, salt, hash = encoded.split("$", 2) + assert algorithm == self.algorithm + return { + "algorithm": algorithm, + "hash": hash, + "salt": salt, + } + + def verify(self, password, encoded): + decoded = self.decode(encoded) + encoded_2 = self.encode(password, decoded["salt"]) + return constant_time_compare(encoded, encoded_2) + + def safe_summary(self, encoded): + decoded = self.decode(encoded) + return { + _("algorithm"): decoded["algorithm"], + _("salt"): mask_hash(decoded["salt"], show=2), + _("hash"): mask_hash(decoded["hash"]), + } + + def must_update(self, encoded): + decoded = self.decode(encoded) + return must_update_salt(decoded["salt"], self.salt_entropy) + + def harden_runtime(self, password, encoded): + pass + + +class PBKDF2SHA1WrappedSHA1PasswordHasher(PBKDF2SHA1PasswordHasher): + """ + A password hasher that wraps SHA1 hashes in a PBKDF2SHA1 hash. + """ + + algorithm = "pbkdf2sha1_wrapped_sha1" + + def encode_sha1_hash(self, sha1_hash, salt, iterations=None): + return super().encode(sha1_hash, salt, iterations) + + def encode(self, password, salt, iterations=None): + _, _, sha1_hash = SHA1PasswordHasher().encode(password, salt).split("$", 2) + return self.encode_sha1_hash(sha1_hash, salt, iterations) diff --git a/geonode/people/migrations/0037_migrate_sha1_passwords.py b/geonode/people/migrations/0037_migrate_sha1_passwords.py new file mode 100644 index 00000000000..cadff4f88ea --- /dev/null +++ b/geonode/people/migrations/0037_migrate_sha1_passwords.py @@ -0,0 +1,23 @@ +from django.db import migrations + +from geonode.people.hashers import PBKDF2SHA1WrappedSHA1PasswordHasher + + +def forwards_func(apps, schema_editor): + User = apps.get_model("people", "Profile") + users = User.objects.filter(password__startswith="sha1$") + hasher = PBKDF2SHA1WrappedSHA1PasswordHasher() + for user in users: + algorithm, salt, sha1_hash = user.password.split("$", 2) + user.password = hasher.encode_sha1_hash(sha1_hash, salt) + user.save(update_fields=["password"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("people", "0036_merge_20210706_0951"), + ] + + operations = [ + migrations.RunPython(forwards_func), + ] diff --git a/geonode/people/tests.py b/geonode/people/tests.py index 0183c3834b1..8bea76afbfc 100644 --- a/geonode/people/tests.py +++ b/geonode/people/tests.py @@ -37,6 +37,8 @@ from geonode.base.populate_test_data import all_public, create_models, create_single_dataset, remove_models from geonode.security.registry import permissions_registry +from geonode.people.hashers import SHA1PasswordHasher +from geonode.people.hashers import PBKDF2SHA1WrappedSHA1PasswordHasher class PeopleAndProfileTests(GeoNodeBaseTestSupport): @@ -1298,3 +1300,25 @@ def test_transfer_resource_subset(self): # since the payload say "default" self.assertTrue(resource_to_transfer.owner == new_owner) self.assertTrue(second_resource.owner == new_owner) + + def test_migrate_sha1_passwords(self): + User = get_user_model() + user = User.objects.create_user(username="sha1user", password="password") + # Manually hash the password using SHA1 and save it directly to the database + hasher = SHA1PasswordHasher() + encoded_password = hasher.encode("password", "salt") + User.objects.filter(pk=user.pk).update(password=encoded_password) + user.refresh_from_db() + self.assertTrue(user.password.startswith("sha1")) + # forward function logic on migration to pbkdf2sha1_wrapped_sha1 + new_hasher = PBKDF2SHA1WrappedSHA1PasswordHasher() + algorithm, salt, sha1_hash = user.password.split("$", 2) + user.password = new_hasher.encode_sha1_hash(sha1_hash, salt) + user.save(update_fields=["password"]) + user.refresh_from_db() + self.assertFalse(user.password.startswith("sha1")) + self.assertTrue(user.password.startswith("pbkdf2sha1_wrapped_sha1")) + # Check that the user can still log in with their original password + self.assertTrue(user.check_password("password")) + # after checking it should be migrated to default hash + self.assertTrue(user.password.startswith("pbkdf2_sha1")) diff --git a/geonode/resource/processing/admin.py b/geonode/resource/processing/admin.py index 1f13d86269e..b2bd0c8ea1f 100644 --- a/geonode/resource/processing/admin.py +++ b/geonode/resource/processing/admin.py @@ -32,7 +32,7 @@ class ProcessingWorkflowAdmin(admin.ModelAdmin): "id", "name", ) - filter_horizontal = ("processing_tasks",) + # filter_horizontal = ("processing_tasks",) inlines = [ProcessingWorkflowTasksInline] diff --git a/geonode/security/middleware.py b/geonode/security/middleware.py index db6ba3ddc99..38a1296dfc9 100644 --- a/geonode/security/middleware.py +++ b/geonode/security/middleware.py @@ -84,6 +84,8 @@ class LoginRequiredMiddleware(MiddlewareMixin): redirect_to = login_url def __init__(self, get_response): + if get_response: + super().__init__(get_response) self.get_response = get_response def process_request(self, request): @@ -108,6 +110,8 @@ class SessionControlMiddleware(MiddlewareMixin): Middleware that checks if session variables have been correctly set. """ + async_mode = False + redirect_to = getattr(settings, "LOGIN_URL", reverse("account_login")) def __init__(self, get_response): diff --git a/geonode/security/tests.py b/geonode/security/tests.py index 4eb00d1800c..b243cf27717 100644 --- a/geonode/security/tests.py +++ b/geonode/security/tests.py @@ -323,7 +323,7 @@ def test_session_ctrl_middleware(self): request = HttpRequest() request.user = admin request.session = engine.SessionStore() - request.session["access_token"] = get_or_create_token(admin) + request.session["access_token"] = str(get_or_create_token(admin)) request.session.save() middleware.process_request(request) self.assertFalse(request.session.is_empty()) diff --git a/geonode/services/views.py b/geonode/services/views.py index 3341fb7fcc1..5959306e5e4 100644 --- a/geonode/services/views.py +++ b/geonode/services/views.py @@ -72,8 +72,9 @@ def register_service(request): if service_handler.indexing_method == enumerations.CASCADED: service_handler.create_cascaded_store(service) service_handler.geonode_service_id = service.id - request.session[service_handler.url] = service_handler - logger.debug("Added handler to the session") + # commented out due to jsonserializer error, will be replaced with cache + # request.session[service_handler.url] = service_handler + # logger.debug("Added handler to the session") messages.add_message(request, messages.SUCCESS, _("Service registered successfully")) result = HttpResponseRedirect(reverse("harvest_resources", kwargs={"service_id": service.id})) else: @@ -95,8 +96,9 @@ def _get_service_handler(request, service): service_handler = get_service_handler(service.service_url, service.type, service.id) if not service_handler.geonode_service_id: service_handler.geonode_service_id = service.id - request.session[service.service_url] = service_handler - logger.debug("Added handler to the session") + # commented out due to jsonserializer error, will be replaced with cache + # request.session[service.service_url] = service_handler + # logger.debug("Added handler to the session") return service_handler @@ -185,14 +187,15 @@ def harvest_resources_handle_post(request, service, handler): @login_required def harvest_resources(request, service_id): service = get_object_or_404(Service, pk=service_id) - try: - handler = request.session[service.service_url] - if not handler.geonode_service_id: - handler.geonode_service_id = service_id - except KeyError: # handler is not saved on the session, recreate it - handler = _get_service_handler(request, service) - if not handler.geonode_service_id: - handler.geonode_service_id = service_id + # commented out due to jsonserializer error, will be replaced with cache + # try: + # handler = request.session[service.service_url] + # if not handler.geonode_service_id: + # handler.geonode_service_id = service_id + # except KeyError: # handler is not saved on the session, recreate it + handler = _get_service_handler(request, service) + if not handler.geonode_service_id: + handler.geonode_service_id = service_id if request.method == "GET": return harvest_resources_handle_get(request, service, handler) elif request.method == "POST": @@ -281,10 +284,11 @@ def service_detail(request, service_id): # speed up the register/harvest resources flow. However, for services # with many resources, keeping the handler in the session leads to degraded # performance - try: - request.session.pop(service.service_url) - except KeyError: - pass + # commented out due to jsonserializer error, will be replaced with cache + # try: + # request.session.pop(service.service_url) + # except KeyError: + # pass return render( request, diff --git a/geonode/settings.py b/geonode/settings.py index 0c84b60d614..53e1e4a6071 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -250,9 +250,9 @@ AUTH_USER_MODEL = os.getenv("AUTH_USER_MODEL", "people.Profile") PASSWORD_HASHERS = [ - "django.contrib.auth.hashers.SHA1PasswordHasher", - "django.contrib.auth.hashers.PBKDF2PasswordHasher", "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "geonode.people.hashers.PBKDF2SHA1WrappedSHA1PasswordHasher", # Wrapped Hasher # 'django.contrib.auth.hashers.Argon2PasswordHasher', # 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', # 'django.contrib.auth.hashers.BCryptPasswordHasher', @@ -470,7 +470,7 @@ "django_filters", "mptt", "storages", - "floppyforms", + # "floppyforms", "tinymce", "widget_tweaks", "django_celery_results", @@ -831,7 +831,7 @@ MESSAGE_STORAGE = "django.contrib.messages.storage.cookie.CookieStorage" # Sessions -SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer" +SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" SESSION_ENGINE = os.environ.get("SESSION_ENGINE", "django.contrib.sessions.backends.db") if SESSION_ENGINE in ("django.contrib.sessions.backends.cached_db", "django.contrib.sessions.backends.cache"): SESSION_CACHE_ALIAS = "memcached" # use memcached cache if a cached backend is requested diff --git a/requirements.txt b/requirements.txt index bad8906b998..d3695526c4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Pillow==11.3.0 lxml==5.2.1 psycopg2==2.9.9 -Django==4.2.23 +Django==5.2 # Other beautifulsoup4==4.12.3 @@ -31,7 +31,7 @@ django-imagekit==5.0.0 django-taggit==5.0.1 django-markdownify==0.9.5 django-mptt==0.16.0 -django-modeltranslation>=0.11,<0.19.0 +django-modeltranslation==0.18.13 django-treebeard==4.7.1 django-guardian<2.4.1 django-downloadview==2.3.0 @@ -61,7 +61,8 @@ Jinja2==3.1.4 dj-database-url==2.1.0 dj-pagination==2.5.0 django-select2==8.1.2 -django-floppyforms<1.10.0 +#django-floppyforms<1.10.0 + django-forms-bootstrap<=3.1.0 django-autocomplete-light==3.11.0 django-invitations<2.1.1 @@ -73,14 +74,17 @@ djangorestframework-gis==1.2.0 djangorestframework-guardian==0.3.0 drf-extensions==0.7.1 drf-spectacular==0.27.2 -dynamic-rest==2.3.0 +#dynamic-rest==2.3.0 +git+https://github.com/GeoNode/dynamic-rest@master#egg=dynamic-rest + -geonode-pinax-notifications==6.0.0.2 +#geonode-pinax-notifications==6.0.0.2 +git+https://github.com/GeoNode/pinax-notifications@master#egg=geonode-pinax-notifications # GeoNode org maintained apps. # django-geonode-mapstore-client==4.0.5 -git+https://github.com/GeoNode/geonode-mapstore-client.git@master#egg=django_geonode_mapstore_client -django-avatar==8.0.0 +-e git+https://github.com/GeoNode/geonode-mapstore-client.git@django_upgrade#egg=django_geonode_mapstore_client +django-avatar==8.0.1 geonode-oauth-toolkit==2.2.2.2 geonode-announcements==2.0.2.2 django-activity-stream==2.0.0 @@ -90,8 +94,8 @@ gn-gsimporter==2.0.4 gisdata==0.5.4 # importer dependencies -setuptools>=59 -gdal<=3.4.3 +setuptools>=59,<81 +gdal<=3.6.4 pdok-geopackage-validator==0.8.5 geonode-django-dynamic-model==0.4.0 diff --git a/setup.cfg b/setup.cfg index 329eb9ff294..3d9ce7276f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,8 +28,7 @@ install_requires = Pillow==11.3.0 lxml==5.2.1 psycopg2==2.9.9 - Django==4.2.23 - + Django==5.2 # Other beautifulsoup4==4.12.3 hyperlink==21.0.0 @@ -57,7 +56,7 @@ install_requires = django-taggit==5.0.1 django-markdownify==0.9.5 django-mptt==0.16.0 - django-modeltranslation>=0.11,<0.19.0 + django-modeltranslation==0.18.13 django-treebeard==4.7.1 django-guardian<2.4.1 django-downloadview==2.3.0 @@ -87,7 +86,7 @@ install_requires = dj-database-url==2.1.0 dj-pagination==2.5.0 django-select2==8.1.2 - django-floppyforms<1.10.0 + #django-floppyforms<1.10.0 django-forms-bootstrap<=3.1.0 django-autocomplete-light==3.11.0 django-invitations<2.1.1 @@ -99,13 +98,17 @@ install_requires = djangorestframework-guardian==0.3.0 drf-extensions==0.7.1 drf-spectacular==0.27.2 - dynamic-rest==2.3.0 + dynamic-rest @ git+https://github.com/GeoNode/dynamic-rest@master#egg=dynamic-rest geonode-pinax-notifications==6.0.0.2 + #geonode-pinax-notifications==6.0.0.2 + geonode-pinax-notifications @ git+https://github.com/GeoNode/pinax-notifications@master#egg=geonode-pinax-notifications # GeoNode org maintained apps. - django-geonode-mapstore-client>=4.0.5,<5.1.0 - django-avatar==8.0.0 + # django-geonode-mapstore-client==4.0.5 + django-geonode-mapstore-client @ git+https://github.com/GeoNode/geonode-mapstore-client.git@django_upgrade#egg=django_geonode_mapstore_client + + django-avatar==8.0.1 geonode-oauth-toolkit==2.2.2.2 geonode-announcements==2.0.2.2 django-activity-stream==2.0.0 @@ -115,8 +118,8 @@ install_requires = gisdata==0.5.4 # importer dependencies - setuptools>=59 - gdal<=3.4.3 + setuptools>=59,<81 + gdal<=3.6.4 pdok-geopackage-validator==0.8.5 geonode-django-dynamic-model==0.4.0