Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GET basic auth from stac-fastapi.core #19

Merged
merged 5 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 1 addition & 40 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ 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.
- 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]
Expand Down Expand Up @@ -54,7 +58,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0/

----

[Unreleased]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.1.0...main>
[Unreleased]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.2.0...main>
[v3.2.0]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.1.0...v3.2.0>
[v3.1.0]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.0.1...v3.1.0>
[v3.0.1]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/compare/v3.0.0...v3.0.1>
[v3.0.0]: <https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/tree/v3.0.0>
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion stac_fastapi/mongo/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
95 changes: 61 additions & 34 deletions stac_fastapi/tests/basic_auth/test_basic_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
33 changes: 29 additions & 4 deletions stac_fastapi/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
20 changes: 19 additions & 1 deletion stac_fastapi/tests/resources/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down