From 63f033f474e8df400c36a940ed39ba4c826f71bd Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 4 May 2024 19:18:40 +0800 Subject: [PATCH 1/5] get basic auth from core --- stac_fastapi/mongo/app.py | 2 +- stac_fastapi/tests/conftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stac_fastapi/mongo/app.py b/stac_fastapi/mongo/app.py index 10785b0..acc8a40 100644 --- a/stac_fastapi/mongo/app.py +++ b/stac_fastapi/mongo/app.py @@ -2,6 +2,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, EsAsyncBaseFiltersClient, @@ -17,7 +18,6 @@ TokenPaginationExtension, TransactionExtension, ) -from stac_fastapi.core.basic_auth import apply_basic_auth # from stac_fastapi.extensions.third_party import BulkTransactionExtension from stac_fastapi.mongo.config import AsyncMongoDBSettings diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index 061893b..b333204 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.mongo.basic_auth import apply_basic_auth if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": from stac_fastapi.opensearch.config import AsyncOpensearchSettings as AsyncSettings From 1c90cd43d2181452196c387e06549d8a13c52399 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 4 May 2024 19:20:56 +0800 Subject: [PATCH 2/5] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db834f1..cca48ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0/ ### Changed -- Moved core basic auth logic to stac-fastapi.core. +- Moved core basic auth logic to stac-fastapi.core. [#19](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/ +- Updated stac-fastapi.core to v2.4.0. [#19](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/ ## [v3.1.0] From 91b9db47073f54918966961dcb506d7a74694194 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 4 May 2024 19:24:52 +0800 Subject: [PATCH 3/5] skip collection extension test, add test --- .../tests/resources/test_collection.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/tests/resources/test_collection.py b/stac_fastapi/tests/resources/test_collection.py index 602b7f2..980ff60 100644 --- a/stac_fastapi/tests/resources/test_collection.py +++ b/stac_fastapi/tests/resources/test_collection.py @@ -107,7 +107,25 @@ async def test_returns_valid_collection(ctx, app_client): @pytest.mark.asyncio -async def test_collection_extensions(ctx, app_client): +async def test_collection_extensions_post(ctx, app_client): + """Test that extensions can be used to define additional top-level properties""" + ctx.collection.get("stac_extensions", []).append( + "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json" + ) + test_asset = {"title": "test", "description": "test", "type": "test"} + ctx.collection["item_assets"] = {"test": test_asset} + ctx.collection["id"] = "test-item-assets" + resp = await app_client.post("/collections", json=ctx.collection) + + assert resp.status_code == 200 + assert resp.json().get("item_assets", {}).get("test") == test_asset + + +@pytest.mark.skip( + reason="https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/238" +) +@pytest.mark.asyncio +async def test_collection_extensions_put(ctx, app_client): """Test that extensions can be used to define additional top-level properties""" ctx.collection.get("stac_extensions", []).append( "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json" From d408ca75318e2f12e766196834b0a30a266ec438 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 4 May 2024 19:28:18 +0800 Subject: [PATCH 4/5] v3.2.0 --- CHANGELOG.md | 6 +++++- setup.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cca48ce..192e3e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0/ ## [Unreleased] + +## [v3.2.0] + ### Changed - Moved core basic auth logic to stac-fastapi.core. [#19](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/ @@ -55,7 +58,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0/ ---- -[Unreleased]: +[Unreleased]: +[v3.2.0]: [v3.1.0]: [v3.0.1]: [v3.0.0]: diff --git a/setup.py b/setup.py index 418328b..55096d3 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( name="stac-fastapi.mongo", - version="3.1.0", + version="3.2.0", description="Mongodb stac-fastapi backend.", long_description=desc, long_description_content_type="text/markdown", From 4d0e7c33c67badff7a9bfe21f847067417ec2375 Mon Sep 17 00:00:00 2001 From: pedro-cf Date: Sat, 4 May 2024 12:47:25 +0100 Subject: [PATCH 5/5] workflow+updated confest+basic_auth_tests --- .github/workflows/cicd.yml | 41 +------- .../tests/basic_auth/test_basic_auth.py | 95 ++++++++++++------- stac_fastapi/tests/conftest.py | 31 +++++- 3 files changed, 90 insertions(+), 77 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index e0f3cdb..254aa16 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -61,43 +61,4 @@ jobs: MONGO_DB: stac MONGO_USER: root MONGO_PASS: example - MONGO_PORT: 27017 - - - name: Run test suite against Mongo w/ Basic Auth - run: | - pipenv run pytest -k "basic_auth" -svvv - env: - MONGO_HOST: 172.17.0.1 - BACKEND: mongo - APP_HOST: 0.0.0.0 - APP_PORT: 8084 - ENVIRONMENT: testing - MONGO_DB: stac - MONGO_USER: root - MONGO_PASS: example - MONGO_PORT: 27017 - 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"]} - ] - } - ] - } + MONGO_PORT: 27017 \ No newline at end of file diff --git a/stac_fastapi/tests/basic_auth/test_basic_auth.py b/stac_fastapi/tests/basic_auth/test_basic_auth.py index cea5e5b..0515364 100644 --- a/stac_fastapi/tests/basic_auth/test_basic_auth.py +++ b/stac_fastapi/tests/basic_auth/test_basic_auth.py @@ -2,65 +2,92 @@ import pytest -# - 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"]}]}]} - @pytest.mark.asyncio -async def test_get_search_not_authenticated(app_client_basic_auth): - """Test public endpoint search without authentication""" +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 = {"query": '{"gsd": {"gt": 14}}'} + params = {"id": ctx.item["id"]} response = await app_client_basic_auth.get("/search", params=params) - assert response.status_code == 200 - assert response.json() == { - "type": "FeatureCollection", - "features": [], - "links": [], - "context": {"returned": 0, "limit": 10, "matched": 0}, - } + 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): - """Test protected post search with reader auhtentication""" +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 = { - "bbox": [97.504892, -45.254738, 174.321298, -2.431580], - "fields": {"exclude": ["properties"]}, - } + 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() == { - "type": "FeatureCollection", - "features": [], - "links": [], - "context": {"returned": 0, "limit": 10, "matched": 0}, - } + 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_insufficient_permissions(app_client_basic_auth): - """Test protected delete collection with reader auhtentication""" +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 cmVhZGVyOnJlYWRlcg==" - } # Assuming this is a valid authorization token + + headers = {"Authorization": "Basic YWRtaW46cGFzc3dvcmQ="} 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 == 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 b333204..042ea4c 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -227,7 +227,7 @@ async def app_client(app): @pytest_asyncio.fixture(scope="session") -async def app_auth(): +async def app_basic_auth(): settings = AsyncSettings() extensions = [ TransactionExtension( @@ -259,14 +259,39 @@ async def app_auth(): 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_auth): +async def app_client_basic_auth(app_basic_auth): await create_collection_index() - async with AsyncClient(app=app_auth, base_url="http://test-server") as c: + async with AsyncClient(app=app_basic_auth, base_url="http://test-server") as c: yield c