Skip to content

Commit 65cc2ec

Browse files
merge
2 parents a59dd32 + f665cc4 commit 65cc2ec

File tree

12 files changed

+248
-65
lines changed

12 files changed

+248
-65
lines changed

geonode/harvesting/harvesters/wms.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from django.conf import settings
3333
from django.contrib.gis import geos
3434
from django.template.defaultfilters import slugify
35-
35+
from requests.auth import HTTPBasicAuth
3636
from geonode.layers.models import Dataset
3737
from geonode.base.models import ResourceBase
3838
from geonode.layers.enumerations import GXP_PTYPES
@@ -201,14 +201,23 @@ def wms_call(self, kind="GetCapabilities", override_version=None, additional_par
201201
params[_param[0]] = _param[1]
202202
# updating default params with custom ones
203203
params = {**params, **additional_params}
204-
205-
# adding basic auth if required
206-
harvester = models.Harvester.objects.get(pk=self.harvester_id).first()
207-
if harvester and harvester._service:
208-
_service = harvester._service
209-
204+
205+
# checking if the services is under basic auth
206+
# getting the service
207+
from geonode.services.models import Service
208+
209+
# check if the connected service has username and password
210+
has_basic_auth = Service.objects.filter(
211+
harvester__pk=self.harvester_id, username__isnull=False, password__isnull=False
212+
)
213+
basic_auth = None
214+
if has_basic_auth.exists():
215+
# if the username and password are set, we can prepare the basic auth for the request
216+
service = has_basic_auth.first()
217+
basic_auth = HTTPBasicAuth(service.username, service.get_password())
218+
210219
get_capabilities_response = self.http_session.get(
211-
self.get_ogc_wms_url(wms_url, version=_version), params=params
220+
self.get_ogc_wms_url(wms_url, version=_version), params=params, auth=basic_auth
212221
)
213222
get_capabilities_response.raise_for_status()
214223
return get_capabilities_response

geonode/services/forms.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,12 @@
3333

3434

3535
class CreateServiceForm(forms.Form):
36+
3637
url = forms.CharField(
3738
label=_("Service URL"),
3839
max_length=512,
3940
widget=forms.TextInput(
40-
attrs={
41-
"size": "65",
42-
"class": "inputText",
43-
"required": "",
44-
"type": "url",
45-
}
41+
attrs={"size": "65", "class": "inputText", "required": "", "type": "url", "autocomplete": "off"}
4642
),
4743
)
4844
type = forms.ChoiceField(
@@ -51,6 +47,22 @@ class CreateServiceForm(forms.Form):
5147
initial="AUTO",
5248
)
5349

50+
username = forms.CharField(
51+
required=False,
52+
initial=None,
53+
label=_("Username (optional)"),
54+
max_length=200,
55+
widget=forms.TextInput(attrs={"autocomplete": "off"}),
56+
)
57+
58+
password = forms.CharField(
59+
required=False,
60+
initial=None,
61+
label=_("password (optional)"),
62+
max_length=200,
63+
widget=forms.PasswordInput(attrs={"autocomplete": "off"}),
64+
)
65+
5466
def clean_url(self):
5567
proposed_url = self.cleaned_data["url"]
5668
existing = Service.objects.filter(base_url=proposed_url).exists()
@@ -65,7 +77,12 @@ def clean(self):
6577
service_type = self.cleaned_data.get("type")
6678
if url is not None and service_type is not None:
6779
try:
68-
service_handler = get_service_handler(base_url=url, service_type=service_type)
80+
service_handler = get_service_handler(
81+
base_url=url,
82+
service_type=service_type,
83+
username=self.cleaned_data.get("username", None),
84+
password=self.cleaned_data.get("password", None),
85+
)
6986
except Exception as e:
7087
logger.error(f"CreateServiceForm cleaning error: {e}")
7188
raise ValidationError(_("Could not connect to the service at %(url)s"), params={"url": url})
@@ -80,6 +97,14 @@ def clean(self):
8097
self.cleaned_data["service_handler"] = service_handler
8198
self.cleaned_data["type"] = service_handler.service_type
8299

100+
def clean_username(self):
101+
# the form return empty string, we want None if is not provided
102+
return self.cleaned_data["username"] or None
103+
104+
def clean_password(self):
105+
# the form return empty string, we want None if is not provided
106+
return self.cleaned_data["password"] or None
107+
83108

84109
class ServiceForm(forms.ModelForm):
85110
title = forms.CharField(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.20 on 2025-03-28 10:05
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("services", "0054_alter_service_type"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="service",
15+
name="password",
16+
field=models.CharField(default=None, max_length=250, null=True, verbose_name="password"),
17+
),
18+
migrations.AddField(
19+
model_name="service",
20+
name="username",
21+
field=models.CharField(default=None, max_length=150, null=True),
22+
),
23+
]

geonode/services/models.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#
1818
#########################################################################
1919
import logging
20+
import base64
2021

2122
from urllib.parse import urlparse, ParseResult
2223

@@ -30,11 +31,16 @@
3031
from geonode.layers.enumerations import GXP_PTYPES
3132
from geonode.people.enumerations import ROLE_VALUES
3233
from geonode.services.serviceprocessors import get_available_service_types
33-
34+
from cryptography.fernet import Fernet
3435
from . import enumerations
3536

3637
service_type_as_tuple = [(k, v["label"]) for k, v in get_available_service_types().items()]
3738

39+
SECRET_KEY = settings.SECRET_KEY # Ensure it's unique per project
40+
ENCRYPTION_KEY = base64.urlsafe_b64encode(SECRET_KEY[:32].encode())
41+
42+
cipher = Fernet(ENCRYPTION_KEY)
43+
3844
logger = logging.getLogger("geonode.services")
3945

4046

@@ -61,6 +67,8 @@ class Service(ResourceBase):
6167
description = models.CharField(max_length=255, null=True, blank=True)
6268
extra_queryparams = models.TextField(null=True, blank=True)
6369
operations = models.JSONField(default=dict, null=True, blank=True)
70+
username = models.CharField(max_length=150, null=True, default=None)
71+
password = models.CharField(_("password"), max_length=250, null=True, default=None)
6472

6573
# Foreign Keys
6674

@@ -70,6 +78,12 @@ class Service(ResourceBase):
7078

7179
# Supported Capabilities
7280

81+
def save(self, notify=False, *args, **kwargs):
82+
if kwargs.get("force_insert", False) and self.needs_authentication:
83+
# if is the first creation, we must encrypt the password
84+
self.password = self.set_password(self.password)
85+
return super().save(notify, *args, **kwargs)
86+
7387
def __str__(self):
7488
return str(self.name)
7589

@@ -79,6 +93,10 @@ def probe(self):
7993
return self.harvester.remote_available
8094
return False
8195

96+
@property
97+
def needs_authentication(self):
98+
return self.password and self.username
99+
82100
def _get_service_url(self):
83101
parsed_url = urlparse(self.base_url)
84102
encoded_get_args = self.extra_queryparams
@@ -109,6 +127,12 @@ def service_type(self):
109127
def get_absolute_url(self):
110128
return "/services/%i" % self.id
111129

130+
def set_password(self, password):
131+
return cipher.encrypt(password.encode()).decode()
132+
133+
def get_password(self):
134+
return cipher.decrypt(self.password.encode()).decode()
135+
112136
class Meta:
113137
# custom permissions,
114138
# change and delete are standard in django-guardian

geonode/services/serviceprocessors/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,17 @@ def get_available_service_types():
5757
return OrderedDict({**default, **parse_services_types()})
5858

5959

60-
def get_service_handler(base_url, service_type=enumerations.AUTO, service_id=None):
60+
def get_service_handler(base_url, service_type=enumerations.AUTO, service_id=None, *args, **kwargs):
6161
"""Return the appropriate remote service handler for the input URL.
6262
If the service type is not explicitly passed in it will be guessed from
6363
"""
6464
handlers = get_available_service_types()
6565

6666
handler = handlers.get(service_type, {}).get("handler")
6767
try:
68-
service = handler(base_url, service_id)
69-
except Exception:
68+
service = handler(base_url, service_id, *args, **kwargs)
69+
except Exception as e:
70+
logger.exception(e)
7071
logger.exception(msg=f"Could not parse service {base_url}")
7172
raise
7273
return service

geonode/services/serviceprocessors/arcgis.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ class ArcMapServiceHandler(base.ServiceHandlerBase):
6565

6666
service_type = enumerations.REST_MAP
6767

68-
def __init__(self, url, geonode_service_id=None):
68+
def __init__(self, url, geonode_service_id=None, *args, **kwargs):
69+
self.args = args
70+
self.kwargs = kwargs
6971
base.ServiceHandlerBase.__init__(self, url, geonode_service_id)
7072
extent, srs = utils.get_esri_extent(self.parsed_service)
7173
try:
@@ -211,9 +213,11 @@ class ArcImageServiceHandler(ArcMapServiceHandler):
211213

212214
service_type = enumerations.REST_IMG
213215

214-
def __init__(self, url, geonode_service_id=None):
216+
def __init__(self, url, geonode_service_id=None, *args, **kwargs):
215217
ArcMapServiceHandler.__init__(self, url, geonode_service_id)
216218
self.url = url
219+
self.args = args
220+
self.kwargs = kwargs
217221
extent, srs = utils.get_esri_extent(self.parsed_service)
218222
try:
219223
_sname = utils.get_esri_service_name(self.url)

geonode/services/serviceprocessors/wms.py

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ class WmsServiceHandler(base.ServiceHandlerBase, base.CascadableServiceHandlerMi
5656

5757
service_type = enumerations.WMS
5858

59-
def __init__(self, url, geonode_service_id=None):
59+
def __init__(self, url, geonode_service_id=None, *args, **kwargs):
6060
base.ServiceHandlerBase.__init__(self, url, geonode_service_id)
61+
self.args = args
62+
self.kwargs = kwargs
6163
self._parsed_service = None
6264
self.indexing_method = INDEXED if self._offers_geonode_projection() else CASCADED
6365
self.name = slugify(self.url)[:255]
@@ -93,7 +95,12 @@ def get_cleaned_url_params(url):
9395
@property
9496
def parsed_service(self):
9597
cleaned_url, service, version, request = WmsServiceHandler.get_cleaned_url_params(self.url)
96-
_url, _parsed_service = WebMapService(cleaned_url.geturl(), version=version)
98+
_url, _parsed_service = WebMapService(
99+
cleaned_url.geturl(),
100+
version=version,
101+
username=self.kwargs.get("username"),
102+
password=self.kwargs.get("password"),
103+
)
97104
return _parsed_service
98105

99106
def probe(self):
@@ -122,43 +129,52 @@ def create_geonode_service(self, owner, parent=None):
122129
:type owner: geonode.people.models.Profile
123130
124131
"""
125-
cleaned_url, service, version, request = WmsServiceHandler.get_cleaned_url_params(self.url)
126-
with transaction.atomic():
127-
instance = models.Service.objects.create(
128-
uuid=str(uuid4()),
129-
base_url=f"{cleaned_url.scheme}://{cleaned_url.netloc}{cleaned_url.path}".encode(
130-
"utf-8", "ignore"
131-
).decode("utf-8"),
132-
extra_queryparams=cleaned_url.query,
133-
type=self.service_type,
134-
method=self.indexing_method,
135-
owner=owner,
136-
metadata_only=True,
137-
version=str(self.parsed_service.identification.version).encode("utf-8", "ignore").decode("utf-8"),
138-
name=self.name,
139-
title=str(self.parsed_service.identification.title).encode("utf-8", "ignore").decode("utf-8")
140-
or self.name,
141-
abstract=str(self.parsed_service.identification.abstract).encode("utf-8", "ignore").decode("utf-8")
142-
or _("Not provided"),
143-
operations=OgcWmsHarvester.get_wms_operations(self.parsed_service.url, version=version),
144-
)
145-
service_harvester = Harvester.objects.create(
146-
name=self.name,
147-
default_owner=owner,
148-
scheduling_enabled=False,
149-
remote_url=instance.service_url,
150-
delete_orphan_resources_automatically=True,
151-
harvester_type=enumerations.HARVESTER_TYPES[self.service_type],
152-
harvester_type_specific_configuration=self.get_harvester_configuration_options(),
153-
)
154-
if service_harvester.update_availability():
155-
service_harvester.initiate_update_harvestable_resources()
156-
else:
157-
logger.exception(GeoNodeException("Could not reach remote endpoint."))
158-
instance.harvester = service_harvester
159-
160-
self.geonode_service_id = instance.id
161-
return instance
132+
service = None
133+
try:
134+
cleaned_url, service, version, request = WmsServiceHandler.get_cleaned_url_params(self.url)
135+
with transaction.atomic():
136+
service = models.Service.objects.create(
137+
uuid=str(uuid4()),
138+
base_url=f"{cleaned_url.scheme}://{cleaned_url.netloc}{cleaned_url.path}".encode(
139+
"utf-8", "ignore"
140+
).decode("utf-8"),
141+
extra_queryparams=cleaned_url.query,
142+
type=self.service_type,
143+
method=self.indexing_method,
144+
owner=owner,
145+
metadata_only=True,
146+
version=str(self.parsed_service.identification.version).encode("utf-8", "ignore").decode("utf-8"),
147+
name=self.name,
148+
title=str(self.parsed_service.identification.title).encode("utf-8", "ignore").decode("utf-8")
149+
or self.name,
150+
abstract=str(self.parsed_service.identification.abstract).encode("utf-8", "ignore").decode("utf-8")
151+
or _("Not provided"),
152+
operations=OgcWmsHarvester.get_wms_operations(self.parsed_service.url, version=version),
153+
username=self.kwargs.get("username", None),
154+
password=self.kwargs.get("password", None),
155+
)
156+
service_harvester = Harvester.objects.create(
157+
name=self.name,
158+
default_owner=owner,
159+
scheduling_enabled=False,
160+
remote_url=service.service_url,
161+
delete_orphan_resources_automatically=True,
162+
harvester_type=enumerations.HARVESTER_TYPES[self.service_type],
163+
harvester_type_specific_configuration=self.get_harvester_configuration_options(),
164+
)
165+
service.harvester = service_harvester
166+
service.save()
167+
if service_harvester.update_availability():
168+
service_harvester.initiate_update_harvestable_resources()
169+
else:
170+
logger.exception(GeoNodeException("Could not reach remote endpoint."))
171+
172+
self.geonode_service_id = service.id
173+
except Exception as e:
174+
logger.exception(e)
175+
if service:
176+
service.delete()
177+
return service
162178

163179
def get_keywords(self):
164180
return self.parsed_service.identification.keywords
@@ -261,8 +277,11 @@ class GeoNodeServiceHandler(WmsServiceHandler):
261277

262278
service_type = enumerations.GN_WMS
263279

264-
def __init__(self, url, geonode_service_id=None):
280+
def __init__(self, url, geonode_service_id=None, *args, **kwargs):
265281
base.ServiceHandlerBase.__init__(self, url, geonode_service_id)
282+
self.args = args
283+
self.kwargs = kwargs
284+
266285
self.indexing_method = INDEXED
267286
self.name = slugify(self.url)[:255]
268287

geonode/services/templates/services/service_detail.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ <h3><strong>{{service.title|default:service.name}}</strong></h3>
1010
<p><strong>{% trans "Abstract" %}:</strong> {{service.abstract}}</p>
1111
<p><strong>{% trans "Keywords" %}:</strong> {{ service.keywords.all|join:", " }}</p>
1212
<p><strong>{% trans "Contact" %}:</strong> <a href="{% url "profile_detail" service.owner.username %}">{{ service.owner }}</a></p>
13-
13+
{% if service.type == 'WMS' and service.needs_authentication %}
14+
<p><strong>SERVICE NOTES:</strong> The service is accessed by Basic auth via the user <strong>{{service.username}}</strong></p>
15+
{% endif %}
1416
{% autoescape off %}
1517
<h3>{% trans "Service Resources" %} <span class="badge">{{ total_resources }}</span></h3>
1618
{% if service.harvester %}

0 commit comments

Comments
 (0)