Skip to content

Commit 6c0449d

Browse files
committed
docker compose examples + test updates + basic_auth module tweaks
1 parent 2ff9c9f commit 6c0449d

File tree

5 files changed

+254
-22
lines changed

5 files changed

+254
-22
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
version: '3.9'
2+
3+
services:
4+
app-elasticsearch:
5+
container_name: stac-fastapi-es
6+
image: stac-utils/stac-fastapi-es
7+
restart: always
8+
build:
9+
context: .
10+
dockerfile: dockerfiles/Dockerfile.dev.es
11+
environment:
12+
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
13+
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
14+
- STAC_FASTAPI_VERSION=2.1
15+
- APP_HOST=0.0.0.0
16+
- APP_PORT=8080
17+
- RELOAD=true
18+
- ENVIRONMENT=local
19+
- WEB_CONCURRENCY=10
20+
- ES_HOST=elasticsearch
21+
- ES_PORT=9200
22+
- ES_USE_SSL=false
23+
- ES_VERIFY_CERTS=false
24+
- BACKEND=elasticsearch
25+
- 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"]}]}]}
26+
ports:
27+
- "8080:8080"
28+
volumes:
29+
- ./stac_fastapi:/app/stac_fastapi
30+
- ./scripts:/app/scripts
31+
- ./esdata:/usr/share/elasticsearch/data
32+
depends_on:
33+
- elasticsearch
34+
command:
35+
bash -c "./scripts/wait-for-it-es.sh es-container:9200 && python -m stac_fastapi.elasticsearch.app"
36+
37+
app-opensearch:
38+
container_name: stac-fastapi-os
39+
image: stac-utils/stac-fastapi-os
40+
restart: always
41+
build:
42+
context: .
43+
dockerfile: dockerfiles/Dockerfile.dev.os
44+
environment:
45+
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
46+
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
47+
- STAC_FASTAPI_VERSION=2.1
48+
- APP_HOST=0.0.0.0
49+
- APP_PORT=8082
50+
- RELOAD=true
51+
- ENVIRONMENT=local
52+
- WEB_CONCURRENCY=10
53+
- ES_HOST=opensearch
54+
- ES_PORT=9202
55+
- ES_USE_SSL=false
56+
- ES_VERIFY_CERTS=false
57+
- BACKEND=opensearch
58+
- 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"]}]}]}
59+
ports:
60+
- "8082:8082"
61+
volumes:
62+
- ./stac_fastapi:/app/stac_fastapi
63+
- ./scripts:/app/scripts
64+
- ./osdata:/usr/share/opensearch/data
65+
depends_on:
66+
- opensearch
67+
command:
68+
bash -c "./scripts/wait-for-it-es.sh os-container:9202 && python -m stac_fastapi.opensearch.app"
69+
70+
elasticsearch:
71+
container_name: es-container
72+
image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION:-8.11.0}
73+
hostname: elasticsearch
74+
environment:
75+
ES_JAVA_OPTS: -Xms512m -Xmx1g
76+
volumes:
77+
- ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
78+
- ./elasticsearch/snapshots:/usr/share/elasticsearch/snapshots
79+
ports:
80+
- "9200:9200"
81+
82+
opensearch:
83+
container_name: os-container
84+
image: opensearchproject/opensearch:${OPENSEARCH_VERSION:-2.11.1}
85+
hostname: opensearch
86+
environment:
87+
- discovery.type=single-node
88+
- plugins.security.disabled=true
89+
- OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
90+
volumes:
91+
- ./opensearch/config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml
92+
- ./opensearch/snapshots:/usr/share/opensearch/snapshots
93+
ports:
94+
- "9202:9202"

