Skip to content

Commit e79e745

Browse files
author
Alex Grosu
committed
Merge remote-tracking branch 'origin/develop' into feat-rspy759/deploy-prip-stac-browser
2 parents 476eee5 + 9ecad76 commit e79e745

File tree

12 files changed

+178
-159
lines changed

12 files changed

+178
-159
lines changed

services/cadip/rs_server_cadip/api/cadip_search.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ def process_session_search( # type: ignore # pylint: disable=too-many-arguments
561561
except ValueError as exception:
562562
logger.error(exception)
563563
raise HTTPException(
564-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
564+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
565565
detail=str(exception),
566566
) from exception
567567
except Exception as exception: # pylint: disable=broad-exception-caught
@@ -611,7 +611,7 @@ def process_files_search( # pylint: disable=too-many-locals
611611
queryables["SessionId"] = split_multiple_values(session_id)
612612

613613
if limit < 1: # type: ignore
614-
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Pagination cannot be less 0")
614+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Pagination cannot be less 0")
615615
# Init dataretriever / get products / return
616616
try:
617617
products = cadip_retriever.init_cadip_provider(station).search(

services/cadip/rs_server_cadip/cadip_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,12 @@ def cadip_map_mission(platform: str, constellation: str) -> str | None:
198198
)
199199
if satellite and satellite not in satellites:
200200
raise HTTPException(
201-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
201+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
202202
detail="Invalid combination of platform-constellation",
203203
)
204204
except (KeyError, IndexError, StopIteration) as exc:
205205
raise HTTPException(
206-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
206+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
207207
detail="Cannot map platform/constellation",
208208
) from exc
209209
return satellite or satellites

services/catalog/rs_server_catalog/user_catalog.py

Lines changed: 10 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@
4949
manage_landing_page,
5050
)
5151
from rs_server_catalog.user_handler import (
52+
adapt_links,
53+
adapt_object_links,
5254
add_user_prefix,
5355
filter_collections,
5456
get_user,
55-
remove_user_from_collection,
56-
remove_user_from_feature,
5757
reroute_url,
5858
)
5959
from rs_server_catalog.utils import (
@@ -137,28 +137,6 @@ def __init__(self, client: CoreCrudClient):
137137
self.client = client
138138
self.s3_files_to_be_deleted: list[str] = []
139139

140-
def remove_user_from_objects(self, content: dict, user: str, object_name: str) -> dict:
141-
"""Remove the user id from the object.
142-
143-
Args:
144-
content (dict): The response content from the middleware
145-
'call_next' loaded in json format.
146-
user (str): The user id to remove.
147-
object_name (str): Precise the object type in the content.
148-
It can be collections or features.
149-
150-
Returns:
151-
dict: The content with the user id removed.
152-
"""
153-
objects = content[object_name]
154-
nb_objects = len(objects)
155-
for i in range(nb_objects):
156-
if object_name == "collections":
157-
objects[i] = remove_user_from_collection(objects[i], user)
158-
else:
159-
objects[i] = remove_user_from_feature(objects[i], user)
160-
return content
161-
162140
def clear_catalog_bucket(self, content: dict):
163141
"""Used to clear specific files from catalog bucket."""
164142
if not self.s3_handler:
@@ -174,47 +152,6 @@ def clear_catalog_bucket(self, content: dict):
174152
if not int(os.environ.get("RSPY_LOCAL_CATALOG_MODE", 0)): # don't delete files if we are in local mode
175153
self.s3_handler.delete_file_from_s3(bucket_name, file_key)
176154

177-
def adapt_object_links(self, my_object: dict, user: str | None) -> dict:
178-
"""adapt all the links from a collection so the user can use them correctly
179-
180-
Args:
181-
object (dict): The collection
182-
user (str): The user id
183-
184-
Returns:
185-
dict: The collection passed in parameter with adapted links
186-
"""
187-
links = my_object.get("links", [])
188-
for j, link in enumerate(links):
189-
link_parser = urlparse(link["href"])
190-
if "properties" in my_object: # If my_object is an item
191-
new_path = add_user_prefix(link_parser.path, user, my_object["collection"], my_object["id"])
192-
else: # If my_object is a collection
193-
new_path = add_user_prefix(link_parser.path, user, my_object["id"])
194-
links[j]["href"] = link_parser._replace(path=new_path).geturl()
195-
return my_object
196-
197-
def adapt_links(self, content: dict, user: str | None, collection_id: str | None, object_name: str) -> dict:
198-
"""adapt all the links that are outside from the collection section
199-
200-
Args:
201-
content (dict): The response content from the middleware
202-
'call_next' loaded in json format.
203-
user (str): The user id.
204-
205-
Returns:
206-
dict: The content passed in parameter with adapted links
207-
"""
208-
links = content["links"]
209-
for link in links:
210-
link_parser = urlparse(link["href"])
211-
new_path = add_user_prefix(link_parser.path, user, collection_id)
212-
link["href"] = link_parser._replace(path=new_path).geturl()
213-
# Go through each item and apply corrections to the links
214-
for i in range(len(content[object_name])):
215-
content[object_name][i] = self.adapt_object_links(content[object_name][i], user)
216-
return content
217-
218155
async def get_item_from_collection(self, request: Request):
219156
"""Get an item from the collection.
220157
@@ -692,10 +629,9 @@ async def manage_search_response(self, request: Request, response: StreamingResp
692629
body = [chunk async for chunk in response.body_iterator]
693630
dec_content = b"".join(map(lambda x: x if isinstance(x, bytes) else x.encode(), body)).decode() # type: ignore
694631
content = json.loads(dec_content)
695-
content = self.remove_user_from_objects(content, self.request_ids["owner_id"], "features")
696-
content = self.adapt_links(content, None, None, "features")
632+
content = adapt_links(content, "features")
697633
for collection_id in self.request_ids["collection_ids"]:
698-
content = self.adapt_links(content, self.request_ids["owner_id"], collection_id, "features")
634+
content = adapt_links(content, "features", self.request_ids["owner_id"], collection_id)
699635

700636
# Add the stac authentication extension
701637
await self.add_authentication_extension(content)
@@ -1012,21 +948,18 @@ async def manage_get_response_content( # pylint: disable=too-many-locals, too-m
1012948
elif (
1013949
"/collections" in request.scope["path"] and "/items" not in request.scope["path"]
1014950
): # /catalog/collections/owner_id:collection_id
1015-
content = remove_user_from_collection(content, self.request_ids["owner_id"])
1016-
content = self.adapt_object_links(content, self.request_ids["owner_id"])
951+
content = adapt_object_links(content, self.request_ids["owner_id"])
1017952
elif (
1018953
"/items" in request.scope["path"] and not self.request_ids["item_id"]
1019954
): # /catalog/owner_id/collections/collection_id/items
1020-
content = self.remove_user_from_objects(content, self.request_ids["owner_id"], "features")
1021-
content = self.adapt_links(
955+
content = adapt_links(
1022956
content,
957+
"features",
1023958
self.request_ids["owner_id"],
1024959
self.request_ids["collection_ids"][0],
1025-
"features",
1026960
)
1027961
elif self.request_ids["item_id"]: # /catalog/owner_id/collections/collection_id/items/item_id
1028-
content = remove_user_from_feature(content, self.request_ids["owner_id"])
1029-
content = self.adapt_object_links(content, self.request_ids["owner_id"])
962+
content = adapt_object_links(content, self.request_ids["owner_id"])
1030963
else:
1031964
logger.debug(f"No link adaptation performed for {request.scope}")
1032965

@@ -1102,15 +1035,13 @@ async def manage_put_post_response(self, request: Request, response: StreamingRe
11021035
response_content = json.loads(b"".join(body).decode()) # type: ignore
11031036
# Don't display geometry and bbox for default case since it was added just for compliance.
11041037
if request.scope["path"] == CATALOG_COLLECTIONS:
1105-
response_content = remove_user_from_collection(response_content, user)
1106-
response_content = self.adapt_object_links(response_content, user)
1038+
response_content = adapt_object_links(response_content, self.request_ids["owner_id"])
11071039
elif (
11081040
request.scope["path"]
11091041
== CATALOG_COLLECTIONS
11101042
+ f"/{user}_{self.request_ids['collection_ids'][0]}/items/{self.request_ids['item_id']}"
11111043
):
1112-
response_content = remove_user_from_feature(response_content, user)
1113-
response_content = self.adapt_object_links(response_content, user)
1044+
response_content = adapt_object_links(response_content, self.request_ids["owner_id"])
11141045
if response_content.get("geometry") == DEFAULT_GEOM:
11151046
response_content["geometry"] = None
11161047
if response_content.get("bbox") == DEFAULT_BBOX:

services/catalog/rs_server_catalog/user_handler.py

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import os
1919
import re
2020
from typing import Any
21+
from urllib.parse import urlparse
2122

2223
from rs_server_common.authentication.oauth2 import AUTH_PREFIX
2324
from rs_server_common.utils.logging import Logging
@@ -227,40 +228,62 @@ def add_user_prefix( # pylint: disable=too-many-return-statements
227228
return new_path
228229

229230

230-
def remove_user_from_feature(feature: dict, user: str) -> dict:
231-
"""Remove the user ID from the collection name in the feature.
231+
def remove_owner_from_collection_name_in_feature(feature: dict, current_user: str = "") -> tuple[dict, str]:
232+
"""Remove the owner name from the collection name in the feature.
233+
The owner name used is the "owner" field of the properties if there is any,
234+
or by default the currently connected user.
235+
Returns the updated feature and the owner name actually removed.
236+
If nothing was removed, returns the original feature and an empty owner name.
232237
233238
Args:
234239
feature (dict): a geojson that contains georeferenced
235-
data and metadata like the collection name.
236-
user (str): The user ID.
240+
data and metadata like the collection name.
241+
current_user (str): current user connected (optional)
237242
238243
Returns:
239-
dict: the feature with a new collection name without the user ID.
244+
dict: the feature with a new collection name without the owner name.
245+
str: the name removed, if any.
240246
"""
241-
if user in feature["collection"]:
247+
if "owner" in feature["properties"]:
248+
user = feature["properties"]["owner"]
249+
else:
250+
user = current_user
251+
252+
if feature["collection"].startswith(f"{user}_"):
242253
feature["collection"] = feature["collection"].removeprefix(f"{user}_")
243-
return feature
254+
return feature, user
255+
return feature, ""
244256

245257

246-
def remove_user_from_collection(collection: dict, user: str) -> dict:
247-
"""Remove the user ID from the id section in the collection.
258+
def remove_owner_from_collection_name_in_collection(collection: dict, current_user: str = "") -> tuple[dict, str]:
259+
"""Remove the owner name from the given collection name.
260+
The owner name used is the "owner" field of the collection if there is any,
261+
or by default the currently connected user.
262+
Returns the updated collection and the owner name actually removed.
263+
If nothing was removed, returns the original collection and an empty owner name.
248264
249265
Args:
250266
collection (dict): A dictionary that contains metadata
251-
about the collection content like the id of the collection.
252-
user (str): The user ID.
267+
about the collection content like the id of the collection.
268+
current_user (str): current user connected (optional)
253269
254270
Returns:
255-
dict: The collection without the user ID in the id section.
271+
dict: The collection without the owner name in the id section.
272+
str: the name removed, if any.
256273
"""
257-
if user and (user in collection.get("id", "")):
274+
if "owner" in collection:
275+
user = collection["owner"]
276+
else:
277+
user = current_user
278+
279+
if collection["id"].startswith(f"{user}_"):
258280
collection["id"] = collection["id"].removeprefix(f"{user}_")
259-
return collection
281+
return collection, user
282+
return collection, ""
260283

261284

262285
def filter_collections(collections: list[dict], prefix: str) -> list[dict]:
263-
"""filter the collections according to the prefix.
286+
"""Filter the collections according to the prefix.
264287
265288
Args:
266289
collections (list[dict]): The list of collections available.
@@ -270,3 +293,64 @@ def filter_collections(collections: list[dict], prefix: str) -> list[dict]:
270293
list[dict]: The list of collections corresponding to the prefix
271294
"""
272295
return [collection for collection in collections if collection["id"].startswith(prefix)]
296+
297+
298+
def adapt_object_links(object_content: dict, current_user: str = "") -> dict:
299+
"""Adapt all the links from a collection using the user and collection name they already contain,
300+
so the user can access them correctly
301+
302+
Args:
303+
object (dict): The collection
304+
305+
Returns:
306+
dict: The collection passed in parameter with adapted links
307+
"""
308+
user = collection_id = feature_id = ""
309+
310+
# Case when object is an item
311+
if "properties" in object_content and "collection" in object_content:
312+
object_content, user = remove_owner_from_collection_name_in_feature(object_content, current_user)
313+
collection_id = object_content["collection"]
314+
feature_id = object_content["id"]
315+
316+
# Case when object is a collection
317+
elif "id" in object_content:
318+
object_content, user = remove_owner_from_collection_name_in_collection(object_content, current_user)
319+
collection_id = object_content["id"]
320+
321+
# Update links with user, collection and feature values retrieved from previous steps
322+
links = object_content.get("links", [])
323+
for j, link in enumerate(links):
324+
link_parser = urlparse(link["href"])
325+
new_path = add_user_prefix(link_parser.path, user, collection_id, feature_id)
326+
links[j]["href"] = link_parser._replace(path=new_path).geturl()
327+
328+
return object_content
329+
330+
331+
def adapt_links(content: dict, object_name: str, current_user: str = "", current_collection_id: str = "") -> dict:
332+
"""Adapt all the links that are outside from the collection section with the given user and collection name,
333+
then the ones inside with the user and collection names they already contain.
334+
335+
Args:
336+
content (dict): The response content from the middleware
337+
'call_next' loaded in json format.
338+
current_user (str): The user id that is currently connected.
339+
current_collection (str): The current collection name.
340+
object_name (str): Type of object we want to also update.
341+
342+
Returns:
343+
dict: The content passed in parameter with adapted links
344+
"""
345+
# Adapt links outside of objects with current user/collection situation
346+
links = content["links"]
347+
for link in links:
348+
link_parser = urlparse(link["href"])
349+
new_path = add_user_prefix(link_parser.path, current_user, current_collection_id)
350+
link["href"] = link_parser._replace(path=new_path).geturl()
351+
352+
# Go through each object and apply corrections to the links using the object's info
353+
for i in range(len(content[object_name])):
354+
content[object_name][i] = adapt_object_links(content[object_name][i], current_user)
355+
356+
return content

services/catalog/tests/test_authentication_catalog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
HTTP_401_UNAUTHORIZED,
4343
HTTP_403_FORBIDDEN,
4444
HTTP_404_NOT_FOUND,
45-
HTTP_422_UNPROCESSABLE_ENTITY,
45+
HTTP_422_UNPROCESSABLE_CONTENT,
4646
HTTP_500_INTERNAL_SERVER_ERROR,
4747
)
4848

@@ -1572,7 +1572,7 @@ async def test_error_when_not_authenticated(mocker, client, httpx_mock: HTTPXMoc
15721572
assert response.status_code not in (
15731573
HTTP_401_UNAUTHORIZED,
15741574
HTTP_403_FORBIDDEN,
1575-
HTTP_422_UNPROCESSABLE_ENTITY, # with 422, the authentication is not called and not tested
1575+
HTTP_422_UNPROCESSABLE_CONTENT, # with 422, the authentication is not called and not tested
15761576
)
15771577

15781578
# With a wrong apikey, we should have a 403 error

services/catalog/tests/test_endpoints/test_catalog_publish_collection_endpoint.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,18 @@ def test_update_more_than_a_collection(self, client):
248248
assert response_content["code"] == "BadRequest"
249249
assert response_content["description"] == "Cannot create or update more than one collection !"
250250

251-
@patch("rs_server_catalog.user_catalog.remove_user_from_collection")
252-
def test_failure_to_create_collection_generic_exception(self, mock_remove_user_from_collection, client):
251+
@patch("rs_server_catalog.user_handler.remove_owner_from_collection_name_in_collection")
252+
def test_failure_to_create_collection_generic_exception(
253+
self,
254+
mock_remove_owner_from_collection_name_in_collection,
255+
client,
256+
):
253257
"""
254258
Test endpoint POST /catalog/collections with an generic exception raised.
255259
Endpoint: POST /catalog/collections
256260
"""
257261
mock_exc = "ValueError"
258-
mock_remove_user_from_collection.side_effect = ValueError(mock_exc)
262+
mock_remove_owner_from_collection_name_in_collection.side_effect = ValueError(mock_exc)
259263

260264
minimal_collection = {
261265
"id": "test_collection_01",
@@ -277,14 +281,18 @@ def test_failure_to_create_collection_generic_exception(self, mock_remove_user_f
277281
assert response_content["code"] == "BadRequest"
278282
assert response_content["description"] == f"Bad request: {mock_exc}"
279283

280-
@patch("rs_server_catalog.user_catalog.remove_user_from_collection")
281-
def test_failure_to_create_collection_runtime_error(self, mock_remove_user_from_collection, client):
284+
@patch("rs_server_catalog.user_handler.remove_owner_from_collection_name_in_collection")
285+
def test_failure_to_create_collection_runtime_error(
286+
self,
287+
mock_remove_owner_from_collection_name_in_collection,
288+
client,
289+
):
282290
"""
283291
Test endpoint POST /catalog/collections with an RuntimeError exception raised.
284292
Endpoint: POST /catalog/collections
285293
"""
286294
mock_exc = "RuntimeError"
287-
mock_remove_user_from_collection.side_effect = RuntimeError(mock_exc)
295+
mock_remove_owner_from_collection_name_in_collection.side_effect = RuntimeError(mock_exc)
288296

289297
minimal_collection = {
290298
"id": "test_collection_02",

0 commit comments

Comments
 (0)