-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from pedro-cf/basic-auth
Basic Authentication
- Loading branch information
Showing
11 changed files
with
445 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
version: '3.9' | ||
|
||
services: | ||
app-mongo: | ||
container_name: stac-fastapi-mongo | ||
image: stac-utils/stac-fastapi-mongo | ||
restart: always | ||
build: | ||
context: . | ||
dockerfile: dockerfiles/Dockerfile.dev.mongo | ||
environment: | ||
- APP_HOST=0.0.0.0 | ||
- APP_PORT=8084 | ||
- RELOAD=true | ||
- ENVIRONMENT=local | ||
- BACKEND=mongo | ||
- MONGO_DB=stac | ||
- MONGO_HOST=mongo | ||
- MONGO_USER=root | ||
- MONGO_PASS=example | ||
- MONGO_PORT=27017 | ||
- 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: | ||
- "8084:8084" | ||
volumes: | ||
- ./stac_fastapi:/app/stac_fastapi | ||
- ./scripts:/app/scripts | ||
depends_on: | ||
- mongo | ||
command: | ||
bash -c "./scripts/wait-for-it-es.sh mongo-container:27017 && python -m stac_fastapi.mongo.app" | ||
|
||
mongo: | ||
container_name: mongo-container | ||
image: mongo:7.0.5 | ||
hostname: mongo | ||
environment: | ||
- MONGO_INITDB_ROOT_USERNAME=root | ||
- MONGO_INITDB_ROOT_PASSWORD=example | ||
ports: | ||
- "27017:27017" | ||
|
||
mongo-express: | ||
image: mongo-express | ||
restart: always | ||
ports: | ||
- "8081:8081" | ||
environment: | ||
- ME_CONFIG_MONGODB_ADMINUSERNAME=root | ||
- ME_CONFIG_MONGODB_ADMINPASSWORD=example | ||
- ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
version: '3.9' | ||
|
||
services: | ||
app-mongo: | ||
container_name: stac-fastapi-mongo | ||
image: stac-utils/stac-fastapi-mongo | ||
restart: always | ||
build: | ||
context: . | ||
dockerfile: dockerfiles/Dockerfile.dev.mongo | ||
environment: | ||
- APP_HOST=0.0.0.0 | ||
- APP_PORT=8084 | ||
- RELOAD=true | ||
- ENVIRONMENT=local | ||
- BACKEND=mongo | ||
- MONGO_DB=stac | ||
- MONGO_HOST=mongo | ||
- MONGO_USER=root | ||
- MONGO_PASS=example | ||
- MONGO_PORT=27017 | ||
- 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: | ||
- "8084:8084" | ||
volumes: | ||
- ./stac_fastapi:/app/stac_fastapi | ||
- ./scripts:/app/scripts | ||
depends_on: | ||
- mongo | ||
command: | ||
bash -c "./scripts/wait-for-it-es.sh mongo-container:27017 && python -m stac_fastapi.mongo.app" | ||
|
||
mongo: | ||
container_name: mongo-container | ||
image: mongo:7.0.5 | ||
hostname: mongo | ||
environment: | ||
- MONGO_INITDB_ROOT_USERNAME=root | ||
- MONGO_INITDB_ROOT_PASSWORD=example | ||
ports: | ||
- "27017:27017" | ||
|
||
mongo-express: | ||
image: mongo-express | ||
restart: always | ||
ports: | ||
- "8081:8081" | ||
environment: | ||
- ME_CONFIG_MONGODB_ADMINUSERNAME=root | ||
- ME_CONFIG_MONGODB_ADMINPASSWORD=example | ||
- ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
"pymongo==4.6.2", | ||
"uvicorn", | ||
"starlette", | ||
"typing_extensions==4.4.0", | ||
] | ||
|
||
extra_reqs = { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
"""Basic Authentication Module.""" | ||
|
||
import json | ||
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 | ||
|
||
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: | ||
print("Basic authentication disabled.") | ||
return | ||
|
||
try: | ||
_BASIC_AUTH = json.loads(basic_auth_json_str) | ||
except json.JSONDecodeError as exception: | ||
print(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)]) | ||
|
||
print("Basic authentication enabled.") |
Empty file.
Oops, something went wrong.