docker-compose.basic_auth_public.yml

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
version: '3.9'
2+
3+
services:
4+
app-elasticsearch:
5+
container_name: stac-fastapi-es
6+
image: stac-utils/stac-fastapi-es
7+
restart: always
8+
build:
9+
context: .
10+
dockerfile: dockerfiles/Dockerfile.dev.es
11+
environment:
12+
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
13+
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
14+
- STAC_FASTAPI_VERSION=2.1
15+
- APP_HOST=0.0.0.0
16+
- APP_PORT=8080
17+
- RELOAD=true
18+
- ENVIRONMENT=local
19+
- WEB_CONCURRENCY=10
20+
- ES_HOST=elasticsearch
21+
- ES_PORT=9200
22+
- ES_USE_SSL=false
23+
- ES_VERIFY_CERTS=false
24+
- BACKEND=elasticsearch
25+
- 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"]}]}]}
26+
ports:
27+
- "8080:8080"
28+
volumes:
29+
- ./stac_fastapi:/app/stac_fastapi
30+
- ./scripts:/app/scripts
31+
- ./esdata:/usr/share/elasticsearch/data
32+
depends_on:
33+
- elasticsearch
34+
command:
35+
bash -c "./scripts/wait-for-it-es.sh es-container:9200 && python -m stac_fastapi.elasticsearch.app"
36+
37+
app-opensearch:
38+
container_name: stac-fastapi-os
39+
image: stac-utils/stac-fastapi-os
40+
restart: always
41+
build:
42+
context: .
43+
dockerfile: dockerfiles/Dockerfile.dev.os
44+
environment:
45+
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
46+
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
47+
- STAC_FASTAPI_VERSION=2.1
48+
- APP_HOST=0.0.0.0
49+
- APP_PORT=8082
50+
- RELOAD=true
51+
- ENVIRONMENT=local
52+
- WEB_CONCURRENCY=10
53+
- ES_HOST=opensearch
54+
- ES_PORT=9202
55+
- ES_USE_SSL=false
56+
- ES_VERIFY_CERTS=false
57+
- BACKEND=opensearch
58+
- 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"]}]}]}
59+
ports:
60+
- "8082:8082"
61+
volumes:
62+
- ./stac_fastapi:/app/stac_fastapi
63+
- ./scripts:/app/scripts
64+
- ./osdata:/usr/share/opensearch/data
65+
depends_on:
66+
- opensearch
67+
command:
68+
bash -c "./scripts/wait-for-it-es.sh os-container:9202 && python -m stac_fastapi.opensearch.app"
69+
70+
elasticsearch:
71+
container_name: es-container
72+
image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION:-8.11.0}
73+
hostname: elasticsearch
74+
environment:
75+
ES_JAVA_OPTS: -Xms512m -Xmx1g
76+
volumes:
77+
- ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
78+
- ./elasticsearch/snapshots:/usr/share/elasticsearch/snapshots
79+
ports:
80+
- "9200:9200"
81+
82+
opensearch:
83+
container_name: os-container
84+
image: opensearchproject/opensearch:${OPENSEARCH_VERSION:-2.11.1}
85+
hostname: opensearch
86+
environment:
87+
- discovery.type=single-node
88+
- plugins.security.disabled=true
89+
- OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
90+
volumes:
91+
- ./opensearch/config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml
92+
- ./opensearch/snapshots:/usr/share/opensearch/snapshots
93+
ports:
94+
- "9202:9202"

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/basic_auth.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Basic Authentication Module."""
22

33
import json
4+
import logging
45
import os
56
import secrets
67
from typing import Any, Dict
@@ -12,13 +13,13 @@
1213

1314
from stac_fastapi.api.app import StacApi
1415

15-
security = HTTPBasic()
16-
16+
_SECURITY = HTTPBasic()
17+
_LOGGER = logging.getLogger("uvicorn.default")
1718
_BASIC_AUTH: Dict[str, Any] = {}
1819

1920

2021
def has_access(
21-
request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(security)]
22+
request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(_SECURITY)]
2223
) -> str:
2324
"""Check if the provided credentials match the expected \
2425
username and password stored in environment variables for basic authentication.
@@ -90,13 +91,13 @@ def apply_basic_auth(api: StacApi) -> None:
9091

9192
basic_auth_json_str = os.environ.get("BASIC_AUTH")
9293
if not basic_auth_json_str:
93-
print("Basic authentication disabled.")
94+
_LOGGER.info("Basic authentication disabled.")
9495
return
9596

9697
try:
9798
_BASIC_AUTH = json.loads(basic_auth_json_str)
9899
except json.JSONDecodeError as exception:
99-
print(f"Invalid JSON format for BASIC_AUTH. {exception=}")
100+
_LOGGER.error(f"Invalid JSON format for BASIC_AUTH. {exception=}")
100101
raise
101102
public_endpoints = _BASIC_AUTH.get("public_endpoints", [])
102103
users = _BASIC_AUTH.get("users")
@@ -111,4 +112,4 @@ def apply_basic_auth(api: StacApi) -> None:
111112
if endpoint not in public_endpoints:
112113
api.add_route_dependencies([endpoint], [Depends(has_access)])
113114

114-
print("Basic authentication enabled.")
115+
_LOGGER.info("Basic authentication enabled.")

stac_fastapi/opensearch/stac_fastapi/opensearch/basic_auth.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Basic Authentication Module."""
22

33
import json
4+
import logging
45
import os
56
import secrets
67
from typing import Any, Dict
@@ -12,13 +13,13 @@
1213

1314
from stac_fastapi.api.app import StacApi
1415

15-
security = HTTPBasic()
16-
16+
_SECURITY = HTTPBasic()
17+
_LOGGER = logging.getLogger("uvicorn.default")
1718
_BASIC_AUTH: Dict[str, Any] = {}
1819

1920

2021
def has_access(
21-
request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(security)]
22+
request: Request, credentials: Annotated[HTTPBasicCredentials, Depends(_SECURITY)]
2223
) -> str:
2324
"""Check if the provided credentials match the expected \
2425
username and password stored in environment variables for basic authentication.
@@ -90,13 +91,13 @@ def apply_basic_auth(api: StacApi) -> None:
9091

