Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import functools
import json
import uuid
from datetime import datetime
Expand Down Expand Up @@ -33,6 +34,9 @@ def get_token_from_auth_header(request: Request) -> Any:


def authentication_middleware(func: Any) -> Any:
# Flask names endpoints after the view's __name__; without wraps every
# decorated route registers as "wrapper" and collides.
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
token = get_token_from_auth_header(request)
# Check if bearer token exists and validate it
Expand Down Expand Up @@ -111,7 +115,7 @@ def validate_bearer_token(token: str | None) -> bool:
return False


@platform_bp.route("/page-usage", methods=["POST"], endpoint="page_usage")
@platform_bp.route("/page-usage", methods=["POST"])
@authentication_middleware
def page_usage() -> Any:
"""Usage endpoint."""
Expand Down Expand Up @@ -262,11 +266,7 @@ def usage() -> Any:
return make_response(result, 500)


@platform_bp.route(
"/platform_details",
methods=["GET"],
endpoint="platform_details",
)
@platform_bp.route("/platform_details", methods=["GET"])
@authentication_middleware
def platform_details() -> Any:
"""Fetch details associated with the platform. This uses the authenticated
Expand All @@ -288,7 +288,7 @@ def platform_details() -> Any:
return result, 200


@platform_bp.route("/cache", methods=["POST", "GET", "DELETE"], endpoint="cache")
@platform_bp.route("/cache", methods=["POST", "GET", "DELETE"])
@authentication_middleware
def cache() -> Any:
"""Cache endpoint.
Expand Down Expand Up @@ -348,11 +348,7 @@ def cache() -> Any:
return "OK", 200


@platform_bp.route(
"/adapter_instance",
methods=["GET"],
endpoint="adapter_instance",
)
@platform_bp.route("/adapter_instance", methods=["GET"])
@authentication_middleware
def adapter_instance() -> Any:
"""Fetch Adapter instance.
Expand Down Expand Up @@ -399,11 +395,7 @@ def adapter_instance() -> Any:
raise APIError(message=msg)


@platform_bp.route(
"/custom_tool_instance",
methods=["GET"],
endpoint="custom_tool_instance",
)
@platform_bp.route("/custom_tool_instance", methods=["GET"])
@authentication_middleware
def custom_tool_instance() -> Any:
"""Fetching exported custom tool instance.
Expand Down Expand Up @@ -438,11 +430,7 @@ def custom_tool_instance() -> Any:
raise APIError(message=msg) from e


@platform_bp.route(
"/agentic_tool_instance",
methods=["GET"],
endpoint="agentic_tool_instance",
)
@platform_bp.route("/agentic_tool_instance", methods=["GET"])
@authentication_middleware
def agentic_tool_instance() -> Any:
"""Fetching exported agentic tool instance.
Expand Down Expand Up @@ -479,11 +467,7 @@ def agentic_tool_instance() -> Any:
raise APIError(message=msg) from e


@platform_bp.route(
"/llm_profile_instance",
methods=["GET"],
endpoint="llm_profile_instance",
)
@platform_bp.route("/llm_profile_instance", methods=["GET"])
@authentication_middleware
def llm_profile_instance() -> Any:
"""Fetching llm profile instance."""
Expand Down
6 changes: 6 additions & 0 deletions platform-service/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import os

# platform_service.env validates required settings at import time.
os.environ.setdefault("REDIS_HOST", "localhost")
os.environ.setdefault("ENCRYPTION_KEY", "test-key")
os.environ.setdefault("DB_SCHEMA", "unstract")
58 changes: 58 additions & 0 deletions platform-service/tests/test_auth_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Tests for bearer token validation used by `authentication_middleware`."""

from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any

import pytest
from flask import Flask
from unstract.platform_service.controller import platform


@pytest.fixture
def app_ctx() -> Iterator[None]:
"""`validate_bearer_token` logs via `flask.current_app`."""
with Flask(__name__).app_context():
yield


def _mock_safe_cursor(monkeypatch: pytest.MonkeyPatch, result_row: Any) -> None:
class _Cursor:
def fetchone(self) -> Any:
return result_row

@contextmanager
def _safe_cursor(query: str, params: tuple = ()) -> Iterator[_Cursor]:
yield _Cursor()

monkeypatch.setattr(platform, "safe_cursor", _safe_cursor)


def test_valid_active_token(app_ctx: None, monkeypatch: pytest.MonkeyPatch) -> None:
_mock_safe_cursor(monkeypatch, ("id", "test-token", True))
assert platform.validate_bearer_token("test-token") is True


def test_rejects_missing_token(app_ctx: None) -> None:
assert platform.validate_bearer_token(None) is False


def test_rejects_unknown_token(app_ctx: None, monkeypatch: pytest.MonkeyPatch) -> None:
_mock_safe_cursor(monkeypatch, None)
assert platform.validate_bearer_token("test-token") is False


def test_rejects_inactive_token(app_ctx: None, monkeypatch: pytest.MonkeyPatch) -> None:
_mock_safe_cursor(monkeypatch, ("id", "test-token", False))
assert platform.validate_bearer_token("test-token") is False


def test_db_error_denies_access(app_ctx: None, monkeypatch: pytest.MonkeyPatch) -> None:
"""Auth must fail closed when the token lookup blows up."""

# Raising on call suffices: `safe_cursor(...)` is invoked inside the try.
def _boom(query: str, params: tuple = ()) -> Any:
raise RuntimeError("db down")

monkeypatch.setattr(platform, "safe_cursor", _boom)
assert platform.validate_bearer_token("test-token") is False
18 changes: 18 additions & 0 deletions platform-service/tests/test_route_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Guards that `authentication_middleware` stays transparent to Flask.

Without `functools.wraps` every decorated route registers as "wrapper", so the
second such route raises at import time.
"""

from flask import Flask
from unstract.platform_service.controller.platform import platform_bp


def test_routes_register_under_their_function_names() -> None:
# NOSONAR — app is never served; bearer-token routes carry no CSRF surface.
app = Flask(__name__) # NOSONAR
app.register_blueprint(platform_bp)

endpoints = {rule.endpoint for rule in app.url_map.iter_rules()}
assert "platform.wrapper" not in endpoints
assert "platform.usage" in endpoints
Loading