Skip to content

Commit 226c95e

Browse files
authored
Merge pull request #19 from Healy-Hyperspatial/move-basic-auth
GET basic auth from stac-fastapi.core
2 parents 5a345a2 + 4d0e7c3 commit 226c95e

File tree

7 files changed

+119
-83
lines changed

7 files changed

+119
-83
lines changed

.github/workflows/cicd.yml

+1-40
Original file line numberDiff line numberDiff line change
@@ -61,43 +61,4 @@ jobs:
6161
MONGO_DB: stac
6262
MONGO_USER: root
6363
MONGO_PASS: example
64-
MONGO_PORT: 27017
65-
66-
- name: Run test suite against Mongo w/ Basic Auth
67-
run: |
68-
pipenv run pytest -k "basic_auth" -svvv
69-
env:
70-
MONGO_HOST: 172.17.0.1
71-
BACKEND: mongo
72-
APP_HOST: 0.0.0.0
73-
APP_PORT: 8084
74-
ENVIRONMENT: testing
75-
MONGO_DB: stac
76-
MONGO_USER: root
77-
MONGO_PASS: example
78-
MONGO_PORT: 27017
79-
BASIC_AUTH: >
80-
{
81-
"public_endpoints": [
82-
{"path": "/","method": "GET"},
83-
{"path": "/search","method": "GET"}
84-
],
85-
"users": [
86-
{"username": "admin", "password": "admin", "permissions": "*"},
87-
{
88-
"username": "reader",
89-
"password": "reader",
90-
"permissions": [
91-
{"path": "/conformance","method": ["GET"]},
92-
{"path": "/collections/{collection_id}/items/{item_id}","method": ["GET"]},
93-
{"path": "/search","method": ["POST"]},
94-
{"path": "/collections","method": ["GET"]},
95-
{"path": "/collections/{collection_id}","method": ["GET"]},
96-
{"path": "/collections/{collection_id}/items","method": ["GET"]},
97-
{"path": "/queryables","method": ["GET"]},
98-
{"path": "/queryables/collections/{collection_id}/queryables","method": ["GET"]},
99-
{"path": "/_mgmt/ping","method": ["GET"]}
100-
]
101-
}
102-
]
103-
}
64+
MONGO_PORT: 27017

CHANGELOG.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0/
77

88
## [Unreleased]
99

10+
11+
## [v3.2.0]
12+
1013
### Changed
1114