9192
basic_auth_json_str = os.environ.get("BASIC_AUTH")
9293
if not basic_auth_json_str:
93-
print("Basic authentication disabled.")
94+
_LOGGER.info("Basic authentication disabled.")
9495
return
9596

9697
try:
9798
_BASIC_AUTH = json.loads(basic_auth_json_str)
9899
except json.JSONDecodeError as exception:
99-
print(f"Invalid JSON format for BASIC_AUTH. {exception=}")
100+
_LOGGER.error(f"Invalid JSON format for BASIC_AUTH. {exception=}")
100101
raise
101102
public_endpoints = _BASIC_AUTH.get("public_endpoints", [])
102103
users = _BASIC_AUTH.get("users")
@@ -111,4 +112,4 @@ def apply_basic_auth(api: StacApi) -> None:
111112
if endpoint not in public_endpoints:
112113
api.add_route_dependencies([endpoint], [Depends(has_access)])
113114

114-
print("Basic authentication enabled.")
115+
_LOGGER.info("Basic authentication enabled.")

stac_fastapi/tests/basic_auth/test_basic_auth.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ async def test_get_search_not_authenticated(app_client_basic_auth, ctx):
1212

1313
response = await app_client_basic_auth.get("/search", params=params)
1414

15-
assert response.status_code == 200
15+
assert response.status_code == 200, response
1616
assert response.json()["features"][0]["geometry"] == ctx.item["geometry"]
1717

1818

@@ -26,26 +26,68 @@ async def test_post_search_authenticated(app_client_basic_auth, ctx):
2626

2727
response = await app_client_basic_auth.post("/search", json=params, headers=headers)
2828

29-
assert response.status_code == 200
29+
assert response.status_code == 200, response
3030
assert response.json()["features"][0]["geometry"] == ctx.item["geometry"]
3131

3232

3333
@pytest.mark.asyncio
34-
async def test_delete_resource_insufficient_permissions(app_client_basic_auth):
34+
async def test_delete_resource_anonymous(
35+
app_client_basic_auth,
36+
):
37+
"""Test protected delete collection without auhtentication"""
38+
if not os.getenv("BASIC_AUTH"):
39+
pytest.skip()
40+
41+
response = await app_client_basic_auth.delete("/collections/test-collection")
42+
43+
assert response.status_code == 401
44+
assert response.json() == {"detail": "Not authenticated"}
45+
46+
47+
@pytest.mark.asyncio
48+
async def test_delete_resource_invalid_credentials(app_client_basic_auth, ctx):
49+
"""Test protected delete collection with admin auhtentication"""
50+
if not os.getenv("BASIC_AUTH"):
51+
pytest.skip()
52+
53+
headers = {"Authorization": "Basic YWRtaW46cGFzc3dvcmQ="}
54+
55+
response = await app_client_basic_auth.delete(
56+
f"/collections/{ctx.collection['id']}", headers=headers
57+
)
58+
59+
assert response.status_code == 401
60+
assert response.json() == {"detail": "Incorrect username or password"}
61+
62+
63+
@pytest.mark.asyncio
64+
async def test_delete_resource_insufficient_permissions(app_client_basic_auth, ctx):
3565
"""Test protected delete collection with reader auhtentication"""
3666
if not os.getenv("BASIC_AUTH"):
3767
pytest.skip()
38-
headers = {
39-
"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="
40-
} # Assuming this is a valid authorization token
68+
69+
headers = {"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="}
4170

4271
response = await app_client_basic_auth.delete(
43-
"/collections/test-collection", headers=headers
72+
f"/collections/{ctx.collection['id']}", headers=headers
4473
)
4574

46-
assert (
47-
response.status_code == 403
48-
) # Expecting a 403 status code for insufficient permissions
75+
assert response.status_code == 403
4976
assert response.json() == {
5077
"detail": "Insufficient permissions for [DELETE /collections/test-collection]"
5178
}
79+
80+
81+
@pytest.mark.asyncio
82+
async def test_delete_resource_sufficient_permissions(app_client_basic_auth, ctx):
83+
"""Test protected delete collection with admin auhtentication"""
84+
if not os.getenv("BASIC_AUTH"):
85+
pytest.skip()
86+
87+
headers = {"Authorization": "Basic YWRtaW46YWRtaW4="}
88+
89+
response = await app_client_basic_auth.delete(
90+
f"/collections/{ctx.collection['id']}", headers=headers
91+
)
92+
93+
assert response.status_code == 204

0 commit comments

Comments
 (0)