From 302951313af948e1f2f7872a0b223034dd26c9fb Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Fri, 26 Apr 2024 13:25:23 +0100 Subject: [PATCH 01/11] basic_auth + tests --- stac_fastapi/core/setup.py | 1 + .../stac_fastapi/elasticsearch/app.py | 3 + .../stac_fastapi/elasticsearch/basic_auth.py | 114 ++++++++++++++++++ .../opensearch/stac_fastapi/opensearch/app.py | 3 + .../stac_fastapi/opensearch/basic_auth.py | 114 ++++++++++++++++++ stac_fastapi/tests/basic_auth/basic_auth.py | 51 ++++++++ stac_fastapi/tests/conftest.py | 50 ++++++++ 7 files changed, 336 insertions(+) create mode 100644 stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py create mode 100644 stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py create mode 100644 stac_fastapi/tests/basic_auth/basic_auth.py diff --git a/stac_fastapi/core/setup.py b/stac_fastapi/core/setup.py index bc7bb8ea..a3eb768d 100644 --- a/stac_fastapi/core/setup.py +++ b/stac_fastapi/core/setup.py @@ -18,6 +18,7 @@ "overrides", "geojson-pydantic", "pygeofilter==0.2.1", + "typing_extensions==4.4.0", ] setup( diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 6d189179..c7aa9b84 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -12,6 +12,7 @@ ) from stac_fastapi.core.extensions import QueryExtension from stac_fastapi.core.session import Session +from stac_fastapi.elasticsearch.basic_auth import apply_basic_auth from stac_fastapi.elasticsearch.config import ElasticsearchSettings from stac_fastapi.elasticsearch.database_logic import ( DatabaseLogic, @@ -77,6 +78,8 @@ app = api.app app.root_path = os.getenv("STAC_FASTAPI_ROOT_PATH", "") +apply_basic_auth(api) + @app.on_event("startup") async def _startup_event() -> None: diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py new file mode 100644 index 00000000..ccff3c1d --- /dev/null +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py @@ -0,0 +1,114 @@ +"""Basic Authentication Module.""" + +import json +import os +import secrets +from typing import Any, Dict + +from fastapi import Depends, HTTPException, Request, status +from fastapi.routing import APIRoute +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +from stac_fastapi.api.app import StacApi + +security = HTTPBasic() + +_BASIC_AUTH: Dict[str, Any] = {} + + +def has_access( + request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(security)] +) -> str: + """Check if the provided credentials match the expected \ + username and password stored in environment variables for basic authentication. + + Args: + request (Request): The FastAPI request object. + credentials (HTTPBasicCredentials): The HTTP basic authentication credentials. + + Returns: + str: The username if authentication is successful. + + Raises: + HTTPException: If authentication fails due to incorrect username or password. + """ + global _BASIC_AUTH + + users = _BASIC_AUTH.get("users") + user: Dict[str, Any] = next( + (u for u in users if u.get("username") == credentials.username), {} + ) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + + # Compare the provided username and password with the correct ones using compare_digest + if not secrets.compare_digest( + credentials.username.encode("utf-8"), user.get("username").encode("utf-8") + ) or not secrets.compare_digest( + credentials.password.encode("utf-8"), user.get("password").encode("utf-8") + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + + permissions = user.get("permissions", []) + path = request.url.path + method = request.method + + if permissions == "*": + return credentials.username + for permission in permissions: + if permission["path"] == path and method in permission.get("method", []): + return credentials.username + + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Insufficient permissions for [{method} {path}]", + ) + + +def apply_basic_auth(api: StacApi) -> None: + """Apply basic authentication to the provided FastAPI application \ + based on environment variables for username, password, and endpoints. + + Args: + api (StacApi): The FastAPI application. + + Raises: + HTTPException: If there are issues with the configuration or format + of the environment variables. + """ + global _BASIC_AUTH + + basic_auth_json_str = os.environ.get("BASIC_AUTH") + if not basic_auth_json_str: + print("Basic authentication disabled.") + return + + try: + _BASIC_AUTH = json.loads(basic_auth_json_str) + except json.JSONDecodeError as exception: + print(f"Invalid JSON format for BASIC_AUTH. {exception=}") + raise + public_endpoints = _BASIC_AUTH.get("public_endpoints", []) + users = _BASIC_AUTH.get("users") + if not users: + raise Exception("Invalid JSON format for BASIC_AUTH. Key 'users' undefined.") + + app = api.app + for route in app.routes: + if isinstance(route, APIRoute): + for method in route.methods: + endpoint = {"path": route.path, "method": method} + if endpoint not in public_endpoints: + api.add_route_dependencies([endpoint], [Depends(has_access)]) + + print("Basic authentication enabled.") diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py index a91e9a86..e3c95733 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py @@ -21,6 +21,7 @@ TransactionExtension, ) from stac_fastapi.extensions.third_party import BulkTransactionExtension +from stac_fastapi.opensearch.basic_auth import apply_basic_auth from stac_fastapi.opensearch.config import OpensearchSettings from stac_fastapi.opensearch.database_logic import ( DatabaseLogic, @@ -77,6 +78,8 @@ app = api.app app.root_path = os.getenv("STAC_FASTAPI_ROOT_PATH", "") +apply_basic_auth(api) + @app.on_event("startup") async def _startup_event() -> None: diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py new file mode 100644 index 00000000..ccff3c1d --- /dev/null +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py @@ -0,0 +1,114 @@ +"""Basic Authentication Module.""" + +import json +import os +import secrets +from typing import Any, Dict + +from fastapi import Depends, HTTPException, Request, status +from fastapi.routing import APIRoute +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +from stac_fastapi.api.app import StacApi + +security = HTTPBasic() + +_BASIC_AUTH: Dict[str, Any] = {} + + +def has_access( + request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(security)] +) -> str: + """Check if the provided credentials match the expected \ + username and password stored in environment variables for basic authentication. + + Args: + request (Request): The FastAPI request object. + credentials (HTTPBasicCredentials): The HTTP basic authentication credentials. + + Returns: + str: The username if authentication is successful. + + Raises: + HTTPException: If authentication fails due to incorrect username or password. + """ + global _BASIC_AUTH + + users = _BASIC_AUTH.get("users") + user: Dict[str, Any] = next( + (u for u in users if u.get("username") == credentials.username), {} + ) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + + # Compare the provided username and password with the correct ones using compare_digest + if not secrets.compare_digest( + credentials.username.encode("utf-8"), user.get("username").encode("utf-8") + ) or not secrets.compare_digest( + credentials.password.encode("utf-8"), user.get("password").encode("utf-8") + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) + + permissions = user.get("permissions", []) + path = request.url.path + method = request.method + + if permissions == "*": + return credentials.username + for permission in permissions: + if permission["path"] == path and method in permission.get("method", []): + return credentials.username + + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Insufficient permissions for [{method} {path}]", + ) + + +def apply_basic_auth(api: StacApi) -> None: + """Apply basic authentication to the provided FastAPI application \ + based on environment variables for username, password, and endpoints. + + Args: + api (StacApi): The FastAPI application. + + Raises: + HTTPException: If there are issues with the configuration or format + of the environment variables. + """ + global _BASIC_AUTH + + basic_auth_json_str = os.environ.get("BASIC_AUTH") + if not basic_auth_json_str: + print("Basic authentication disabled.") + return + + try: + _BASIC_AUTH = json.loads(basic_auth_json_str) + except json.JSONDecodeError as exception: + print(f"Invalid JSON format for BASIC_AUTH. {exception=}") + raise + public_endpoints = _BASIC_AUTH.get("public_endpoints", []) + users = _BASIC_AUTH.get("users") + if not users: + raise Exception("Invalid JSON format for BASIC_AUTH. Key 'users' undefined.") + + app = api.app + for route in app.routes: + if isinstance(route, APIRoute): + for method in route.methods: + endpoint = {"path": route.path, "method": method} + if endpoint not in public_endpoints: + api.add_route_dependencies([endpoint], [Depends(has_access)]) + + print("Basic authentication enabled.") diff --git a/stac_fastapi/tests/basic_auth/basic_auth.py b/stac_fastapi/tests/basic_auth/basic_auth.py new file mode 100644 index 00000000..ad804247 --- /dev/null +++ b/stac_fastapi/tests/basic_auth/basic_auth.py @@ -0,0 +1,51 @@ +import os + +import pytest + + +@pytest.mark.asyncio +async def test_get_search_not_authenticated(app_client_basic_auth, ctx): + """Test public endpoint search without authentication""" + if not os.getenv("BASIC_AUTH"): + pytest.skip() + params = {"id": ctx.item["id"]} + + response = await app_client_basic_auth.get("/search", params=params) + + assert response.status_code == 200 + assert response.json()["features"][0]["geometry"] == ctx.item["geometry"] + + +@pytest.mark.asyncio +async def test_post_search_authenticated(app_client_basic_auth, ctx): + """Test protected post search with reader auhtentication""" + if not os.getenv("BASIC_AUTH"): + pytest.skip() + params = {"id": ctx.item["id"]} + headers = {"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="} + + response = await app_client_basic_auth.post("/search", json=params, headers=headers) + + assert response.status_code == 200 + assert response.json()["features"][0]["geometry"] == ctx.item["geometry"] + + +@pytest.mark.asyncio +async def test_delete_resource_insufficient_permissions(app_client_basic_auth): + """Test protected delete collection with reader auhtentication""" + if not os.getenv("BASIC_AUTH"): + pytest.skip() + headers = { + "Authorization": "Basic cmVhZGVyOnJlYWRlcg==" + } # Assuming this is a valid authorization token + + response = await app_client_basic_auth.delete( + "/collections/test-collection", headers=headers + ) + + assert ( + response.status_code == 403 + ) # Expecting a 403 status code for insufficient permissions + assert response.json() == { + "detail": "Insufficient permissions for [DELETE /collections/test-collection]" + } diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index 227509c9..f5e6989e 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -25,6 +25,7 @@ create_collection_index, create_index_templates, ) + from stac_fastapi.opensearch.basic_auth import apply_basic_auth else: from stac_fastapi.elasticsearch.config import ( ElasticsearchSettings as SearchSettings, @@ -35,6 +36,7 @@ create_collection_index, create_index_templates, ) + from stac_fastapi.elasticsearch.basic_auth import apply_basic_auth from stac_fastapi.extensions.core import ( # FieldsExtension, ContextExtension, @@ -222,3 +224,51 @@ async def app_client(app): async with AsyncClient(app=app, base_url="http://test-server") as c: yield c + + +@pytest_asyncio.fixture(scope="session") +async def app_basic_auth(): + settings = AsyncSettings() + extensions = [ + TransactionExtension( + client=TransactionsClient( + database=database, session=None, settings=settings + ), + settings=settings, + ), + ContextExtension(), + SortExtension(), + FieldsExtension(), + QueryExtension(), + TokenPaginationExtension(), + FilterExtension(), + ] + + post_request_model = create_post_request_model(extensions) + + stac_api = StacApi( + settings=settings, + client=CoreClient( + database=database, + session=None, + extensions=extensions, + post_request_model=post_request_model, + ), + extensions=extensions, + search_get_request_model=create_get_request_model(extensions), + search_post_request_model=post_request_model, + ) + + os.environ["BASIC_AUTH"] = '{"public_endpoints":[{"path":"/","method":"GET"},{"path":"/search","method":"GET"}],"users":[{"username":"admin","password":"admin","permissions":"*"},{"username":"reader","password":"reader","permissions":[{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}]}]}' + apply_basic_auth(stac_api) + + return stac_api.app + + +@pytest_asyncio.fixture(scope="session") +async def app_client_basic_auth(app_basic_auth): + await create_index_templates() + await create_collection_index() + + async with AsyncClient(app=app_basic_auth, base_url="http://test-server") as c: + yield c From 2ff9c9fde49d352774ab5e1a0eaaa2b2292637ac Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Fri, 26 Apr 2024 13:47:17 +0100 Subject: [PATCH 02/11] test fixes --- .../{basic_auth.py => test_basic_auth.py} | 0 stac_fastapi/tests/conftest.py | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) rename stac_fastapi/tests/basic_auth/{basic_auth.py => test_basic_auth.py} (100%) diff --git a/stac_fastapi/tests/basic_auth/basic_auth.py b/stac_fastapi/tests/basic_auth/test_basic_auth.py similarity index 100% rename from stac_fastapi/tests/basic_auth/basic_auth.py rename to stac_fastapi/tests/basic_auth/test_basic_auth.py diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index f5e6989e..9031c7c1 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -18,6 +18,7 @@ from stac_fastapi.core.extensions import QueryExtension if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": + from stac_fastapi.opensearch.basic_auth import apply_basic_auth from stac_fastapi.opensearch.config import AsyncOpensearchSettings as AsyncSettings from stac_fastapi.opensearch.config import OpensearchSettings as SearchSettings from stac_fastapi.opensearch.database_logic import ( @@ -25,7 +26,6 @@ create_collection_index, create_index_templates, ) - from stac_fastapi.opensearch.basic_auth import apply_basic_auth else: from stac_fastapi.elasticsearch.config import ( ElasticsearchSettings as SearchSettings, @@ -258,10 +258,34 @@ async def app_basic_auth(): search_get_request_model=create_get_request_model(extensions), search_post_request_model=post_request_model, ) - - os.environ["BASIC_AUTH"] = '{"public_endpoints":[{"path":"/","method":"GET"},{"path":"/search","method":"GET"}],"users":[{"username":"admin","password":"admin","permissions":"*"},{"username":"reader","password":"reader","permissions":[{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}]}]}' + + os.environ[ + "BASIC_AUTH" + ] = """{ + "public_endpoints": [ + {"path": "/", "method": "GET"}, + {"path": "/search", "method": "GET"} + ], + "users": [ + {"username": "admin", "password": "admin", "permissions": "*"}, + { + "username": "reader", "password": "reader", + "permissions": [ + {"path": "/conformance", "method": ["GET"]}, + {"path": "/collections/{collection_id}/items/{item_id}", "method": ["GET"]}, + {"path": "/search", "method": ["POST"]}, + {"path": "/collections", "method": ["GET"]}, + {"path": "/collections/{collection_id}", "method": ["GET"]}, + {"path": "/collections/{collection_id}/items", "method": ["GET"]}, + {"path": "/queryables", "method": ["GET"]}, + {"path": "/queryables/collections/{collection_id}/queryables", "method": ["GET"]}, + {"path": "/_mgmt/ping", "method": ["GET"]} + ] + } + ] + }""" apply_basic_auth(stac_api) - + return stac_api.app From 6c0449de186b1488138b68b718d02ca46c666472 Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Fri, 26 Apr 2024 15:01:22 +0100 Subject: [PATCH 03/11] docker compose examples + test updates + basic_auth module tweaks --- docker-compose.basic_auth_protected.yml | 94 +++++++++++++++++++ docker-compose.basic_auth_public.yml | 94 +++++++++++++++++++ .../stac_fastapi/elasticsearch/basic_auth.py | 13 +-- .../stac_fastapi/opensearch/basic_auth.py | 13 +-- .../tests/basic_auth/test_basic_auth.py | 62 ++++++++++-- 5 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 docker-compose.basic_auth_protected.yml create mode 100644 docker-compose.basic_auth_public.yml diff --git a/docker-compose.basic_auth_protected.yml b/docker-compose.basic_auth_protected.yml new file mode 100644 index 00000000..cedbf154 --- /dev/null +++ b/docker-compose.basic_auth_protected.yml @@ -0,0 +1,94 @@ +version: '3.9' + +services: + app-elasticsearch: + container_name: stac-fastapi-es + image: stac-utils/stac-fastapi-es + restart: always + build: + context: . + dockerfile: dockerfiles/Dockerfile.dev.es + environment: + - STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch + - STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend + - STAC_FASTAPI_VERSION=2.1 + - APP_HOST=0.0.0.0 + - APP_PORT=8080 + - RELOAD=true + - ENVIRONMENT=local + - WEB_CONCURRENCY=10 + - ES_HOST=elasticsearch + - ES_PORT=9200 + - ES_USE_SSL=false + - ES_VERIFY_CERTS=false + - BACKEND=elasticsearch + - BASIC_AUTH={"users":[{"username":"admin","password":"admin","permissions":"*"},{"username":"reader","password":"reader","permissions":[{"path":"/","method":["GET"]},{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["GET","POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}]}]} + ports: + - "8080:8080" + volumes: + - ./stac_fastapi:/app/stac_fastapi + - ./scripts:/app/scripts + - ./esdata:/usr/share/elasticsearch/data + depends_on: + - elasticsearch + command: + bash -c "./scripts/wait-for-it-es.sh es-container:9200 && python -m stac_fastapi.elasticsearch.app" + + app-opensearch: + container_name: stac-fastapi-os + image: stac-utils/stac-fastapi-os + restart: always + build: + context: . + dockerfile: dockerfiles/Dockerfile.dev.os + environment: + - STAC_FASTAPI_TITLE=stac-fastapi-opensearch + - STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend + - STAC_FASTAPI_VERSION=2.1 + - APP_HOST=0.0.0.0 + - APP_PORT=8082 + - RELOAD=true + - ENVIRONMENT=local + - WEB_CONCURRENCY=10 + - ES_HOST=opensearch + - ES_PORT=9202 + - ES_USE_SSL=false + - ES_VERIFY_CERTS=false + - BACKEND=opensearch + - BASIC_AUTH={"users":[{"username":"admin","password":"admin","permissions":"*"},{"username":"reader","password":"reader","permissions":[{"path":"/","method":["GET"]},{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["GET","POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}]}]} + ports: + - "8082:8082" + volumes: + - ./stac_fastapi:/app/stac_fastapi + - ./scripts:/app/scripts + - ./osdata:/usr/share/opensearch/data + depends_on: + - opensearch + command: + bash -c "./scripts/wait-for-it-es.sh os-container:9202 && python -m stac_fastapi.opensearch.app" + + elasticsearch: + container_name: es-container + image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION:-8.11.0} + hostname: elasticsearch + environment: + ES_JAVA_OPTS: -Xms512m -Xmx1g + volumes: + - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml + - ./elasticsearch/snapshots:/usr/share/elasticsearch/snapshots + ports: + - "9200:9200" + + opensearch: + container_name: os-container + image: opensearchproject/opensearch:${OPENSEARCH_VERSION:-2.11.1} + hostname: opensearch + environment: + - discovery.type=single-node + - plugins.security.disabled=true + - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m + volumes: + - ./opensearch/config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml + - ./opensearch/snapshots:/usr/share/opensearch/snapshots + ports: + - "9202:9202" diff --git a/docker-compose.basic_auth_public.yml b/docker-compose.basic_auth_public.yml new file mode 100644 index 00000000..ccac31ac --- /dev/null +++ b/docker-compose.basic_auth_public.yml @@ -0,0 +1,94 @@ +version: '3.9' + +services: + app-elasticsearch: + container_name: stac-fastapi-es + image: stac-utils/stac-fastapi-es + restart: always + build: + context: . + dockerfile: dockerfiles/Dockerfile.dev.es + environment: + - STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch + - STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend + - STAC_FASTAPI_VERSION=2.1 + - APP_HOST=0.0.0.0 + - APP_PORT=8080 + - RELOAD=true + - ENVIRONMENT=local + - WEB_CONCURRENCY=10 + - ES_HOST=elasticsearch + - ES_PORT=9200 + - ES_USE_SSL=false + - ES_VERIFY_CERTS=false + - BACKEND=elasticsearch + - BASIC_AUTH={"public_endpoints":[{"path":"/","method":"GET"},{"path":"/conformance","method":"GET"},{"path":"/collections/{collection_id}/items/{item_id}","method":"GET"},{"path":"/search","method":"GET"},{"path":"/search","method":"POST"},{"path":"/collections","method":"GET"},{"path":"/collections/{collection_id}","method":"GET"},{"path":"/collections/{collection_id}/items","method":"GET"},{"path":"/queryables","method":"GET"},{"path":"/queryables/collections/{collection_id}/queryables","method":"GET"},{"path":"/_mgmt/ping","method":"GET"}],"users":[{"username":"admin","password":"admin","permissions":[{"path":"/","method":["GET"]},{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET","POST","PUT","DELETE"]},{"path":"/search","method":["GET","POST"]},{"path":"/collections","method":["GET","PUT","POST"]},{"path":"/collections/{collection_id}","method":["GET","DELETE"]},{"path":"/collections/{collection_id}/items","method":["GET","POST"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}]}]} + ports: + - "8080:8080" + volumes: + - ./stac_fastapi:/app/stac_fastapi + - ./scripts:/app/scripts + - ./esdata:/usr/share/elasticsearch/data + depends_on: + - elasticsearch + command: + bash -c "./scripts/wait-for-it-es.sh es-container:9200 && python -m stac_fastapi.elasticsearch.app" + + app-opensearch: + container_name: stac-fastapi-os + image: stac-utils/stac-fastapi-os + restart: always + build: + context: . + dockerfile: dockerfiles/Dockerfile.dev.os + environment: + - STAC_FASTAPI_TITLE=stac-fastapi-opensearch + - STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend + - STAC_FASTAPI_VERSION=2.1 + - APP_HOST=0.0.0.0 + - APP_PORT=8082 + - RELOAD=true + - ENVIRONMENT=local + - WEB_CONCURRENCY=10 + - ES_HOST=opensearch + - ES_PORT=9202 + - ES_USE_SSL=false + - ES_VERIFY_CERTS=false + - BACKEND=opensearch + - BASIC_AUTH={"public_endpoints":[{"path":"/","method":"GET"},{"path":"/conformance","method":"GET"},{"path":"/collections/{collection_id}/items/{item_id}","method":"GET"},{"path":"/search","method":"GET"},{"path":"/search","method":"POST"},{"path":"/collections","method":"GET"},{"path":"/collections/{collection_id}","method":"GET"},{"path":"/collections/{collection_id}/items","method":"GET"},{"path":"/queryables","method":"GET"},{"path":"/queryables/collections/{collection_id}/queryables","method":"GET"},{"path":"/_mgmt/ping","method":"GET"}],"users":[{"username":"admin","password":"admin","permissions":[{"path":"/","method":["GET"]},{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET","POST","PUT","DELETE"]},{"path":"/search","method":["GET","POST"]},{"path":"/collections","method":["GET","PUT","POST"]},{"path":"/collections/{collection_id}","method":["GET","DELETE"]},{"path":"/collections/{collection_id}/items","method":["GET","POST"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}]}]} + ports: + - "8082:8082" + volumes: + - ./stac_fastapi:/app/stac_fastapi + - ./scripts:/app/scripts + - ./osdata:/usr/share/opensearch/data + depends_on: + - opensearch + command: + bash -c "./scripts/wait-for-it-es.sh os-container:9202 && python -m stac_fastapi.opensearch.app" + + elasticsearch: + container_name: es-container + image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION:-8.11.0} + hostname: elasticsearch + environment: + ES_JAVA_OPTS: -Xms512m -Xmx1g + volumes: + - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml + - ./elasticsearch/snapshots:/usr/share/elasticsearch/snapshots + ports: + - "9200:9200" + + opensearch: + container_name: os-container + image: opensearchproject/opensearch:${OPENSEARCH_VERSION:-2.11.1} + hostname: opensearch + environment: + - discovery.type=single-node + - plugins.security.disabled=true + - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m + volumes: + - ./opensearch/config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml + - ./opensearch/snapshots:/usr/share/opensearch/snapshots + ports: + - "9202:9202" diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py index ccff3c1d..a1c59184 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py @@ -1,6 +1,7 @@ """Basic Authentication Module.""" import json +import logging import os import secrets from typing import Any, Dict @@ -12,13 +13,13 @@ from stac_fastapi.api.app import StacApi -security = HTTPBasic() - +_SECURITY = HTTPBasic() +_LOGGER = logging.getLogger("uvicorn.default") _BASIC_AUTH: Dict[str, Any] = {} def has_access( - request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(security)] + request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(_SECURITY)] ) -> str: """Check if the provided credentials match the expected \ username and password stored in environment variables for basic authentication. @@ -90,13 +91,13 @@ def apply_basic_auth(api: StacApi) -> None: basic_auth_json_str = os.environ.get("BASIC_AUTH") if not basic_auth_json_str: - print("Basic authentication disabled.") + _LOGGER.info("Basic authentication disabled.") return try: _BASIC_AUTH = json.loads(basic_auth_json_str) except json.JSONDecodeError as exception: - print(f"Invalid JSON format for BASIC_AUTH. {exception=}") + _LOGGER.error(f"Invalid JSON format for BASIC_AUTH. {exception=}") raise public_endpoints = _BASIC_AUTH.get("public_endpoints", []) users = _BASIC_AUTH.get("users") @@ -111,4 +112,4 @@ def apply_basic_auth(api: StacApi) -> None: if endpoint not in public_endpoints: api.add_route_dependencies([endpoint], [Depends(has_access)]) - print("Basic authentication enabled.") + _LOGGER.info("Basic authentication enabled.") diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py index ccff3c1d..a1c59184 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py @@ -1,6 +1,7 @@ """Basic Authentication Module.""" import json +import logging import os import secrets from typing import Any, Dict @@ -12,13 +13,13 @@ from stac_fastapi.api.app import StacApi -security = HTTPBasic() - +_SECURITY = HTTPBasic() +_LOGGER = logging.getLogger("uvicorn.default") _BASIC_AUTH: Dict[str, Any] = {} def has_access( - request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(security)] + request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(_SECURITY)] ) -> str: """Check if the provided credentials match the expected \ username and password stored in environment variables for basic authentication. @@ -90,13 +91,13 @@ def apply_basic_auth(api: StacApi) -> None: basic_auth_json_str = os.environ.get("BASIC_AUTH") if not basic_auth_json_str: - print("Basic authentication disabled.") + _LOGGER.info("Basic authentication disabled.") return try: _BASIC_AUTH = json.loads(basic_auth_json_str) except json.JSONDecodeError as exception: - print(f"Invalid JSON format for BASIC_AUTH. {exception=}") + _LOGGER.error(f"Invalid JSON format for BASIC_AUTH. {exception=}") raise public_endpoints = _BASIC_AUTH.get("public_endpoints", []) users = _BASIC_AUTH.get("users") @@ -111,4 +112,4 @@ def apply_basic_auth(api: StacApi) -> None: if endpoint not in public_endpoints: api.add_route_dependencies([endpoint], [Depends(has_access)]) - print("Basic authentication enabled.") + _LOGGER.info("Basic authentication enabled.") diff --git a/stac_fastapi/tests/basic_auth/test_basic_auth.py b/stac_fastapi/tests/basic_auth/test_basic_auth.py index ad804247..b8939b05 100644 --- a/stac_fastapi/tests/basic_auth/test_basic_auth.py +++ b/stac_fastapi/tests/basic_auth/test_basic_auth.py @@ -12,7 +12,7 @@ async def test_get_search_not_authenticated(app_client_basic_auth, ctx): response = await app_client_basic_auth.get("/search", params=params) - assert response.status_code == 200 + assert response.status_code == 200, response assert response.json()["features"][0]["geometry"] == ctx.item["geometry"] @@ -26,26 +26,68 @@ async def test_post_search_authenticated(app_client_basic_auth, ctx): response = await app_client_basic_auth.post("/search", json=params, headers=headers) - assert response.status_code == 200 + assert response.status_code == 200, response assert response.json()["features"][0]["geometry"] == ctx.item["geometry"] @pytest.mark.asyncio -async def test_delete_resource_insufficient_permissions(app_client_basic_auth): +async def test_delete_resource_anonymous( + app_client_basic_auth, +): + """Test protected delete collection without auhtentication""" + if not os.getenv("BASIC_AUTH"): + pytest.skip() + + response = await app_client_basic_auth.delete("/collections/test-collection") + + assert response.status_code == 401 + assert response.json() == {"detail": "Not authenticated"} + + +@pytest.mark.asyncio +async def test_delete_resource_invalid_credentials(app_client_basic_auth, ctx): + """Test protected delete collection with admin auhtentication""" + if not os.getenv("BASIC_AUTH"): + pytest.skip() + + headers = {"Authorization": "Basic YWRtaW46cGFzc3dvcmQ="} + + response = await app_client_basic_auth.delete( + f"/collections/{ctx.collection['id']}", headers=headers + ) + + assert response.status_code == 401 + assert response.json() == {"detail": "Incorrect username or password"} + + +@pytest.mark.asyncio +async def test_delete_resource_insufficient_permissions(app_client_basic_auth, ctx): """Test protected delete collection with reader auhtentication""" if not os.getenv("BASIC_AUTH"): pytest.skip() - headers = { - "Authorization": "Basic cmVhZGVyOnJlYWRlcg==" - } # Assuming this is a valid authorization token + + headers = {"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="} response = await app_client_basic_auth.delete( - "/collections/test-collection", headers=headers + f"/collections/{ctx.collection['id']}", headers=headers ) - assert ( - response.status_code == 403 - ) # Expecting a 403 status code for insufficient permissions + assert response.status_code == 403 assert response.json() == { "detail": "Insufficient permissions for [DELETE /collections/test-collection]" } + + +@pytest.mark.asyncio +async def test_delete_resource_sufficient_permissions(app_client_basic_auth, ctx): + """Test protected delete collection with admin auhtentication""" + if not os.getenv("BASIC_AUTH"): + pytest.skip() + + headers = {"Authorization": "Basic YWRtaW46YWRtaW4="} + + response = await app_client_basic_auth.delete( + f"/collections/{ctx.collection['id']}", headers=headers + ) + + assert response.status_code == 204 From 86bb5ef0a7ce45753e5fc7255aad967bfe781b85 Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Fri, 26 Apr 2024 15:02:43 +0100 Subject: [PATCH 04/11] docs --- CHANGELOG.md | 2 ++ README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4aab32..fd80caa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +- Added option to include Basic Auth. + ## [v2.3.0] ### Changed diff --git a/README.md b/README.md index edce25a4..78b2e599 100644 --- a/README.md +++ b/README.md @@ -272,3 +272,83 @@ curl -X "POST" "http://localhost:9200/_aliases" \ ``` The modified Items with lowercase identifiers will now be visible to users accessing `my-collection` in the STAC API. + + +## Basic Auth + +#### Environment Variable Configuration + +Basic authentication is an optional feature. You can enable it by setting the environment variable `BASIC_AUTH` as a JSON string. + +Example: +``` +BASIC_AUTH={"users":[{"username":"user","password":"pass","permissions":"*"}]} +``` + +### User Permissions Configuration + +In order to set endpoints with specific access permissions, you can configure the `users` key with a list of user objects. Each user object should contain the username, password, and their respective permissions. + +Example: This example illustrates the configuration for two users: an **admin** user with full permissions (*) and a **reader** user with limited permissions to specific read-only endpoints. +```json +{ + "users": [ + { + "username": "admin", + "password": "admin", + "permissions": "*" + }, + { + "username": "reader", + "password": "reader", + "permissions": [ + {"path": "/", "method": ["GET"]}, + {"path": "/conformance", "method": ["GET"]}, + {"path": "/collections/{collection_id}/items/{item_id}", "method": ["GET"]}, + {"path": "/search", "method": ["GET", "POST"]}, + {"path": "/collections", "method": ["GET"]}, + {"path": "/collections/{collection_id}", "method": ["GET"]}, + {"path": "/collections/{collection_id}/items", "method": ["GET"]}, + {"path": "/queryables", "method": ["GET"]}, + {"path": "/queryables/collections/{collection_id}/queryables", "method": ["GET"]}, + {"path": "/_mgmt/ping", "method": ["GET"]} + ] + } + ] +} +``` + + +### Public Endpoints Configuration + +In order to set endpoints with public access, you can configure the public_endpoints key with a list of endpoint objects. Each endpoint object should specify the path and method of the endpoint. + +Example: This example demonstrates the configuration for public endpoints, allowing access without authentication to read-only endpoints. +```json +{ + "public_endpoints": [ + {"path": "/", "method": "GET"}, + {"path": "/conformance", "method": "GET"}, + {"path": "/collections/{collection_id}/items/{item_id}", "method": "GET"}, + {"path": "/search", "method": "GET"}, + {"path": "/search", "method": "POST"}, + {"path": "/collections", "method": "GET"}, + {"path": "/collections/{collection_id}", "method": "GET"}, + {"path": "/collections/{collection_id}/items", "method": "GET"}, + {"path": "/queryables", "method": "GET"}, + {"path": "/queryables/collections/{collection_id}/queryables", "method": "GET"}, + {"path": "/_mgmt/ping", "method": "GET"} + ], + "users": [ + { + "username": "admin", + "password": "admin", + "permissions": "*" + } + ] +} +``` + +### Basic Authentication Configurations + +See `docker-compose.basic_auth_protected.yml` and `docker-compose.basic_auth_public.yml` for basic authentication configurations. \ No newline at end of file From de4a48641e02a99fc5cc7ecbd0023187b520074b Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Fri, 26 Apr 2024 15:07:21 +0100 Subject: [PATCH 05/11] docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 78b2e599..3723f2f9 100644 --- a/README.md +++ b/README.md @@ -349,6 +349,6 @@ Example: This example demonstrates the configuration for public endpoints, allow } ``` -### Basic Authentication Configurations +### Docker Compose Configurations See `docker-compose.basic_auth_protected.yml` and `docker-compose.basic_auth_public.yml` for basic authentication configurations. \ No newline at end of file From be9591a043ce55e69d6021a4d33e365bd2082b20 Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Fri, 26 Apr 2024 16:10:02 +0100 Subject: [PATCH 06/11] moved basic_auth.py to core --- .../stac_fastapi/core}/basic_auth.py | 0 .../stac_fastapi/elasticsearch/app.py | 2 +- .../opensearch/stac_fastapi/opensearch/app.py | 2 +- .../stac_fastapi/opensearch/basic_auth.py | 115 ------------------ stac_fastapi/tests/conftest.py | 3 +- 5 files changed, 3 insertions(+), 119 deletions(-) rename stac_fastapi/{elasticsearch/stac_fastapi/elasticsearch => core/stac_fastapi/core}/basic_auth.py (100%) delete mode 100644 stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py b/stac_fastapi/core/stac_fastapi/core/basic_auth.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py rename to stac_fastapi/core/stac_fastapi/core/basic_auth.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index c7aa9b84..95ee913b 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -12,7 +12,7 @@ ) from stac_fastapi.core.extensions import QueryExtension from stac_fastapi.core.session import Session -from stac_fastapi.elasticsearch.basic_auth import apply_basic_auth +from stac_fastapi.core.basic_auth import apply_basic_auth from stac_fastapi.elasticsearch.config import ElasticsearchSettings from stac_fastapi.elasticsearch.database_logic import ( DatabaseLogic, diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py index e3c95733..57f5b02b 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py @@ -21,7 +21,7 @@ TransactionExtension, ) from stac_fastapi.extensions.third_party import BulkTransactionExtension -from stac_fastapi.opensearch.basic_auth import apply_basic_auth +from stac_fastapi.core.basic_auth import apply_basic_auth from stac_fastapi.opensearch.config import OpensearchSettings from stac_fastapi.opensearch.database_logic import ( DatabaseLogic, diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py deleted file mode 100644 index a1c59184..00000000 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Basic Authentication Module.""" - -import json -import logging -import os -import secrets -from typing import Any, Dict - -from fastapi import Depends, HTTPException, Request, status -from fastapi.routing import APIRoute -from fastapi.security import HTTPBasic, HTTPBasicCredentials -from typing_extensions import Annotated - -from stac_fastapi.api.app import StacApi - -_SECURITY = HTTPBasic() -_LOGGER = logging.getLogger("uvicorn.default") -_BASIC_AUTH: Dict[str, Any] = {} - - -def has_access( - request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(_SECURITY)] -) -> str: - """Check if the provided credentials match the expected \ - username and password stored in environment variables for basic authentication. - - Args: - request (Request): The FastAPI request object. - credentials (HTTPBasicCredentials): The HTTP basic authentication credentials. - - Returns: - str: The username if authentication is successful. - - Raises: - HTTPException: If authentication fails due to incorrect username or password. - """ - global _BASIC_AUTH - - users = _BASIC_AUTH.get("users") - user: Dict[str, Any] = next( - (u for u in users if u.get("username") == credentials.username), {} - ) - - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Basic"}, - ) - - # Compare the provided username and password with the correct ones using compare_digest - if not secrets.compare_digest( - credentials.username.encode("utf-8"), user.get("username").encode("utf-8") - ) or not secrets.compare_digest( - credentials.password.encode("utf-8"), user.get("password").encode("utf-8") - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Basic"}, - ) - - permissions = user.get("permissions", []) - path = request.url.path - method = request.method - - if permissions == "*": - return credentials.username - for permission in permissions: - if permission["path"] == path and method in permission.get("method", []): - return credentials.username - - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=f"Insufficient permissions for [{method} {path}]", - ) - - -def apply_basic_auth(api: StacApi) -> None: - """Apply basic authentication to the provided FastAPI application \ - based on environment variables for username, password, and endpoints. - - Args: - api (StacApi): The FastAPI application. - - Raises: - HTTPException: If there are issues with the configuration or format - of the environment variables. - """ - global _BASIC_AUTH - - basic_auth_json_str = os.environ.get("BASIC_AUTH") - if not basic_auth_json_str: - _LOGGER.info("Basic authentication disabled.") - return - - try: - _BASIC_AUTH = json.loads(basic_auth_json_str) - except json.JSONDecodeError as exception: - _LOGGER.error(f"Invalid JSON format for BASIC_AUTH. {exception=}") - raise - public_endpoints = _BASIC_AUTH.get("public_endpoints", []) - users = _BASIC_AUTH.get("users") - if not users: - raise Exception("Invalid JSON format for BASIC_AUTH. Key 'users' undefined.") - - app = api.app - for route in app.routes: - if isinstance(route, APIRoute): - for method in route.methods: - endpoint = {"path": route.path, "method": method} - if endpoint not in public_endpoints: - api.add_route_dependencies([endpoint], [Depends(has_access)]) - - _LOGGER.info("Basic authentication enabled.") diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index 9031c7c1..7eb506bb 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -16,9 +16,9 @@ TransactionsClient, ) from stac_fastapi.core.extensions import QueryExtension +from stac_fastapi.core.basic_auth import apply_basic_auth if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": - from stac_fastapi.opensearch.basic_auth import apply_basic_auth from stac_fastapi.opensearch.config import AsyncOpensearchSettings as AsyncSettings from stac_fastapi.opensearch.config import OpensearchSettings as SearchSettings from stac_fastapi.opensearch.database_logic import ( @@ -36,7 +36,6 @@ create_collection_index, create_index_templates, ) - from stac_fastapi.elasticsearch.basic_auth import apply_basic_auth from stac_fastapi.extensions.core import ( # FieldsExtension, ContextExtension, From 217ff80215275747faf26a9349c66372e8ad6127 Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Fri, 26 Apr 2024 16:13:25 +0100 Subject: [PATCH 07/11] pre-commit run --all-files --- stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py | 2 +- stac_fastapi/opensearch/stac_fastapi/opensearch/app.py | 2 +- stac_fastapi/tests/conftest.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 95ee913b..49d199d6 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -4,6 +4,7 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model +from stac_fastapi.core.basic_auth import apply_basic_auth from stac_fastapi.core.core import ( BulkTransactionsClient, CoreClient, @@ -12,7 +13,6 @@ ) from stac_fastapi.core.extensions import QueryExtension from stac_fastapi.core.session import Session -from stac_fastapi.core.basic_auth import apply_basic_auth from stac_fastapi.elasticsearch.config import ElasticsearchSettings from stac_fastapi.elasticsearch.database_logic import ( DatabaseLogic, diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py index 57f5b02b..ac697b2b 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py @@ -4,6 +4,7 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model +from stac_fastapi.core.basic_auth import apply_basic_auth from stac_fastapi.core.core import ( BulkTransactionsClient, CoreClient, @@ -21,7 +22,6 @@ TransactionExtension, ) from stac_fastapi.extensions.third_party import BulkTransactionExtension -from stac_fastapi.core.basic_auth import apply_basic_auth from stac_fastapi.opensearch.config import OpensearchSettings from stac_fastapi.opensearch.database_logic import ( DatabaseLogic, diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index 7eb506bb..bac8fd24 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -10,13 +10,13 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model +from stac_fastapi.core.basic_auth import apply_basic_auth from stac_fastapi.core.core import ( BulkTransactionsClient, CoreClient, TransactionsClient, ) from stac_fastapi.core.extensions import QueryExtension -from stac_fastapi.core.basic_auth import apply_basic_auth if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": from stac_fastapi.opensearch.config import AsyncOpensearchSettings as AsyncSettings From 4b7cfe1ba42c4e4cf5662d341700fa7774c14480 Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Fri, 26 Apr 2024 20:06:56 +0100 Subject: [PATCH 08/11] tweak --- stac_fastapi/core/stac_fastapi/core/basic_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/core/stac_fastapi/core/basic_auth.py b/stac_fastapi/core/stac_fastapi/core/basic_auth.py index a1c59184..c504978d 100644 --- a/stac_fastapi/core/stac_fastapi/core/basic_auth.py +++ b/stac_fastapi/core/stac_fastapi/core/basic_auth.py @@ -13,8 +13,8 @@ from stac_fastapi.api.app import StacApi -_SECURITY = HTTPBasic() _LOGGER = logging.getLogger("uvicorn.default") +_SECURITY = HTTPBasic() _BASIC_AUTH: Dict[str, Any] = {} From c3408cff0dc6c89ffd657778764351cdff593f00 Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Sun, 28 Apr 2024 18:39:49 +0100 Subject: [PATCH 09/11] fixed docstrings on tests --- stac_fastapi/tests/basic_auth/test_basic_auth.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stac_fastapi/tests/basic_auth/test_basic_auth.py b/stac_fastapi/tests/basic_auth/test_basic_auth.py index b8939b05..46ed1648 100644 --- a/stac_fastapi/tests/basic_auth/test_basic_auth.py +++ b/stac_fastapi/tests/basic_auth/test_basic_auth.py @@ -5,7 +5,7 @@ @pytest.mark.asyncio async def test_get_search_not_authenticated(app_client_basic_auth, ctx): - """Test public endpoint search without authentication""" + """Test public endpoint [GET /search] without authentication""" if not os.getenv("BASIC_AUTH"): pytest.skip() params = {"id": ctx.item["id"]} @@ -18,7 +18,7 @@ async def test_get_search_not_authenticated(app_client_basic_auth, ctx): @pytest.mark.asyncio async def test_post_search_authenticated(app_client_basic_auth, ctx): - """Test protected post search with reader auhtentication""" + """Test protected endpoint [POST /search] with reader auhtentication""" if not os.getenv("BASIC_AUTH"): pytest.skip() params = {"id": ctx.item["id"]} @@ -34,7 +34,7 @@ async def test_post_search_authenticated(app_client_basic_auth, ctx): async def test_delete_resource_anonymous( app_client_basic_auth, ): - """Test protected delete collection without auhtentication""" + """Test protected endpoint [DELETE /collections/{collection_id}] without auhtentication""" if not os.getenv("BASIC_AUTH"): pytest.skip() @@ -46,7 +46,7 @@ async def test_delete_resource_anonymous( @pytest.mark.asyncio async def test_delete_resource_invalid_credentials(app_client_basic_auth, ctx): - """Test protected delete collection with admin auhtentication""" + """Test protected endpoint [DELETE /collections/{collection_id}] with invalid credentials""" if not os.getenv("BASIC_AUTH"): pytest.skip() @@ -62,7 +62,7 @@ async def test_delete_resource_invalid_credentials(app_client_basic_auth, ctx): @pytest.mark.asyncio async def test_delete_resource_insufficient_permissions(app_client_basic_auth, ctx): - """Test protected delete collection with reader auhtentication""" + """Test protected endpoint [DELETE /collections/{collection_id}] with reader user which has insufficient permissions""" if not os.getenv("BASIC_AUTH"): pytest.skip() @@ -80,7 +80,7 @@ async def test_delete_resource_insufficient_permissions(app_client_basic_auth, c @pytest.mark.asyncio async def test_delete_resource_sufficient_permissions(app_client_basic_auth, ctx): - """Test protected delete collection with admin auhtentication""" + """Test protected endpoint [DELETE /collections/{collection_id}] with admin user auhtentication which has sufficient permissions""" if not os.getenv("BASIC_AUTH"): pytest.skip() From 345a841cb9049c5e32d13095b0d2d4681613d3ba Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Sun, 28 Apr 2024 19:22:33 +0100 Subject: [PATCH 10/11] . --- stac_fastapi/tests/basic_auth/test_basic_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/tests/basic_auth/test_basic_auth.py b/stac_fastapi/tests/basic_auth/test_basic_auth.py index 46ed1648..0515364b 100644 --- a/stac_fastapi/tests/basic_auth/test_basic_auth.py +++ b/stac_fastapi/tests/basic_auth/test_basic_auth.py @@ -80,7 +80,7 @@ async def test_delete_resource_insufficient_permissions(app_client_basic_auth, c @pytest.mark.asyncio async def test_delete_resource_sufficient_permissions(app_client_basic_auth, ctx): - """Test protected endpoint [DELETE /collections/{collection_id}] with admin user auhtentication which has sufficient permissions""" + """Test protected endpoint [DELETE /collections/{collection_id}] with admin user which has sufficient permissions""" if not os.getenv("BASIC_AUTH"): pytest.skip() From 18029a0267d3013419dca7789bfda3ad6b84fa64 Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Tue, 30 Apr 2024 16:02:56 +0100 Subject: [PATCH 11/11] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd80caa9..0d976b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] -- Added option to include Basic Auth. +- Added option to include Basic Auth [#232](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/232) ## [v2.3.0]