Repository: CS-SI/eodag
Pull request #986 (2024-01-12 14:46:39)

923 new provider for creodias s3
- provider creodias s3 added
- new plugins for search (CreodiasS3Search) and download (CreodiasS3Download) created 


github-actions[bot]: ## Test Results
  2 files   -     2    2 suites   - 2   1m 31s :stopwatch: - 2m 39s
448 tests ±    0  443 :white_check_mark:  -     2   3 :zzz: ± 0  2 :x: +2 
676 runs   - 1 116  638 :white_check_mark:  - 1 076  36 :zzz:  - 42  2 :x: +2 

For more details on these failures, see [this check](https://github.com/CS-SI/eodag/runs/20429162043).

Results for commit 85a8b950. ± Comparison against base commit 0f094d51.

[test-results]:data:application/gzip;base64,H4sIAB9SoWUC/02MRw7DIBQFr2KxzgLTyWUiqoTiEmFYWbl7vh1cljPvaVYU0xAW9OzIo0NLTeUEX7MpaZ4AdQ8MS9k2xtRBr6U6tyt6qXf6gLpENGloyb8IOc8ZDAaT67Q1hRQNjqSg6jSteF5uxZ3vQTePYyoASHGjrOZYe8JIJJ6TYKnXQcmeRo2xtMI7i3v0/QEk230jBQEAAA==

github-actions[bot]: <strong>Code Coverage (Ubuntu)</strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `81%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against 408fc2b12c4e0155ff2ff7dda2178a29718803e6 </p>
github-actions[bot]: <strong>Code Coverage (Windows)</strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `76%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against 408fc2b12c4e0155ff2ff7dda2178a29718803e6 </p>

Patch:
diff --git a/docs/getting_started_guide/providers.rst b/docs/getting_started_guide/providers.rst
index ff07d3e4..377917eb 100644
--- a/docs/getting_started_guide/providers.rst
+++ b/docs/getting_started_guide/providers.rst
@@ -13,6 +13,7 @@ Products from the following providers are made avaiable through ``eodag``:
 * `peps <https://peps.cnes.fr/rocket/#/home>`_: French National Space Agency (CNES) catalog for Sentinel products
 * `aws_eos <https://eos.com/>`_: EOS search for Amazon public datasets
 * `creodias <https://creodias.eu/>`_: CloudFerro DIAS
+* `creodias_s3 <https://creodias.eu/>`_: CloudFerro DIAS data through S3 protocol
 * `onda <https://www.onda-dias.eu/cms/>`_: Serco DIAS
 * `astraea_eod <https://eod-catalog-svc-prod.astraea.earth/api.html>`_: Astraea Earth OnDemand STAC API
 * `usgs_satapi_aws <https://landsatlook.usgs.gov/sat-api/>`_: USGS Landsatlook SAT API
diff --git a/docs/getting_started_guide/register.rst b/docs/getting_started_guide/register.rst
index d4397c39..2b13757e 100644
--- a/docs/getting_started_guide/register.rst
+++ b/docs/getting_started_guide/register.rst
@@ -22,6 +22,9 @@ to each provider supported by ``eodag``:
   `Authenticate using an OTP <https://eodag.readthedocs.io/en/latest/getting_started_guide/configure.html#authenticate-using-an-otp-one-time-password-two-factor-authentication>`__
   to see how to proceed.
 
+* ``creodias_s3``: Create an account on `creodias <https://creodias.eu/>`__, then go to `keymanager <https://eodata-keymanager.creodias.eu/>`__ and
+  click `Add credential` to generate the s3 access key and secret key. Add those credentials to the user configuration file (variables `aws_access_key_id` and `aws_secret_access_key`).
+
 * ``onda``: create an account `here: <https://www.onda-dias.eu/cms/>`__
 
 * ``ecmwf``: create an account `here <https://apps.ecmwf.int/registration/>`__.
diff --git a/docs/index.rst b/docs/index.rst
index 03ffebfa..51e24aa1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -30,7 +30,8 @@ types (Sentinel 1, Sentinel 2, Sentinel 3, Landsat, etc.) that can be searched a
       `usgs_satapi_aws <https://landsatlook.usgs.gov/sat-api/>`_, `earth_search <https://www.element84.com/earth-search/>`_,
       `earth_search_gcs <https://cloud.google.com/storage/docs/public-datasets>`_,
       `usgs <https://earthexplorer.usgs.gov/>`_, `theia <https://theia.cnes.fr/atdistrib/rocket/>`_,
-      `peps <https://peps.cnes.fr/rocket/#/home>`_, `aws_eos <https://eos.com/>`_, `creodias <https://creodias.eu/>`_,
+      `peps <https://peps.cnes.fr/rocket/#/home>`_, `aws_eos <https://eos.com/>`_,
+      `creodias <https://creodias.eu/>`_, `creodias_s3 <https://creodias.eu/>`_,
       `onda <https://www.onda-dias.eu/cms/>`_, `ecmwf <https://www.ecmwf.int/>`_,
       `cop_ads <https://ads.atmosphere.copernicus.eu>`_, `cop_cds <https://cds.climate.copernicus.eu>`_,
       `sara <https://copernicus.nci.org.au>`_,
diff --git a/eodag/config.py b/eodag/config.py
index bba53bad..9f437fd4 100644
--- a/eodag/config.py
+++ b/eodag/config.py
@@ -267,6 +267,7 @@ class PluginConfig(yaml.YAMLObject):
     collection: bool  # PostJsonSearch for aws_eos
     max_connections: int  # StaticStacSearch
     timeout: float  # StaticStacSearch
+    s3_bucket: str  # CreodiasS3Search
 
     # download -------------------------------------------------------------------------
     base_uri: str
@@ -294,7 +295,7 @@ class PluginConfig(yaml.YAMLObject):
     authorization_uri: str  # OIDCAuthorizationCodeFlowAuth
     login_form_xpath: str  # OIDCAuthorizationCodeFlowAuth
     user_consent_form_xpath: str  # OIDCAuthorizationCodeFlowAuth
-    user_consent_form_data: str  # OIDCAuthorizationCodeFlowAuth
+    user_consent_form_data: Dict[str, str]  # OIDCAuthorizationCodeFlowAuth
     token_exchange_post_data_method: str  # OIDCAuthorizationCodeFlowAuth
     token_uri: str  # OIDCAuthorizationCodeFlowAuth
     token_key: str  # OIDCAuthorizationCodeFlowAuth
diff --git a/eodag/plugins/authentication/__init__.py b/eodag/plugins/authentication/__init__.py
index 29786342..b84235b3 100644
--- a/eodag/plugins/authentication/__init__.py
+++ b/eodag/plugins/authentication/__init__.py
@@ -18,8 +18,6 @@
 """EODAG authentication package"""
 from __future__ import annotations
 
-from typing import Dict, Union
-
 from requests.auth import AuthBase
 
 from eodag.plugins.authentication.base import Authentication
@@ -28,6 +26,6 @@ from eodag.plugins.authentication.base import Authentication
 class DummyAuth(Authentication):
     """Dummy authentication"""
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> AuthBase:
         """authenticate"""
         return AuthBase()
diff --git a/eodag/plugins/authentication/aws_auth.py b/eodag/plugins/authentication/aws_auth.py
index 75688293..d1b9cf48 100644
--- a/eodag/plugins/authentication/aws_auth.py
+++ b/eodag/plugins/authentication/aws_auth.py
@@ -17,12 +17,12 @@
 # limitations under the License.
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Dict, Union
+from typing import TYPE_CHECKING, Dict
 
 from eodag.plugins.authentication.base import Authentication
 
 if TYPE_CHECKING:
-    from requests.auth import AuthBase
+    from botocore.client import S3
 
     from eodag.config import PluginConfig
 
@@ -39,13 +39,15 @@ class AwsAuth(Authentication):
       will be skipped if AWS credentials are filled in eodag conf
     """
 
+    s3_client: S3
+
     def __init__(self, provider: str, config: PluginConfig) -> None:
         super(AwsAuth, self).__init__(provider, config)
         self.aws_access_key_id = None
         self.aws_secret_access_key = None
         self.profile_name = None
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> Dict[str, str]:
         """Authenticate
 
         :returns: dict containing AWS/boto3 non-empty credentials
diff --git a/eodag/plugins/authentication/generic.py b/eodag/plugins/authentication/generic.py
index a4826206..5d0271e2 100644
--- a/eodag/plugins/authentication/generic.py
+++ b/eodag/plugins/authentication/generic.py
@@ -17,7 +17,7 @@
 # limitations under the License.
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Dict, Union
+from typing import TYPE_CHECKING
 
 from requests.auth import HTTPBasicAuth, HTTPDigestAuth
 
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
 class GenericAuth(Authentication):
     """GenericAuth authentication plugin"""
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> AuthBase:
         """Authenticate"""
         self.validate_config_credentials()
         method = getattr(self.config, "method", "basic")
diff --git a/eodag/plugins/authentication/header.py b/eodag/plugins/authentication/header.py
index 9df31dad..fd263f67 100644
--- a/eodag/plugins/authentication/header.py
+++ b/eodag/plugins/authentication/header.py
@@ -17,7 +17,7 @@
 # limitations under the License.
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Dict, Union
+from typing import TYPE_CHECKING, Dict
 
 from requests.auth import AuthBase
 
@@ -60,7 +60,7 @@ class HTTPHeaderAuth(Authentication):
     Expect an undefined behaviour if you use empty braces in header value strings.
     """
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> AuthBase:
         """Authenticate"""
         self.validate_config_credentials()
         headers = {
diff --git a/eodag/plugins/authentication/keycloak.py b/eodag/plugins/authentication/keycloak.py
index 76f833db..c0b77d45 100644
--- a/eodag/plugins/authentication/keycloak.py
+++ b/eodag/plugins/authentication/keycloak.py
@@ -99,7 +99,7 @@ class KeycloakOIDCPasswordAuth(Authentication):
                     f"{self.provider}: {param}",
                 )
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> AuthBase:
         """
         Makes authentication request
         """
diff --git a/eodag/plugins/authentication/oauth.py b/eodag/plugins/authentication/oauth.py
index cbc7ac97..2687796b 100644
--- a/eodag/plugins/authentication/oauth.py
+++ b/eodag/plugins/authentication/oauth.py
@@ -17,13 +17,11 @@
 # limitations under the License.
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Dict, Union
+from typing import TYPE_CHECKING, Dict
 
 from eodag.plugins.authentication.base import Authentication
 
 if TYPE_CHECKING:
-    from requests.auth import AuthBase
-
     from eodag.config import PluginConfig
 
 
@@ -35,7 +33,7 @@ class OAuth(Authentication):
         self.access_key = None
         self.secret_key = None
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> Dict[str, str]:
         """Authenticate"""
         self.validate_config_credentials()
         self.access_key = self.config.credentials["aws_access_key_id"]
diff --git a/eodag/plugins/authentication/openid_connect.py b/eodag/plugins/authentication/openid_connect.py
index ef8b15b0..c76df0bb 100644
--- a/eodag/plugins/authentication/openid_connect.py
+++ b/eodag/plugins/authentication/openid_connect.py
@@ -20,7 +20,7 @@ from __future__ import annotations
 import re
 import string
 from random import SystemRandom
-from typing import TYPE_CHECKING, Any, Dict, Optional, Union
+from typing import TYPE_CHECKING, Any, Dict, Optional
 
 import requests
 from lxml import etree
@@ -144,7 +144,7 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
             )
         self.session = requests.Session()
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> AuthBase:
         """Authenticate"""
         state = self.compute_state()
         authentication_response = self.authenticate_user(state)
@@ -234,7 +234,7 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
                 "The state received in the authorized url does not match initially computed state"
             )
         code = qs["code"][0]
-        token_exchange_data = {
+        token_exchange_data: Dict[str, Any] = {
             "redirect_uri": self.config.redirect_uri,
             "client_id": self.config.client_id,
             "code": code,
diff --git a/eodag/plugins/authentication/qsauth.py b/eodag/plugins/authentication/qsauth.py
index 0c2594f3..36f2ab4c 100644
--- a/eodag/plugins/authentication/qsauth.py
+++ b/eodag/plugins/authentication/qsauth.py
@@ -17,7 +17,7 @@
 # limitations under the License.
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Dict, Union
+from typing import TYPE_CHECKING, Any
 from urllib.parse import parse_qs, urlparse
 
 import requests
@@ -60,7 +60,7 @@ class HttpQueryStringAuth(Authentication):
     :meth:`~eodag.plugins.authentication.query_string.HttpQueryStringAuth.authenticate`
     """
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> AuthBase:
         """Authenticate"""
         self.validate_config_credentials()
 
diff --git a/eodag/plugins/authentication/sas_auth.py b/eodag/plugins/authentication/sas_auth.py
index b2cb3271..c1ed7bb0 100644
--- a/eodag/plugins/authentication/sas_auth.py
+++ b/eodag/plugins/authentication/sas_auth.py
@@ -19,7 +19,7 @@ from __future__ import annotations
 
 import logging
 from json import JSONDecodeError
-from typing import TYPE_CHECKING, Dict, Optional, Union
+from typing import TYPE_CHECKING, Dict, Optional
 
 import requests
 from requests.auth import AuthBase
@@ -39,7 +39,10 @@ class RequestsSASAuth(AuthBase):
     """A custom authentication class to be used with requests module"""
 
     def __init__(
-        self, auth_uri: str, signed_url_key: str, headers: Optional[str] = None
+        self,
+        auth_uri: str,
+        signed_url_key: str,
+        headers: Optional[Dict[str, str]] = None,
     ) -> None:
         self.auth_uri = auth_uri
         self.signed_url_key = signed_url_key
@@ -82,7 +85,7 @@ class SASAuth(Authentication):
         # credentials are optionnal
         pass
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> AuthBase:
         """Authenticate"""
         self.validate_config_credentials()
 
diff --git a/eodag/plugins/authentication/token.py b/eodag/plugins/authentication/token.py
index 93c6376f..dbd15041 100644
--- a/eodag/plugins/authentication/token.py
+++ b/eodag/plugins/authentication/token.py
@@ -18,7 +18,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, Dict, Optional, Union
+from typing import TYPE_CHECKING, Any, Dict, Optional
 from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
 
 import requests
@@ -65,12 +65,12 @@ class TokenAuth(Authentication):
                 f"Missing credentials inputs for provider {self.provider}: {e}"
             )
 
