Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8d2c7e4
Upgrade to django5
mattiagiupponi May 26, 2025
14ec9e0
Merge branch 'master' of github.com:GeoNode/geonode into ISSUE_13122
mattiagiupponi May 26, 2025
605922f
Upgrade to django5
mattiagiupponi May 26, 2025
0d115a4
Upgrade to django5
mattiagiupponi May 26, 2025
f1fefe5
Upgrade to django5
mattiagiupponi May 27, 2025
962e1c4
Merge branch 'master' of github.com:GeoNode/geonode into ISSUE_13122
mattiagiupponi May 27, 2025
ea629c8
Upgrade to django5
mattiagiupponi May 27, 2025
c642e17
Upgrade to django5
mattiagiupponi May 27, 2025
79d822f
Upgrade to django5
mattiagiupponi May 27, 2025
b809bed
Upgrade to django5
mattiagiupponi May 27, 2025
2fef765
Merge branch 'master' of github.com:GeoNode/geonode into ISSUE_13122
mattiagiupponi Jun 11, 2025
c102301
Upgrade to django5
mattiagiupponi Jun 12, 2025
b888984
Upgrade to django5
mattiagiupponi Jun 12, 2025
fdf6d9b
merge with master
mattiagiupponi Jun 13, 2025
33fbda1
merge with master
mattiagiupponi Jun 13, 2025
4110462
merge with master
mattiagiupponi Jun 13, 2025
6ffdf01
Fixes #13122 django5 upgrade
mattiagiupponi Jun 17, 2025
394af2e
Fixes #13122: Added Pbk2sha1 hasher migration from sha1
sijandh35 Jul 10, 2025
ff7847d
Fixes #13122: solved not null voilation on test case fail due to null…
sijandh35 Jul 16, 2025
cd1bb93
Fixes #13122: changes from assertQuerysetEqual to assertQuerySetEqual
sijandh35 Jul 16, 2025
bdc09f3
Fixes #13122: disable the session serialization for service handler w…
sijandh35 Jul 17, 2025
c526ca6
Fixes #13122: test case for migrations of sha password
sijandh35 Jul 17, 2025
f381e17
Merge branch 'master' into ISSUE_13122
sijandh35 Jul 17, 2025
58c0da9
Fixes #13122: boto3 back to previous
sijandh35 Jul 17, 2025
d26724f
Fixes #13122: solves 'LoginRequiredMiddleware' object has no attribut…
sijandh35 Jul 17, 2025
a0df6dc
Fixes #13122: solves 'LoginRequiredMiddleware' object has no attribut…
sijandh35 Jul 17, 2025
467450b
Fixes #13122: remove unnecessary import
sijandh35 Jul 17, 2025
a1320d9
Fixes #13122: commented reason on middlewaremixin for test case fail …
sijandh35 Jul 17, 2025
ef715a0
Fixes #13122: add super call to resolve ansync issue on MiddlewareMixin
sijandh35 Jul 18, 2025
226f788
Fixes #13122: add super call to resolve ansync issue on MiddlewareMixin
sijandh35 Jul 18, 2025
80fb10e
[Fixes #13122] Fix unwanted changes
mattiagiupponi Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/.env
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,4 @@ MODIFY_TOPICCATEGORY=True
AVATAR_GRAVATAR_SSL=True
EXIF_ENABLED=True
CREATE_LAYER=True
FAVORITE_ENABLED=True
FAVORITE_ENABLED=True
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions geonode/base/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

class BaseAppConfig(NotificationsAppConfigBase, AppConfig):
name = "geonode.base"
default_auto_field = "django.db.models.BigAutoField"
NOTIFICATIONS = (
(
"request_download_resourcebase",
Expand Down
35 changes: 20 additions & 15 deletions geonode/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions geonode/documents/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

class DocumentsAppConfig(NotificationsAppConfigBase, AppConfig):
name = "geonode.documents"
default_auto_field = "django.db.models.BigAutoField"
NOTIFICATIONS = (
(
"document_created",
Expand Down
2 changes: 1 addition & 1 deletion geonode/geoapps/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
class GeoNodeAppsConfig(NotificationsAppConfigBase, AppConfig):
name = "geonode.geoapps"
type = "GEONODE_APP"

default_auto_field = "django.db.models.BigAutoField"
NOTIFICATIONS = (
(
"geoapp_created",
Expand Down
2 changes: 1 addition & 1 deletion geonode/layers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
1 change: 1 addition & 0 deletions geonode/layers/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions geonode/maps/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

class MapsAppConfig(NotificationsAppConfigBase, AppConfig):
name = "geonode.maps"
default_auto_field = "django.db.models.BigAutoField"
NOTIFICATIONS = (
(
"map_created",
Expand Down
2 changes: 1 addition & 1 deletion geonode/metadata/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
83 changes: 83 additions & 0 deletions geonode/people/hashers.py
Original file line number Diff line number Diff line change
@@ -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()
Comment thread Dismissed
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)
23 changes: 23 additions & 0 deletions geonode/people/migrations/0037_migrate_sha1_passwords.py
Original file line number Diff line number Diff line change
@@ -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),
]
24 changes: 24 additions & 0 deletions geonode/people/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"))
2 changes: 1 addition & 1 deletion geonode/resource/processing/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ProcessingWorkflowAdmin(admin.ModelAdmin):
"id",
"name",
)
filter_horizontal = ("processing_tasks",)
# filter_horizontal = ("processing_tasks",)
inlines = [ProcessingWorkflowTasksInline]


Expand Down
4 changes: 4 additions & 0 deletions geonode/security/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion geonode/security/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
36 changes: 20 additions & 16 deletions geonode/services/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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


Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading