Skip to content

Commit 36f414d

Browse files
etjafabiani
andauthored
[Fixes GeoNode#10537] Improve rules creation using GeoFence batch - more imp… (GeoNode#10585)
* [Code Formatting] Revert formatting on conflicting files * [Fixes GeoNode#10537] Improve rules creation using GeoFence batch - more improvements * [GeoNode#10574] Align code formatting with black Co-authored-by: afabiani <[email protected]>
1 parent e440111 commit 36f414d

File tree

10 files changed

+485
-416
lines changed

10 files changed

+485
-416
lines changed

geonode/api/tests.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from guardian.shortcuts import get_anonymous_user
3131

3232
from geonode import geoserver
33+
from geonode.geoserver.manager import GeoServerResourceManager
3334
from geonode.maps.models import Map
3435
from geonode.layers.models import Dataset
3536
from geonode.documents.models import Document
@@ -142,26 +143,31 @@ def test_dataset_get_detail_unauth_dataset_not_public(self):
142143
"""
143144
list_url = reverse("api_dispatch_list", kwargs={"api_name": "api", "resource_name": "datasets"})
144145

146+
resp = self.client.get(list_url)
147+
self.assertEqual(len(self.deserialize(resp)["objects"]), 8)
148+
145149
layer = Dataset.objects.first()
146150
layer.set_permissions(self.perm_spec)
147151
layer.clear_dirty_state()
148-
self.assertHttpNotFound(self.api_client.get(f"{list_url + str(layer.id)}/"))
149152

150-
self.api_client.client.login(username=self.user, password=self.passwd)
151-
resp = self.api_client.get(f"{list_url + str(layer.id)}/")
152-
self.assertValidJSONResponse(resp)
153+
resp = self.client.get(list_url)
154+
self.assertEqual(len(self.deserialize(resp)["objects"]), 7)
155+
self.assertHttpNotFound(self.client.get(f"{list_url + str(layer.id)}/"))
156+
157+
self.client.login(username=self.user, password=self.passwd)
158+
self.assertValidJSONResponse(self.client.get(f"{list_url + str(layer.id)}/"))
153159

154160
# with delayed security
155-
with self.settings(DELAYED_SECURITY_SIGNALS=True):
161+
with self.settings(DELAYED_SECURITY_SIGNALS=True, GEOFENCE_SECURITY_ENABLED=True):
156162
if check_ogc_backend(geoserver.BACKEND_PACKAGE):
157-
from geonode.geoserver.security import sync_geofence_with_guardian
158163

159-
sync_geofence_with_guardian(layer, self.perm_spec)
164+
gm = GeoServerResourceManager()
165+
gm.set_permissions(layer.uuid, instance=layer, permissions=self.perm_spec)
160166
self.assertTrue(layer.dirty_state)
161167

162168
self.client.login(username=self.user, password=self.passwd)
163169
resp = self.client.get(list_url)
164-
self.assertEqual(len(self.deserialize(resp)["objects"]), 7)
170+
self.assertEqual(len(self.deserialize(resp)["objects"]), 7) # admin can't see resources in dirty_state
165171

166172
self.client.logout()
167173
resp = self.client.get(list_url)
@@ -171,8 +177,7 @@ def test_dataset_get_detail_unauth_dataset_not_public(self):
171177

172178
get_user_model().objects.create(
173179
username="imnew",
174-
password="pbkdf2_sha256$12000$UE4gAxckVj4Z$N\
175-
6NbOXIQWWblfInIoq/Ta34FdRiPhawCIZ+sOO3YQs=",
180+
password="pbkdf2_sha256$12000$UE4gAxckVj4Z$N6NbOXIQWWblfInIoq/Ta34FdRiPhawCIZ+sOO3YQs=",
176181
)
177182
self.client.login(username="imnew", password="thepwd")
178183
resp = self.client.get(list_url)

geonode/geoserver/geofence.py

Lines changed: 142 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,24 @@
1717
#
1818
#########################################################################
1919

20+
import itertools
2021
import logging
21-
import urllib
2222
import requests
23-
24-
from django.conf import settings
2523
from requests.auth import HTTPBasicAuth
24+
import traceback
25+
import urllib
2626

2727
logger = logging.getLogger(__name__)
2828

29-
ogc_server_settings = settings.OGC_SERVER["default"]
30-
3129

3230
class GeofenceException(Exception):
3331
pass
3432

3533

3634
class Rule:
3735
"""_summary_
38-
JSON representation of a GeoFence Rule
36+
A GeoFence Rule.
37+
Provides the object to be rendered as JSON.
3938
4039
e.g.:
4140
{"Rule":
@@ -56,14 +55,15 @@ class Rule:
5655
ALLOW = "ALLOW"
5756
DENY = "DENY"
5857
LIMIT = "LIMIT"
58+
5959
CM_MIXED = "MIXED"
6060

6161
def __init__(
6262
self,
63-
priority,
64-
workspace,
65-
layer,
6663
access: (str, bool),
64+
priority=None,
65+
workspace=None,
66+
layer=None,
6767
user=None,
6868
group=None,
6969
service=None,
@@ -105,14 +105,18 @@ def __init__(
105105
if limits:
106106
self.fields["limits"] = limits
107107

108-
def get_object(self):
108+
def set_priority(self, pri: int):
109+
self.fields["priority"] = pri
110+
111+
def get_object(self) -> dict:
109112
logger.debug(f"Creating Rule object: {self.fields}")
110113
return {"Rule": self.fields}
111114

112115

113116
class Batch:
114117
"""_summary_
115-
Returns a list of Operations that GeoFence can execute in a batch
118+
A GeoFence Batch.
119+
It's a list of operations that will be executed transactionally inside GeoFence.
116120
117121
e.g.:
118122
{
@@ -203,38 +207,64 @@ def add_insert_rule(self, rule: Rule):
203207
operation.update(rule.get_object())
204208
self.operations.append(operation)
205209

206-
def get_batch_length(self):
210+
def length(self) -> int:
207211
return len(self.operations)
208212

209-
def get_object(self):
210-
logger.debug(f"Creating Batch object {self.log_name} with {len(self.operations)} operations")
213+
def get_object(self) -> dict:
211214
return {"Batch": {"operations": self.operations}}
212215

213216

214-
class GeofenceClient:
217+
class AutoPriorityBatch(Batch):
215218
"""_summary_
216-
Instance of a simple GeoFence REST client allowing to interact with the GeoServer APIs.
217-
Exposes few utility methods to insert or purge the rules and run batches of operations.
219+
A Batch that handles the priority of the inserted rules.
220+
The first rule will have the declared `start_rule_pri`, next Rules will have the priority incremented.
221+
"""
218222

219-
Returns:
220-
_type_: Rule
223+
def __init__(self, start_rule_pri: int, log_name=None) -> None:
224+
super().__init__(log_name)
225+
self.pri = itertools.count(start_rule_pri)
226+
227+
def add_insert_rule(self, rule: Rule):
228+
rule.set_priority(self.pri.__next__())
229+
super().add_insert_rule(rule)
230+
231+
232+
class GeoFenceClient:
233+
"""_summary_
234+
A GeoFence REST client allowing to interact with the embedded GeoFence API (which is slightly incompatible
235+
with the original standalone GeoFence API.)
236+
The class methods map on GeoFence operations.
237+
Functionalities needing more than one call are implemented within GeoFenceUtils.
221238
"""
222239

223240
def __init__(self, baseurl: str, username: str, pw: str) -> None:
241+
if not baseurl.endswith("/"):
242+
baseurl += "/"
243+
224244
self.baseurl = baseurl
225245
self.username = username
226246
self.pw = pw
247+
self.timeout = 60
248+
249+
def set_timeout(self, timeout: int):
250+
self.timeout = timeout
227251

228252
def invalidate_cache(self):
229-
r = requests.put(
230-
f'{self.baseurl.rstrip("/")}/geofence/ruleCache/invalidate', auth=HTTPBasicAuth(self.username, self.pw)
231-
)
253+
r = requests.put(f"{self.baseurl}ruleCache/invalidate", auth=HTTPBasicAuth(self.username, self.pw))
232254

233255
if r.status_code != 200:
234256
logger.debug("Could not invalidate cache")
235257
raise GeofenceException("Could not invalidate cache")
236258

237-
def get_rules(self, page=None, entries=None, workspace=None, workspace_any=None, layer=None, layer_any=None):
259+
def get_rules(
260+
self,
261+
page: int = None,
262+
entries: int = None,
263+
workspace: str = None,
264+
workspace_any: bool = None,
265+
layer: str = None,
266+
layer_any: bool = None,
267+
):
238268
if (page is None and entries is not None) or (page is not None and entries is None):
239269
raise GeofenceException(f"Bad page/entries combination {page}/{entries}")
240270

@@ -257,13 +287,13 @@ def get_rules(self, page=None, entries=None, workspace=None, workspace_any=None,
257287
if value is not None:
258288
params[param] = value
259289

260-
url = f'{self.baseurl.rstrip("/")}/geofence/rules.json?{urllib.parse.urlencode(params)}'
290+
url = f"{self.baseurl}rules.json?{urllib.parse.urlencode(params)}"
261291

262292
r = requests.get(
263293
url,
264294
headers={"Content-type": "application/json"},
265295
auth=HTTPBasicAuth(self.username, self.pw),
266-
timeout=ogc_server_settings.get("TIMEOUT", 10),
296+
timeout=self.timeout,
267297
verify=False,
268298
)
269299

@@ -284,10 +314,10 @@ def get_rules_count(self):
284314
http://<host>:<port>/geoserver/rest/geofence/rules/count.json
285315
"""
286316
r = requests.get(
287-
f'{self.baseurl.rstrip("/")}/geofence/rules/count.json',
317+
f"{self.baseurl}rules/count.json",
288318
headers={"Content-type": "application/json"},
289319
auth=HTTPBasicAuth(self.username, self.pw),
290-
timeout=ogc_server_settings.get("TIMEOUT", 10),
320+
timeout=self.timeout,
291321
verify=False,
292322
)
293323

@@ -310,73 +340,127 @@ def insert_rule(self, rule: Rule):
310340
http://<host>:<port>/geoserver/rest/geofence/rules
311341
"""
312342
r = requests.post(
313-
f'{self.baseurl.rstrip("/")}/geofence/rules',
314-
# headers={'Content-type': 'application/json'},
343+
f"{self.baseurl}rules",
315344
json=rule.get_object(),
316345
auth=HTTPBasicAuth(self.username, self.pw),
317-
timeout=ogc_server_settings.get("TIMEOUT", 60),
346+
timeout=self.timeout,
318347
verify=False,
319348
)
320349

321350
if r.status_code not in (200, 201):
322-
logger.debug(f"Could not insert rule: [{r.status_code}] - {r.content}")
323-
raise GeofenceException(f"Could not insert rule: [{r.status_code}]")
351+
logger.debug(f"Could not insert rule: [{r.status_code}] - {r.text}")
352+
raise GeofenceException(f"Could not insert rule: [{r.status_code}] - {r.text}")
324353

325354
except Exception as e:
326355
logger.debug("Error while inserting rule", exc_info=e)
327356
raise GeofenceException(f"Error while inserting rule: {e}")
328357

329-
def run_batch(self, batch: Batch):
330-
if batch.get_batch_length() == 0:
358+
def run_batch(self, batch: Batch, timeout: int = None) -> bool:
359+
if batch.length() == 0:
331360
logger.debug(f"Skipping batch execution {batch.log_name}")
332-
return
361+
return False
333362

363+
logger.debug(f"Running batch {batch.log_name} with {batch.length()} operations")
334364
try:
335365
"""
336366
curl -X GET -u admin:geoserver \
337367
http://<host>:<port>/geoserver/rest/geofence/rules/count.json
338368
"""
339369
r = requests.post(
340-
f'{self.baseurl.rstrip("/")}/geofence/batch/exec',
370+
f"{self.baseurl}batch/exec",
341371
json=batch.get_object(),
342372
auth=HTTPBasicAuth(self.username, self.pw),
343-
timeout=ogc_server_settings.get("TIMEOUT", 60),
373+
timeout=timeout or self.timeout,
344374
verify=False,
345375
)
346376

347377
if r.status_code != 200:
348-
logger.debug(f"Error while running batch {batch.log_name}: [{r.status_code}] - {r.content}")
378+
logger.debug(
379+
f"Error while running batch {batch.log_name}: [{r.status_code}] - {r.content}"
380+
f"\n {batch.get_object()}"
381+
)
349382
raise GeofenceException(f"Error while running batch {batch.log_name}: [{r.status_code}]")
350383

351-
return
384+
return True
352385

353386
except Exception as e:
354-
logger.debug(f"Error while requesting batch execution {batch.log_name}", exc_info=e)
387+
logger.info(f"Error while requesting batch exec {batch.log_name}")
388+
logger.debug(f"Error while requesting batch exec {batch.log_name} --> {batch.get_object()}", exc_info=e)
355389
raise GeofenceException(f"Error while requesting batch execution {batch.log_name}: {e}")
356390

357-
def purge_all_rules(self):
391+
392+
class GeoFenceUtils:
393+
def __init__(self, client: GeoFenceClient):
394+
self.geofence = client
395+
396+
def delete_all_rules(self):
358397
"""purge all existing GeoFence Cache Rules"""
359-
rules_objs = self.get_rules()
398+
rules_objs = self.geofence.get_rules()
360399
rules = rules_objs["rules"]
361400

362401
batch = Batch("Purge All")
363402
for rule in rules:
364403
batch.add_delete_rule(rule["id"])
365404

366405
logger.debug(f"Going to remove all {len(rules)} rules in geofence")
367-
self.run_batch(batch)
368-
369-
def purge_layer_rules(self, layer_name: str, workspace: str = None):
370-
"""purge existing GeoFence Cache Rules related to a specific Layer"""
371-
gs_rules = self.get_rules(workspace=workspace, workspace_any=False, layer=layer_name, layer_any=False)
372-
373-
batch = Batch(f"Purge {workspace}:{layer_name}")
374-
375-
if gs_rules and gs_rules["rules"]:
376-
logger.debug(f"Going to remove {len(gs_rules['rules'])} rules for layer '{layer_name}'")
377-
for r in gs_rules["rules"]:
378-
if r["layer"] and r["layer"] == layer_name:
379-
batch.add_delete_rule(r["id"])
380-
else:
381-
logger.debug(f"Bad rule retrieved for dataset '{layer_name}': {r}")
382-
self.run_batch(batch)
406+
self.geofence.run_batch(batch)
407+
408+
def collect_delete_layer_rules(self, workspace_name: str, layer_name: str, batch: Batch = None) -> Batch:
409+
"""Collect delete operations in a Batch for all rules related to a layer"""
410+
411+
try:
412+
# Scan GeoFence Rules associated to the Dataset
413+
gs_rules = self.geofence.get_rules(
414+
workspace=workspace_name, workspace_any=False, layer=layer_name, layer_any=False
415+
)
416+
417+
if not batch:
418+
batch = Batch(f"Delete {workspace_name}:{layer_name}")
419+
420+
cnt = 0
421+
if gs_rules and gs_rules["rules"]:
422+
logger.debug(
423+
f"Going to collect {len(gs_rules['rules'])} rules for layer '{workspace_name}:{layer_name}'"
424+
)
425+
for r in gs_rules["rules"]:
426+
if r["layer"] and r["layer"] == layer_name:
427+
batch.add_delete_rule(r["id"])
428+
cnt += 1
429+
else:
430+
logger.warning(f"Bad rule retrieved for dataset '{workspace_name or ''}:{layer_name}': {r}")
431+
432+
logger.debug(f"Adding {cnt} rule deletion operations for '{workspace_name or ''}:{layer_name}")
433+
return batch
434+
435+
except Exception as e:
436+
logger.error(f"Error collecting rules for {workspace_name}:{layer_name}", exc_info=e)
437+
tb = traceback.format_exc()
438+
logger.debug(tb)
439+
440+
def delete_layer_rules(self, workspace_name: str, layer_name: str) -> bool:
441+
"""Delete all Rules related to a specific Layer"""
442+
try:
443+
batch = self.collect_delete_layer_rules(workspace_name, layer_name)
444+
logger.debug(f"Going to remove {batch.length()} rules for layer {workspace_name}:{layer_name}")
445+
return self.geofence.run_batch(batch)
446+
447+
except Exception as e:
448+
logger.error(f"Error removing rules for {workspace_name}:{layer_name}", exc_info=e)
449+
tb = traceback.format_exc()
450+
logger.debug(tb)
451+
return False
452+
453+
def get_first_available_priority(self):
454+
"""Get the highest Rules priority"""
455+
try:
456+
rules_count = self.geofence.get_rules_count()
457+
rules_objs = self.geofence.get_rules(page=rules_count - 1, entries=1)
458+
if len(rules_objs["rules"]) > 0:
459+
highest_priority = rules_objs["rules"][0]["priority"]
460+
else:
461+
highest_priority = 0
462+
return int(highest_priority) + 1
463+
except Exception:
464+
tb = traceback.format_exc()
465+
logger.debug(tb)
466+
return -1

0 commit comments

Comments
 (0)