12-
- Moved core basic auth logic to stac-fastapi.core.
15+
- Moved core basic auth logic to stac-fastapi.core. [#19](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/
16+
- Updated stac-fastapi.core to v2.4.0. [#19](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/
1317

1418

1519
## [v3.1.0]
@@ -54,7 +58,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0/
5458

5559
----
5660

57-
[Unreleased]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.1.0...main>
61+
[Unreleased]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.2.0...main>
62+
[v3.2.0]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.1.0...v3.2.0>
5863
[v3.1.0]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.0.1...v3.1.0>
5964
[v3.0.1]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.0.0...v3.0.1>
6065
[v3.0.0]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/tree/v3.0.0>

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
setup(
3232
name="stac-fastapi.mongo",
33-
version="3.1.0",
33+
version="3.2.0",
3434
description="Mongodb stac-fastapi backend.",
3535
long_description=desc,
3636
long_description_content_type="text/markdown",

stac_fastapi/mongo/app.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from stac_fastapi.api.app import StacApi
44
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
5+
from stac_fastapi.core.basic_auth import apply_basic_auth
56
from stac_fastapi.core.core import ( # BulkTransactionsClient,
67
CoreClient,
78
EsAsyncBaseFiltersClient,
@@ -17,7 +18,6 @@
1718
TokenPaginationExtension,
1819
TransactionExtension,
1920
)
20-
from stac_fastapi.core.basic_auth import apply_basic_auth
2121

2222
# from stac_fastapi.extensions.third_party import BulkTransactionExtension
2323
from stac_fastapi.mongo.config import AsyncMongoDBSettings

stac_fastapi/tests/basic_auth/test_basic_auth.py

+61-34
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,92 @@
22

33
import pytest
44

5-
# - 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"]}]}]}
6-
75

86
@pytest.mark.asyncio
9-
async def test_get_search_not_authenticated(app_client_basic_auth):
10-
"""Test public endpoint search without authentication"""
7+
async def test_get_search_not_authenticated(app_client_basic_auth, ctx):
8+
"""Test public endpoint [GET /search] without authentication"""
119
if not os.getenv("BASIC_AUTH"):
1210
pytest.skip()
13-
params = {"query": '{"gsd": {"gt": 14}}'}
11+
params = {"id": ctx.item["id"]}
1412

1513
response = await app_client_basic_auth.get("/search", params=params)
1614

17-
assert response.status_code == 200
18-
assert response.json() == {
19-
"type": "FeatureCollection",
20-
"features": [],
21-
"links": [],
22-
"context": {"returned": 0, "limit": 10, "matched": 0},
23-
}
15+
assert response.status_code == 200, response
16+
assert response.json()["features"][0]["geometry"] == ctx.item["geometry"]
2417

2518

2619
@pytest.mark.asyncio
27-
async def test_post_search_authenticated(app_client_basic_auth):
28-
"""Test protected post search with reader auhtentication"""
20+
async def test_post_search_authenticated(app_client_basic_auth, ctx):
21+
"""Test protected endpoint [POST /search] with reader auhtentication"""
2922
if not os.getenv("BASIC_AUTH"):
3023
pytest.skip()
31-
params = {
32-
"bbox": [97.504892, -45.254738, 174.321298, -2.431580],
33-
"fields": {"exclude": ["properties"]},
34-
}
24+
params = {"id": ctx.item["id"]}
3525
headers = {"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="}
3626

3727
response = await app_client_basic_auth.post("/search", json=params, headers=headers)
3828

39-
assert response.status_code == 200
40-
assert response.json() == {
41-
"type": "FeatureCollection",
42-
"features": [],
43-
"links": [],
44-
"context": {"returned": 0, "limit": 10, "matched": 0},
45-
}
29+
assert response.status_code == 200, response
30+
assert response.json()["features"][0]["geometry"] == ctx.item["geometry"]
31+
32+
33+
@pytest.mark.asyncio
34+
async def test_delete_resource_anonymous(
35+
app_client_basic_auth,
36+
):
37+
"""Test protected endpoint [DELETE /collections/{collection_id}] 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"}
4645

4746

4847
@pytest.mark.asyncio
49-
async def test_delete_resource_insufficient_permissions(app_client_basic_auth):
50-
"""Test protected delete collection with reader auhtentication"""
48+
async def test_delete_resource_invalid_credentials(app_client_basic_auth, ctx):
49+
"""Test protected endpoint [DELETE /collections/{collection_id}] with invalid credentials"""
5150
if not os.getenv("BASIC_AUTH"):
5251
pytest.skip()
53-
headers = {
54-
"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="
55-
} # Assuming this is a valid authorization token
52+
53+
headers = {"Authorization": "Basic YWRtaW46cGFzc3dvcmQ="}
5654

5755
response = await app_client_basic_auth.delete(
58-
"/collections/test-collection", headers=headers
56+
f"/collections/{ctx.collection['id']}", headers=headers
5957
)
6058

61-
assert (
62-
response.status_code == 403
63-
) # Expecting a 403 status code for insufficient permissions
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):
65+
"""Test protected endpoint [DELETE /collections/{collection_id}] with reader user which has insufficient permissions"""
66+
if not os.getenv("BASIC_AUTH"):
67+
pytest.skip()
68+
69+
headers = {"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="}
70+
71+
response = await app_client_basic_auth.delete(
72+
f"/collections/{ctx.collection['id']}", headers=headers
73+
)
74+
75+
assert response.status_code == 403
6476
assert response.json() == {
6577
"detail": "Insufficient permissions for [DELETE /collections/test-collection]"
6678
}
79+
80+
81+
@pytest.mark.asyncio
82+
async def test_delete_resource_sufficient_permissions(app_client_basic_auth, ctx):
83+
"""Test protected endpoint [DELETE /collections/{collection_id}] with admin user which has sufficient permissions"""
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

stac_fastapi/tests/conftest.py

+29-4
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010

1111
from stac_fastapi.api.app import StacApi
1212
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
13+
from stac_fastapi.core.basic_auth import apply_basic_auth
1314
from stac_fastapi.core.core import (
1415
BulkTransactionsClient,
1516
CoreClient,
1617
TransactionsClient,
1718
)
1819
from stac_fastapi.core.extensions import QueryExtension
19-
from stac_fastapi.mongo.basic_auth import apply_basic_auth
2020

2121
if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch":
2222
from stac_fastapi.opensearch.config import AsyncOpensearchSettings as AsyncSettings
@@ -227,7 +227,7 @@ async def app_client(app):
227227

228228

229229
@pytest_asyncio.fixture(scope="session")
230-
async def app_auth():
230+
async def app_basic_auth():
231231
settings = AsyncSettings()
232232
extensions = [
233233
TransactionExtension(
@@ -259,14 +259,39 @@ async def app_auth():
259259
search_post_request_model=post_request_model,
260260
)
261261

262+
os.environ[
263+
"BASIC_AUTH"
264+
] = """{
265+
"public_endpoints": [
266+
{"path": "/", "method": "GET"},
267+
{"path": "/search", "method": "GET"}
268+
],
269+
"users": [
270+
{"username": "admin", "password": "admin", "permissions": "*"},
271+
{
272+
"username": "reader", "password": "reader",
273+
"permissions": [
274+
{"path": "/conformance", "method": ["GET"]},
275+
{"path": "/collections/{collection_id}/items/{item_id}", "method": ["GET"]},
276+
{"path": "/search", "method": ["POST"]},
277+
{"path": "/collections", "method": ["GET"]},
278+
{"path": "/collections/{collection_id}", "method": ["GET"]},
279+
{"path": "/collections/{collection_id}/items", "method": ["GET"]},
280+
{"path": "/queryables", "method": ["GET"]},
281+
{"path": "/queryables/collections/{collection_id}/queryables", "method": ["GET"]},
282+
{"path": "/_mgmt/ping", "method": ["GET"]}
283+
]
284+
}
285+
]
286+
}"""
262287
apply_basic_auth(stac_api)
263288

264289
return stac_api.app
265290

266291

267292
@pytest_asyncio.fixture(scope="session")
268-
async def app_client_basic_auth(app_auth):
293+
async def app_client_basic_auth(app_basic_auth):
269294
await create_collection_index()
270295

271-
async with AsyncClient(app=app_auth, base_url="http://test-server") as c:
296+
async with AsyncClient(app=app_basic_auth, base_url="http://test-server") as c:
272297
yield c

stac_fastapi/tests/resources/test_collection.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,25 @@ async def test_returns_valid_collection(ctx, app_client):
107107

108108

109109
@pytest.mark.asyncio
110-
async def test_collection_extensions(ctx, app_client):
110+
async def test_collection_extensions_post(ctx, app_client):
111+
"""Test that extensions can be used to define additional top-level properties"""
112+
ctx.collection.get("stac_extensions", []).append(
113+
"https://stac-extensions.github.io/item-assets/v1.0.0/schema.json"
114+
)
115+
test_asset = {"title": "test", "description": "test", "type": "test"}
116+
ctx.collection["item_assets"] = {"test": test_asset}
117+
ctx.collection["id"] = "test-item-assets"
118+
resp = await app_client.post("/collections", json=ctx.collection)
119+
120+
assert resp.status_code == 200
121+
assert resp.json().get("item_assets", {}).get("test") == test_asset
122+
123+
124+
@pytest.mark.skip(
125+
reason="https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/238"
126+
)
127+
@pytest.mark.asyncio
128+
async def test_collection_extensions_put(ctx, app_client):
111129
"""Test that extensions can be used to define additional top-level properties"""
112130
ctx.collection.get("stac_extensions", []).append(
113131
"https://stac-extensions.github.io/item-assets/v1.0.0/schema.json"

0 commit comments

Comments
 (0)