-    def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
+    def authenticate(self) -> AuthBase:
         """Authenticate"""
         self.validate_config_credentials()
 
         # append headers to req if some are specified in config
-        req_kwargs = (
+        req_kwargs: Dict[str, Any] = (
             {"headers": dict(self.config.headers, **USER_AGENT)}
             if hasattr(self.config, "headers")
             else {"headers": USER_AGENT}
diff --git a/eodag/plugins/download/aws.py b/eodag/plugins/download/aws.py
index 6466bfd1..2eb0f07a 100644
--- a/eodag/plugins/download/aws.py
+++ b/eodag/plugins/download/aws.py
@@ -231,7 +231,7 @@ class AwsDownload(Download):
         :param product: The EO product to download
         :type product: :class:`~eodag.api.product._product.EOProduct`
         :param auth: (optional) The configuration of a plugin of type Authentication
-        :type auth: :class:`~eodag.config.PluginConfig`
+        :type auth: Union[AuthBase, Dict[str, str]]
         :param progress_callback: (optional) A method or a callable object
                                   which takes a current size and a maximum
                                   size as inputs and handle progress bar
diff --git a/eodag/plugins/download/creodias_s3.py b/eodag/plugins/download/creodias_s3.py
new file mode 100644
index 00000000..89108878
--- /dev/null
+++ b/eodag/plugins/download/creodias_s3.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
+#
+# This file is part of EODAG project
+#     https://www.github.com/CS-SI/EODAG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import boto3
+from botocore.exceptions import ClientError
+
+from eodag.plugins.download.aws import AwsDownload
+from eodag.utils.exceptions import MisconfiguredError
+
+
+class CreodiasS3Download(AwsDownload):
+    """
+    Download on creodias s3 from their VMs
+    """
+
+    def _get_authenticated_objects_unsigned(self, bucket_name, prefix, auth_dict):
+        """Auth strategy using no-sign-request"""
+
+        raise ClientError(
+            {"Error": {"Code": "AccessDenied", "Message": "skip unsigned"}},
+            "_get_authenticated_objects_unsigned",
+        )
+
+    def _get_authenticated_objects_from_auth_keys(self, bucket_name, prefix, auth_dict):
+        """Auth strategy using RequestPayer=requester and ``aws_access_key_id``/``aws_secret_access_key``
+        from provided credentials"""
+
+        # check if credentials are missing
+        required_creds = ["aws_access_key_id", "aws_secret_access_key"]
+        if not all(auth_dict.get(x, None) for x in required_creds):
+            raise MisconfiguredError(
+                f"Incomplete credentials for {self.provider}, missing "
+                f"{[x for x in required_creds if not auth_dict.get(x, None)]}"
+            )
+
+        s3_session = boto3.session.Session(**auth_dict)
+        s3_resource = s3_session.resource(
+            "s3", endpoint_url=getattr(self.config, "base_uri", None)
+        )
+        objects = s3_resource.Bucket(bucket_name).objects.filter()
+        list(objects.filter(Prefix=prefix).limit(1))
+        self.s3_session = s3_session
+        return objects
diff --git a/eodag/plugins/search/creodias_s3.py b/eodag/plugins/search/creodias_s3.py
new file mode 100644
index 00000000..fc745e64
--- /dev/null
+++ b/eodag/plugins/search/creodias_s3.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
+#
+# This file is part of EODAG project
+#     https://www.github.com/CS-SI/EODAG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import logging
+from types import MethodType
+from typing import Any, Dict, List
+
+import boto3
+import botocore
+from botocore.exceptions import BotoCoreError
+
+from eodag import EOProduct
+from eodag.config import PluginConfig
+from eodag.plugins.authentication.aws_auth import AwsAuth
+from eodag.plugins.search.qssearch import QueryStringSearch
+from eodag.utils.exceptions import AuthenticationError, MisconfiguredError, RequestError
+
+DATA_EXTENSIONS = ["jp2", "tiff", "nc", "grib"]
+logger = logging.getLogger("eodag.search.creodiass3")
+
+
+def patched_register_downloader(self, downloader, authenticator):
+    """Add the download information to the product.
+    :param self: product to which information should be added
+    :type self: EoProduct
+    :param downloader: The download method that it can use
+    :type downloader: Concrete subclass of
+                      :class:`~eodag.plugins.download.base.Download` or
+                      :class:`~eodag.plugins.api.base.Api`
+    :param authenticator: The authentication method needed to perform the download
+    :type authenticator: Concrete subclass of
+                         :class:`~eodag.plugins.authentication.base.Authentication`
+    """
+    # register downloader
+    self.register_downloader_only(downloader, authenticator)
+    # and also update assets
+    try:
+        _update_assets(self, downloader.config, authenticator)
+    except BotoCoreError as e:
+        raise RequestError(f"could not update assets: {str(e)}") from e
+
+
+def _update_assets(product: EOProduct, config: PluginConfig, auth: AwsAuth):
+    product.assets = {}
+    prefix = (
+        product.properties.get("productIdentifier", None).replace("/eodata/", "") + "/"
+    )
+    if prefix:
+        try:
+            auth_dict = auth.authenticate()
+            required_creds = ["aws_access_key_id", "aws_secret_access_key"]
+            if not all(getattr(auth, x) for x in required_creds):
+                raise MisconfiguredError(
+                    f"Incomplete credentials for {product.provider}, missing "
+                    f"{[x for x in required_creds if not getattr(auth, x)]}"
+                )
+            if not getattr(auth, "s3_client", None):
+                auth.s3_client = boto3.client(
+                    "s3",
+                    endpoint_url=config.base_uri,
+                    **auth_dict,
+                )
+            logger.debug(f"Listing assets in {prefix}")
+
+            product.assets = dict()
+            for asset in auth.s3_client.list_objects(
+                Bucket=config.s3_bucket, Prefix=prefix, MaxKeys=300
+            )["Contents"]:
+                asset_basename = (
+                    asset["Key"].split("/")[-1] if "/" in asset["Key"] else asset["Key"]
+                )
+
+                if len(asset_basename) > 0 and asset_basename not in product.assets:
+                    role = (
+                        "data"
+                        if asset_basename.split(".")[-1] in DATA_EXTENSIONS
+                        else "metadata"
+                    )
+
+                    product.assets[asset_basename] = {
+                        "title": asset_basename,
+                        "roles": [role],
+                        "href": f"s3://{config.s3_bucket}/{asset['Key']}",
+                    }
+            # update driver
+            product.driver = product.get_driver()
+
+        except botocore.exceptions.ClientError as e:
+            if str(auth.config.auth_error_code) in str(e):
+                raise AuthenticationError(
+                    f"Authentication failed on {config.base_uri} s3"
+                ) from e
+            else:
+                raise RequestError(
+                    "assets for product %s could not be found", prefix
+                ) from e
+
+
+class CreodiasS3Search(QueryStringSearch):
+    """
+    Search on creodias and adapt results to s3
+    """
+
+    def __init__(self, provider, config):
+        super(CreodiasS3Search, self).__init__(provider, config)
+
+    def normalize_results(
+        self, results: List[Dict[str, Any]], **kwargs: Any
+    ) -> List[EOProduct]:
+        """Build EOProducts from provider results"""
+
+        products = super(CreodiasS3Search, self).normalize_results(results, **kwargs)
+
+        for product in products:
+            # backup original register_downloader to register_downloader_only
+            product.register_downloader_only = product.register_downloader
+            # patched register_downloader that will also update assets
+            product.register_downloader = MethodType(
+                patched_register_downloader, product
+            )
+
+        return products
diff --git a/eodag/resources/providers.yml b/eodag/resources/providers.yml
index 78477217..812bd604 100644
--- a/eodag/resources/providers.yml
+++ b/eodag/resources/providers.yml
@@ -6728,3 +6728,325 @@
     order_status_success:
       status: completed
       message: "Done!"
+
+---
+!provider
+  name: creodias_s3
+  priority: 0
+  description: CloudFerro DIAS data through S3 protocol
+  roles:
+    - host
+  url: https://creodias.eu/
+  search: !plugin
+    type: CreodiasS3Search
+    api_endpoint: 'http://datahub.creodias.eu/resto/api/collections/{collection}/search.json'
+    s3_endpoint: 'https://eodata.cloudferro.com'
+    need_auth: true
+    timeout: 60
+    pagination:
+      next_page_url_tpl: '{url}?{search}&maxRecords={items_per_page}&page={page}&exactCount=1'
+      total_items_nb_key_path: '$.properties.totalResults'
+      max_items_per_page: 1_000
+    discover_metadata:
+      auto_discovery: true
+      metadata_pattern: '^(?!collection)[a-zA-Z0-9]+$'
+      search_param: '{metadata}={{{metadata}}}'
+      metadata_path: '$.properties.*'
+    discover_product_types:
+      fetch_url: https://finder.creodias.eu/attributes.json
+      result_type: json
+      results_entry: 'collections[?billing=="free"]'
+      generic_product_type_id: '$.id'
+      generic_product_type_parsable_properties:
+        collection: '$.id'
+      generic_product_type_parsable_metadata:
+        abstract: '$.description'
+        instrument: '{$.summaries.instruments#csv_list}'
+        platform: '{$.summaries.constellation#csv_list}'
+        platformSerialIdentifier: '{$.summaries.platform#csv_list}'
+        processingLevel: '$.summaries."processing:level"'
+        keywords: '{$.keywords#csv_list}'
+        license: '$.license'
+        title: '$.title'
+        missionStartDate: '$.extent.temporal.interval[0][0]'
+    metadata_mapping:
+      uid: '$.id'
+      productType:
+        - productType
+        - '$.properties.productType'
+      platform: '$.properties.collection'
+      platformSerialIdentifier:
+        - platform
+        - '$.properties.platform'
+      instrument:
+        - instrument
+        - '$.properties.instrument'
+      processingLevel:
+        - processingLevel
+        - '$.properties.processingLevel'
+      # INSPIRE obligated OpenSearch Parameters for Collection Search (Table 4)
+      title: '{$.properties.title#remove_extension}'
+      topicCategory: '$.properties.topicCategory'
+      keyword: '$.properties.keywords'
+      abstract: '$.properties.description'
+      resolution:
+        - 'resolution'
+        - '$.properties.resolution'
+      organisationName:
+        - 'organisationName'
+        - '$.properties.organisationName'
+      publicationDate: '$.properties.published'
+      accessConstraint: '$.properties.license'
+
+      # OpenSearch Parameters for Product Search (Table 5)
+      parentIdentifier:
+        - 'parentIdentifier'
+        - '$.properties.parentIdentifier'
+      acquisitionType: '$.properties.acquisitionType'
+      orbitNumber:
+        - 'orbitNumber'
+        - '$.properties.orbitNumber'
+      orbitDirection:
+        - 'orbitDirection'
+        - '$.properties.orbitDirection'
+      swathIdentifier:
+        - 'swath'
+        - '$.properties.swath'
+      cloudCover:
+        - 'cloudCover=[0,{cloudCover}]'
+        - '$.properties.cloudCover'
+      snowCover:
+        - 'snowCover=[0,{snowCover}]'
+        - '$.properties.snowCover'
+      productVersion: '$.properties.version'
+      productQualityStatus: '$.properties.onlineQualityCheck'
+      processorName: '$.properties.processingName'
+      processingCenter: '$.properties.processingCenter'
+      creationDate: '$.properties.dhusIngestDate'
+      modificationDate: '$.properties.updated'
+      sensorMode:
+        - 'sensorMode'
+        - '$.properties.sensorMode'
+      # OpenSearch Parameters for Acquistion Parameters Search (Table 6)
+      startTimeFromAscendingNode:
+        - startDate
+        - '$.properties.startDate'
+      completionTimeFromAscendingNode:
+        - completionDate
+        - '$.properties.completionDate'
+      polarizationMode:
+        - 'polarisation'
+        - '$.properties.polarisation'
+
+      # Custom parameters (not defined in the base document referenced above)
+      id:
+        - 'productIdentifier=%{id#remove_extension}%'
+        - '{$.properties.title#remove_extension}'
+      tileIdentifier: '$.null'
+      # The geographic extent of the product
+      geometry:
+        - 'geometry={geometry#to_rounded_wkt}'
+        # - '$.geometry'
+        - '($.geometry.`str()`.`sub(/^\\[\\]$/, POLYGON((180 -90, 180 90, -180 90, -180 -90, 180 -90)))`)|($.geometry[*])'
+      # The url of the quicklook
+      quicklook: '$.properties.thumbnail'
+      # The url to download the product "as is" (literal or as a template to be completed either after the search result
+      # is obtained from the provider or during the eodag download phase)
+      downloadLink: 'https://zipper.creodias.eu/download/{uid}'
+      # storageStatus must be one of ONLINE, STAGING, OFFLINE
+      storageStatus: '$.properties.status'
+
+      # Additional metadata provided by the providers but that don't appear in the reference spec
+      thumbnail: '$.properties.thumbnail'
+  download: !plugin
+    type: CreodiasS3Download
+    flatten_top_dirs: True
+    base_uri: 'https://eodata.cloudferro.com'
+    s3_bucket: 'eodata'
+  auth: !plugin
+    type: AwsAuth
+    auth_error_code: 403
+  products:
+    # S1
+    S1_SAR_RAW:
+      productType: RAW
+      collection: Sentinel1
+      metadata_mapping:
+        cloudCover: '$.null'
+    S1_SAR_GRD:
+      productType: GRD
+      collection: Sentinel1
+      metadata_mapping:
+        cloudCover: '$.null'
+    S1_SAR_SLC:
+      productType: SLC
+      collection: Sentinel1
+      metadata_mapping:
+        cloudCover: '$.null'
+    S1_SAR_OCN:
+      productType: OCN
+      collection: Sentinel1
+      metadata_mapping:
+        cloudCover: '$.null'
+    # S2
+    S2_MSI_L1C:
+      collection: SENTINEL-2
+      productType: S2MSI1C
+      prefixes_delimiter: ".jp2"
+    S2_MSI_L2A:
+      collection: SENTINEL-2
+      productType: S2MSI2A
+    # S3 SRAL
+    S3_SRA:
+      productType: SRA
+      collection: Sentinel3
+      metadata_mapping:
+        cloudCover: '$.null'
+    S3_SRA_A:
+      productType: SRA_A
+      collection: Sentinel3
+      metadata_mapping:
+        cloudCover: '$.null'
+    S3_SRA_BS:
+      productType: SRA_BS
+      collection: Sentinel3
+      metadata_mapping:
+        cloudCover: '$.null'
+    S3_LAN:
+      productType: LAN
+      collection: Sentinel3
+      metadata_mapping:
+        cloudCover: '$.null'
+    S3_WAT:
+      productType: WAT
+      collection: Sentinel3
+      metadata_mapping:
+        cloudCover: '$.null'
+    # S3 OLCI
+    S3_EFR:
+      productType: EFR
+      collection: Sentinel3
+    S3_ERR:
+      productType: ERR
+      collection: Sentinel3
+    S3_OLCI_L2LRR:
+      productType: LRR
+      collection: Sentinel3
+    S3_OLCI_L2LFR:
+      productType: LFR
+      collection: Sentinel3
+    S3_OLCI_L2WRR:
+      productType: WRR
+      collection: Sentinel3
+    S3_OLCI_L2WFR:
+      productType: WFR
+      collection: Sentinel3
+    # S3 SLSTR
+    S3_SLSTR_L1RBT:
+      productType: RBT
+      collection: Sentinel3
+    S3_SLSTR_L2LST:
+      productType: LST
+      collection: Sentinel3
+    S3_SLSTR_L2WST:
+      productType: WST
+      collection: Sentinel3
+    S3_SLSTR_L2AOD:
+      productType: AOD___
+      collection: Sentinel3
+    S3_SLSTR_L2FRP:
+      productType: FRP___
+      collection: Sentinel3
+    # S3 SY
+    S3_SY_AOD:
+      productType: SY_2_AOD___
+      collection: Sentinel3
+    S3_SY_SYN:
+      productType: SY_2_SYN___
+      collection: Sentinel3
+    S3_SY_V10:
+      productType: SY_2_V10___
+      collection: Sentinel3
+    S3_SY_VG1:
+      productType: SY_2_VG1___
+      collection: Sentinel3
+    S3_SY_VGP:
+      productType: SY_2_VGP___
+      collection: Sentinel3
+    # S5P L1
+    S5P_L1B_IR_SIR:
+      productType: L1B_IR_SIR
+      collection: Sentinel5P
+    S5P_L1B_IR_UVN:
+      productType: L1B_IR_UVN
+      collection: Sentinel5P
+    S5P_L1B_RA_BD1:
+      productType: L1B_RA_BD1
+      collection: Sentinel5P
+    S5P_L1B_RA_BD2:
+      productType: L1B_RA_BD2
+      collection: Sentinel5P
+    S5P_L1B_RA_BD3:
+      productType: L1B_RA_BD3
+      collection: Sentinel5P
+    S5P_L1B_RA_BD4:
+      productType: L1B_RA_BD4
+      collection: Sentinel5P
+    S5P_L1B_RA_BD5:
+      productType: L1B_RA_BD5
+      collection: Sentinel5P
+    S5P_L1B_RA_BD6:
+      productType: L1B_RA_BD6
+      collection: Sentinel5P
+    S5P_L1B_RA_BD7:
+      productType: L1B_RA_BD7
+      collection: Sentinel5P
+    S5P_L1B_RA_BD8:
+      productType: L1B_RA_BD8
+      collection: Sentinel5P
+    # S5P L2
+    S5P_L2_NO2:
+      productType: L2__NO2___
+      collection: Sentinel5P
+    S5P_L2_CLOUD:
+      productType: L2__CLOUD_
+      collection: Sentinel5P
+    S5P_L2_O3:
+      productType: L2__O3____
+      collection: Sentinel5P
+    S5P_L2_CO:
+      productType: L2__CO____
+      collection: Sentinel5P
+    S5P_L2_AER_AI:
+      productType: L2__AER_AI
+      collection: Sentinel5P
+    S5P_L2_O3_PR:
+      productType: L2__O3__PR
+      collection: Sentinel5P
+    S5P_L2_O3_TCL:
+      productType: L2__O3_TCL
+      collection: Sentinel5P
+    S5P_L2_AER_LH:
+      productType: L2__AER_LH
+      collection: Sentinel5P
+    S5P_L2_HCHO:
+      productType: L2__HCHO__
+      collection: Sentinel5P
+    S5P_L2_CH4:
+      productType: L2__CH4___
+      collection: Sentinel5P
+    S5P_L2_NP_BD3:
+      productType: L2__NP_BD3
+      collection: Sentinel5P
+    S5P_L2_NP_BD6:
+      productType: L2__NP_BD6
+      collection: Sentinel5P
+    S5P_L2_NP_BD7:
+      productType: L2__NP_BD7
+      collection: Sentinel5P
+    S5P_L2_SO2:
+      productType: L2__SO2___
+      collection: Sentinel5P
+    GENERIC_PRODUCT_TYPE:
+      productType: '{productType}'
+      collection: '{collection}'
diff --git a/eodag/resources/user_conf_template.yml b/eodag/resources/user_conf_template.yml
index 7651e1a9..60b9abc4 100644
--- a/eodag/resources/user_conf_template.yml
+++ b/eodag/resources/user_conf_template.yml
@@ -199,3 +199,13 @@ wekeo:
         credentials:
             username:
             password:
+
+creodias_s3:
+    priority: # Lower value means lower priority (Default: 0)
+    search:   # Search parameters configuration
+    download:
+        outputs_prefix:
+    auth:
+        credentials:
+          aws_access_key_id:
+          aws_secret_access_key:
diff --git a/eodag/rest/stac.py b/eodag/rest/stac.py
index 8f84ddea..4c627592 100644
--- a/eodag/rest/stac.py
+++ b/eodag/rest/stac.py
@@ -242,7 +242,10 @@ class StacItem(StacCommon):
             )
 
             product_dict = deepcopy(product.__dict__)
-            product_dict["assets"] = product.assets.as_dict()
+            if isinstance(product.assets, dict):
+                product_dict["assets"] = product.assets
+            else:
+                product_dict["assets"] = product.assets.as_dict()
 
             product_item = jsonpath_parse_dict_items(
                 item_model,
diff --git a/setup.cfg b/setup.cfg
index a1ca6f7b..7ece3fea 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -146,6 +146,7 @@ eodag.plugins.download =
     AwsDownload = eodag.plugins.download.aws:AwsDownload
     HTTPDownload = eodag.plugins.download.http:HTTPDownload
     S3RestDownload = eodag.plugins.download.s3rest:S3RestDownload
+    CreodiasS3Download = eodag.plugins.download.creodias_s3:CreodiasS3Download
 eodag.plugins.search =
     CSWSearch = eodag.plugins.search.csw:CSWSearch
     QueryStringSearch = eodag.plugins.search.qssearch:QueryStringSearch
@@ -156,6 +157,7 @@ eodag.plugins.search =
     StaticStacSearch = eodag.plugins.search.static_stac_search:StaticStacSearch
     BuildPostSearchResult = eodag.plugins.search.build_search_result:BuildPostSearchResult
     DataRequestSearch = eodag.plugins.search.data_request_search:DataRequestSearch
+    CreodiasS3Search = eodag.plugins.search.creodias_s3:CreodiasS3Search
 
 [flake8]
 ignore = E203, W503


Test patch:
diff --git a/tests/resources/provider_responses/creodias_s3_objects.json b/tests/resources/provider_responses/creodias_s3_objects.json
new file mode 100644
index 00000000..7c434475
--- /dev/null
+++ b/tests/resources/provider_responses/creodias_s3_objects.json
@@ -0,0 +1,37 @@
+{
+  "Contents": [
+      {
+        "Key": "Sentinel-1/SAR/GRD/2014/10/10/S1A_EW_GRDH_1SDH_20141010T120113_20141010T120222_002767_0031C8_D5D7.SAFE/support/s1-map-overlay.xsd",
+        "LastModified": "2018-06-01T15:22:13Z",
+        "ETag": "504eb851210859c67acb42ea4fed5fc5",
+        "Size": 450,
+        "StorageClass": "STANDARD",
+        "Owner": {
+          "DisplayName": "user1",
+          "ID": "user1"
+        }
+      },
+      {
+        "Key": "Sentinel-1/SAR/GRD/2014/10/10/S1A_EW_GRDH_1SDH_20141010T120113_20141010T120222_002767_0031C8_D5D7.SAFE/support/s1-object-types.xsd",
+        "LastModified": "2018-06-01T15:22:13Z",
+        "ETag": "8aabb6347fb0a5f555aeb3e9aa3b55bb",
+        "Size": 62654,
+        "StorageClass": "STANDARD",
+        "Owner": {
+          "DisplayName": "user1",
+          "ID": "user1"
+        }
+      },
+      {
+        "Key": "Sentinel-1/SAR/GRD/2014/10/10/S1A_EW_GRDH_1SDH_20141010T120113_20141010T120222_002767_0031C8_D5D7.SAFE/support/s1-product-preview.xsd",
+        "LastModified": "2018-06-01T15:22:13Z",
+        "ETag": "351b284603c155dcb83cdb5daedaf098",
+        "Size": 440,
+        "StorageClass": "STANDARD",
+        "Owner": {
+          "DisplayName": "user1",
+          "ID": "user1"
+        }
+      }
+    ]
+}
diff --git a/tests/units/test_core.py b/tests/units/test_core.py
index bc308123..f1da82c3 100644
--- a/tests/units/test_core.py
+++ b/tests/units/test_core.py
@@ -118,10 +118,10 @@ class TestCore(TestCoreBase):
         "CLMS_GLO_LAI_333M": ["wekeo"],
         "CLMS_GLO_NDVI_1KM_LTS": ["wekeo"],
         "CLMS_GLO_NDVI_333M": ["wekeo"],
-        "COP_DEM_GLO30_DGED": ["creodias", "wekeo"],
-        "COP_DEM_GLO30_DTED": ["creodias"],
-        "COP_DEM_GLO90_DGED": ["creodias", "wekeo"],
-        "COP_DEM_GLO90_DTED": ["creodias"],
+        "COP_DEM_GLO30_DGED": ["creodias", "creodias_s3", "wekeo"],
+        "COP_DEM_GLO30_DTED": ["creodias", "creodias_s3"],
+        "COP_DEM_GLO90_DGED": ["creodias", "creodias_s3", "wekeo"],
+        "COP_DEM_GLO90_DTED": ["creodias", "creodias_s3"],
         "EEA_DAILY_SSM_1KM": ["wekeo"],
         "EEA_DAILY_SWI_1KM": ["wekeo"],
         "EEA_DAILY_VI": ["wekeo"],
@@ -180,17 +180,27 @@ class TestCore(TestCoreBase):
             "aws_eos",
             "cop_dataspace",
             "creodias",
+            "creodias_s3",
             "onda",
             "peps",
             "planetary_computer",
             "sara",
             "wekeo",
         ],
-        "S1_SAR_OCN": ["cop_dataspace", "creodias", "onda", "peps", "sara", "wekeo"],
-        "S1_SAR_RAW": ["cop_dataspace", "creodias", "onda", "wekeo"],
+        "S1_SAR_OCN": [
+            "cop_dataspace",
+            "creodias",
+            "creodias_s3",
+            "onda",
+            "peps",
+            "sara",
+            "wekeo",
+        ],
+        "S1_SAR_RAW": ["cop_dataspace", "creodias", "creodias_s3", "onda", "wekeo"],
         "S1_SAR_SLC": [
             "cop_dataspace",
             "creodias",
+            "creodias_s3",
             "onda",
             "peps",
             "sara",
@@ -201,6 +211,7 @@ class TestCore(TestCoreBase):
             "aws_eos",
             "cop_dataspace",
             "creodias",
+            "creodias_s3",
             "earth_search",
             "earth_search_gcs",
             "onda",
@@ -214,6 +225,7 @@ class TestCore(TestCoreBase):
             "aws_eos",
             "cop_dataspace",
             "creodias",
+            "creodias_s3",
             "earth_search",
             "onda",
             "planetary_computer",
@@ -226,12 +238,13 @@ class TestCore(TestCoreBase):
         "S2_MSI_L2B_MAJA_SNOW": ["theia"],
         "S2_MSI_L2B_MAJA_WATER": ["theia"],
         "S2_MSI_L3A_WASP": ["theia"],
-        "S3_EFR": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
-        "S3_ERR": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
-        "S3_LAN": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
+        "S3_EFR": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara", "wekeo"],
+        "S3_ERR": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara", "wekeo"],
+        "S3_LAN": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara", "wekeo"],
         "S3_OLCI_L2LFR": [
             "cop_dataspace",
             "creodias",
+            "creodias_s3",
             "onda",
             "sara",
             "wekeo",
@@ -239,40 +252,85 @@ class TestCore(TestCoreBase):
         "S3_OLCI_L2LRR": [
             "cop_dataspace",
             "creodias",
+            "creodias_s3",
+            "onda",
+            "sara",
+            "wekeo",
+        ],
+        "S3_OLCI_L2WFR": [
+            "cop_dataspace",
+            "creodias",
+            "creodias_s3",
+            "onda",
+            "sara",
+            "wekeo",
+        ],
+        "S3_OLCI_L2WRR": [
+            "cop_dataspace",
+            "creodias",
+            "creodias_s3",
             "onda",
             "sara",
             "wekeo",
         ],
-        "S3_OLCI_L2WFR": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
-        "S3_OLCI_L2WRR": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
         "S3_RAC": ["sara"],
         "S3_SLSTR_L1RBT": [
             "cop_dataspace",
             "creodias",
+            "creodias_s3",
             "onda",
             "sara",
             "wekeo",
         ],
         "S3_SLSTR_L2": ["wekeo"],
-        "S3_SLSTR_L2AOD": ["cop_dataspace", "creodias", "sara", "wekeo"],
+        "S3_SLSTR_L2AOD": ["cop_dataspace", "creodias", "creodias_s3", "sara", "wekeo"],
         "S3_SLSTR_L2FRP": [
             "cop_dataspace",
             "creodias",
+            "creodias_s3",
+            "onda",
+            "sara",
+            "wekeo",
+        ],
+        "S3_SLSTR_L2LST": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara"],
+        "S3_SLSTR_L2WST": [
+            "cop_dataspace",
+            "creodias",
+            "creodias_s3",
+            "onda",
+            "sara",
+            "wekeo",
+        ],
+        "S3_SRA": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara", "wekeo"],
+        "S3_SRA_A": [
+            "cop_dataspace",
+            "creodias",
+            "creodias_s3",
+            "onda",
+            "sara",
+            "wekeo",
+        ],
+        "S3_SRA_BS": [
+            "cop_dataspace",
+            "creodias",
+            "creodias_s3",
+            "onda",
+            "sara",
+            "wekeo",
+        ],
+        "S3_SY_AOD": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara"],
+        "S3_SY_SYN": [
+            "cop_dataspace",
+            "creodias",
+            "creodias_s3",
             "onda",
             "sara",
             "wekeo",
         ],
-        "S3_SLSTR_L2LST": ["cop_dataspace", "creodias", "onda", "sara"],
-        "S3_SLSTR_L2WST": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
-        "S3_SRA": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
-        "S3_SRA_A": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
-        "S3_SRA_BS": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
-        "S3_SY_AOD": ["cop_dataspace", "creodias", "onda", "sara"],
-        "S3_SY_SYN": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
-        "S3_SY_V10": ["cop_dataspace", "creodias", "onda", "sara"],
-        "S3_SY_VG1": ["cop_dataspace", "creodias", "onda", "sara"],
-        "S3_SY_VGP": ["cop_dataspace", "creodias", "onda", "sara"],
-        "S3_WAT": ["cop_dataspace", "creodias", "onda", "sara", "wekeo"],
+        "S3_SY_V10": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara"],
+        "S3_SY_VG1": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara"],
+        "S3_SY_VGP": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara"],
+        "S3_WAT": ["cop_dataspace", "creodias", "creodias_s3", "onda", "sara", "wekeo"],
         "S3_OLCI_L2WFR_BC003": ["wekeo"],
         "S3_OLCI_L2WRR_BC003": ["wekeo"],
         "S3_SRA_1A_BC004": ["wekeo"],
@@ -289,30 +347,30 @@ class TestCore(TestCoreBase):
         "S6_P4_L2HR_F06": ["wekeo"],
         "S6_AMR_L2_F06": ["wekeo"],
         "S5P_L1B2_IR_ALL": ["wekeo"],
-        "S5P_L1B_IR_SIR": ["cop_dataspace", "creodias"],
-        "S5P_L1B_IR_UVN": ["cop_dataspace", "creodias"],
-        "S5P_L1B_RA_BD1": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L1B_RA_BD2": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L1B_RA_BD3": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L1B_RA_BD4": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L1B_RA_BD5": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L1B_RA_BD6": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L1B_RA_BD7": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L1B_RA_BD8": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_AER_AI": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_AER_LH": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_CH4": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_CLOUD": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_CO": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_HCHO": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_NO2": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_NP_BD3": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_NP_BD6": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_NP_BD7": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_O3": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_O3_PR": ["cop_dataspace", "creodias", "onda"],
-        "S5P_L2_O3_TCL": ["cop_dataspace", "creodias"],
-        "S5P_L2_SO2": ["cop_dataspace", "creodias", "onda"],
+        "S5P_L1B_IR_SIR": ["cop_dataspace", "creodias", "creodias_s3"],
+        "S5P_L1B_IR_UVN": ["cop_dataspace", "creodias", "creodias_s3"],
+        "S5P_L1B_RA_BD1": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L1B_RA_BD2": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L1B_RA_BD3": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L1B_RA_BD4": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L1B_RA_BD5": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L1B_RA_BD6": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L1B_RA_BD7": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L1B_RA_BD8": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_AER_AI": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_AER_LH": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_CH4": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_CLOUD": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_CO": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_HCHO": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_NO2": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_NP_BD3": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_NP_BD6": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_NP_BD7": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_O3": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_O3_PR": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
+        "S5P_L2_O3_TCL": ["cop_dataspace", "creodias", "creodias_s3"],
+        "S5P_L2_SO2": ["cop_dataspace", "creodias", "creodias_s3", "onda"],
         "SATELLITE_CARBON_DIOXIDE": ["cop_cds", "wekeo"],
         "SATELLITE_METHANE": ["cop_cds", "wekeo"],
         "SATELLITE_SEA_LEVEL_BLACK_SEA": ["cop_cds", "wekeo"],
@@ -349,6 +407,7 @@ class TestCore(TestCoreBase):
             "cop_dataspace",
             "planetary_computer",
             "hydroweb_next",
+            "creodias_s3",
         ],
     }
     SUPPORTED_PROVIDERS = [
@@ -372,6 +431,7 @@ class TestCore(TestCoreBase):
         "planetary_computer",
         "hydroweb_next",
         "wekeo",
+        "creodias_s3",
     ]
 
     def setUp(self):
diff --git a/tests/units/test_download_plugins.py b/tests/units/test_download_plugins.py
index 39e756f1..d2e6cb60 100644
--- a/tests/units/test_download_plugins.py
+++ b/tests/units/test_download_plugins.py
@@ -1351,3 +1351,64 @@ class TestDownloadPluginS3Rest(BaseDownloadPluginTest):
         run()
         mock_order.assert_called_once_with(mock.ANY, self.product, auth=None)
         self.assertEqual(mock_order_status.call_count, 2)
+
+
+class TestDownloadPluginCreodiasS3(BaseDownloadPluginTest):
+    @mock.patch("eodag.plugins.download.aws.flatten_top_directories", autospec=True)
+    @mock.patch(
+        "eodag.plugins.download.aws.AwsDownload.check_manifest_file_list", autospec=True
+    )
+    @mock.patch(
+        "eodag.plugins.download.aws.AwsDownload.finalize_s2_safe_product", autospec=True
+    )
+    @mock.patch(
+        "eodag.plugins.download.aws.AwsDownload.get_chunk_dest_path", autospec=True
+    )
+    @mock.patch(
+        "eodag.plugins.download.creodias_s3.CreodiasS3Download._get_authenticated_objects_from_auth_keys",
+        autospec=True,
+    )
+    @mock.patch("eodag.plugins.download.aws.requests.get", autospec=True)
+    def test_plugins_download_creodias_s3(
+        self,
+        mock_requests_get,
+        mock_get_authenticated_objects,
+        mock_get_chunk_dest_path,
+        mock_finalize_s2_safe_product,
+        mock_check_manifest_file_list,
+        mock_flatten_top_directories,
+    ):
+        product = EOProduct(
+            "creodias_s3",
+            dict(
+                geometry="POINT (0 0)",
+                title="dummy_product",
+                id="dummy",
+            ),
+        )
+        product.location = product.remote_location = "a"
+        assets = {
+            "a1": {"title": "a1", "href": "s3://eodata/a/a1"},
+            "a2": {"title": "a2", "href": "s3://eodata/a/a2"},
+        }
+        product.assets = assets
+        plugin = self.get_download_plugin(product)
+        product.properties["tileInfo"] = "http://example.com/tileInfo.json"
+        # authenticated objects mock
+        mock_get_authenticated_objects.return_value.keys.return_value = [
+            "a1",
+            "a2",
+        ]
+        mock_get_authenticated_objects.return_value.filter.side_effect = (
+            lambda *x, **y: [mock.Mock(size=0, key=y["Prefix"])]
+        )
+
+        plugin.download(product, outputs_prefix=self.output_dir, auth={})
+
+        mock_get_authenticated_objects.assert_called_once_with(
+            plugin, "eodata", "a", {}
+        )
+        self.assertEqual(mock_get_chunk_dest_path.call_count, 2)
+        self.assertEqual(mock_finalize_s2_safe_product.call_count, 0)
+        self.assertEqual(mock_check_manifest_file_list.call_count, 0)
+        self.assertEqual(mock_flatten_top_directories.call_count, 1)
diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py
index d8401df3..9ee6a43a 100644
--- a/tests/units/test_search_plugins.py
+++ b/tests/units/test_search_plugins.py
@@ -22,9 +22,11 @@ from datetime import datetime, timedelta, timezone
 from pathlib import Path
 from unittest import mock
 
+import boto3
 import dateutil
 import responses
 import yaml
+from botocore.stub import Stubber
 from requests import RequestException
 
 from tests.context import (
@@ -34,6 +36,7 @@ from tests.context import (
     USER_AGENT,
     AuthenticationError,
     EOProduct,
+    MisconfiguredError,
     PluginManager,
     RequestError,
     cached_parse,
@@ -1514,3 +1517,82 @@ class TestSearchPluginDataRequestSearch(BaseSearchPluginTest):
             )
 
         run()
+
+
+class TestSearchPluginCreodiasS3Search(BaseSearchPluginTest):
+    def setUp(self):
+        super(TestSearchPluginCreodiasS3Search, self).setUp()
+        self.provider = "creodias_s3"
+
+    @mock.patch("eodag.plugins.search.qssearch.requests.get", autospec=True)
+    def test_plugins_search_creodias_s3_links(self, mock_request):
+        # s3 links should be added to products with register_downloader
+        search_plugin = self.get_search_plugin("S1_SAR_GRD", self.provider)
+        client = boto3.client("s3", aws_access_key_id="a", aws_secret_access_key="b")
+        stubber = Stubber(client)
+        s3_response_file = (
+            Path(TEST_RESOURCES_PATH) / "provider_responses/creodias_s3_objects.json"
+        )
+        with open(s3_response_file) as f:
+            list_objects_response = json.load(f)
+        creodias_search_result_file = (
+            Path(TEST_RESOURCES_PATH) / "eodag_search_result_creodias.geojson"
+        )
+        with open(creodias_search_result_file) as f:
+            creodias_search_result = json.load(f)
+        mock_request.return_value = MockResponse(creodias_search_result, 200)
+
+        res = search_plugin.query("S1_SAR_GRD")
+        for product in res[0]:
+            download_plugin = self.plugins_manager.get_download_plugin(product)
+            auth_plugin = self.plugins_manager.get_auth_plugin(self.provider)
+            stubber.add_response("list_objects", list_objects_response)
+            stubber.activate()
+            setattr(auth_plugin, "s3_client", client)
+            # fails if credentials are missing
+            auth_plugin.config.credentials = {
+                "aws_access_key_id": "",
+                "aws_secret_access_key": "",
+            }
+            with self.assertRaisesRegex(
+                MisconfiguredError,
+                r"^Incomplete credentials .* \['aws_access_key_id', 'aws_secret_access_key'\]$",
+            ):
+                product.register_downloader(download_plugin, auth_plugin)
+            auth_plugin.config.credentials = {
+                "aws_access_key_id": "foo",
+                "aws_secret_access_key": "bar",
+            }
+            product.register_downloader(download_plugin, auth_plugin)
+        assets = res[0][0].assets
+        # check if s3 links have been created correctly
+        for asset in assets.values():
+            self.assertIn("s3://eodata/Sentinel-1/SAR/GRD/2014/10/10", asset["href"])
+
+    @mock.patch("eodag.plugins.search.qssearch.requests.get", autospec=True)
+    def test_plugins_search_creodias_s3_client_error(self, mock_request):
+        # request error should be raised when there is an error when fetching data from the s3
+        search_plugin = self.get_search_plugin("S1_SAR_GRD", self.provider)
+        client = boto3.client("s3", aws_access_key_id="a", aws_secret_access_key="b")
+        stubber = Stubber(client)
+
+        creodias_search_result_file = (
+            Path(TEST_RESOURCES_PATH) / "eodag_search_result_creodias.geojson"
+        )
+        with open(creodias_search_result_file) as f:
+            creodias_search_result = json.load(f)
+        mock_request.return_value = MockResponse(creodias_search_result, 200)
+
+        with self.assertRaises(RequestError):
+            res = search_plugin.query("S1_SAR_GRD")
+            for product in res[0]:
+                download_plugin = self.plugins_manager.get_download_plugin(product)
+                auth_plugin = self.plugins_manager.get_auth_plugin(self.provider)
+                auth_plugin.config.credentials = {
+                    "aws_access_key_id": "foo",
+                    "aws_secret_access_key": "bar",
+                }
+                stubber.add_client_error("list_objects")
+                stubber.activate()
+                setattr(auth_plugin, "s3_client", client)
+                product.register_downloader(download_plugin, auth_plugin)