diff --git a/CHANGELOG.md b/CHANGELOG.md index 9deead23..85ce856f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Added option to include Basic Auth [#232](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/232) + ### Fixed - Fixed `POST /collections/test-collection/items` returning an item with an empty links array [#236](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/236) diff --git a/README.md b/README.md index edce25a4..3723f2f9 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": "*" + } + ] +} +``` + +### 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 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/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/core/stac_fastapi/core/basic_auth.py b/stac_fastapi/core/stac_fastapi/core/basic_auth.py new file mode 100644 index 00000000..c504978d --- /dev/null +++ b/stac_fastapi/core/stac_fastapi/core/basic_auth.py @@ -0,0 +1,115 @@ +"""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 + +_LOGGER = logging.getLogger("uvicorn.default") +_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: + _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/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 6d189179..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, @@ -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/app.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py index a91e9a86..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, @@ -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/tests/basic_auth/test_basic_auth.py b/stac_fastapi/tests/basic_auth/test_basic_auth.py new file mode 100644 index 00000000..0515364b --- /dev/null +++ b/stac_fastapi/tests/basic_auth/test_basic_auth.py @@ -0,0 +1,93 @@ +import os + +import pytest + + +@pytest.mark.asyncio +async def test_get_search_not_authenticated(app_client_basic_auth, ctx): + """Test public endpoint [GET /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, response + 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 endpoint [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, response + assert response.json()["features"][0]["geometry"] == ctx.item["geometry"] + + +@pytest.mark.asyncio +async def test_delete_resource_anonymous( + app_client_basic_auth, +): + """Test protected endpoint [DELETE /collections/{collection_id}] 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 endpoint [DELETE /collections/{collection_id}] with invalid credentials""" + 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 endpoint [DELETE /collections/{collection_id}] with reader user which has insufficient permissions""" + if not os.getenv("BASIC_AUTH"): + pytest.skip() + + headers = {"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="} + + response = await app_client_basic_auth.delete( + f"/collections/{ctx.collection['id']}", headers=headers + ) + + 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 endpoint [DELETE /collections/{collection_id}] with admin user which has sufficient permissions""" + 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 diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index 227509c9..bac8fd24 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -10,6 +10,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, @@ -222,3 +223,75 @@ 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