Skip to content
Closed
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
4 changes: 4 additions & 0 deletions application/single_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ def register_route_blueprint(name, registrar, auth_guard=None):
from background_tasks import start_background_task_threads

from route_external_health import *
from route_external_public_documents import *

_app_init_lock = threading.Lock()
_app_initialized = False
Expand Down Expand Up @@ -1428,6 +1429,9 @@ def finalize_m365_json_request(response):
register_route_blueprint('external_health', register_route_external_health)
register_route_blueprint('external_no_auth_health', register_no_auth_health)

# ------------------- External Public Document Routes (bearer-token protected) ----------
register_route_blueprint('external_public_documents', register_route_external_public_documents)

if __name__ == '__main__':
debug_mode = os.environ.get("FLASK_DEBUG", "0") == "1"
use_gunicorn = os.environ.get("SIMPLECHAT_USE_GUNICORN", "0").strip().lower() in ('1', 'true', 'yes', 'on')
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.061"
VERSION = "0.261.062"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
7 changes: 7 additions & 0 deletions application/single_app/route_external_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ def external_health_check():
time_string = now.strftime("%Y-%m-%d %H:%M:%S")
return time_string, 200

@bp.route('/external/testaccesstoken', methods=['POST'])
@swagger_route(security=get_auth_security())
@accesstoken_required
def external_test_access_token():
"""Confirm a bearer token is valid and carries the ExternalApi role."""
return "Access token is valid.", 200


def register_no_auth_health(bp):
@bp.route('/external/healthcheckz', methods=['GET'])
Expand Down
9 changes: 9 additions & 0 deletions docs/explanation/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes).

### **(v0.261.062)**

#### Bug Fixes

* **External Public Document API Routes Registered Again**
* Registered the existing bearer-token protected `/external/public_documents` routes (list, upload, delete) in `app.py`, which previously defined them but never mounted them, so external bulk-loading clients received 404 responses.
* Restored the `POST /external/testaccesstoken` preflight route, protected by `@accesstoken_required` (ExternalApi role).
* (Ref: `app.py`, `route_external_health.py`, `route_external_public_documents.py`, `functional_tests/test_external_public_documents_route_registration.py`)

### **(v0.261.061)**

#### New Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"enhanced_citations": ("login_required", "user_required"),
"external_health": (),
"external_no_auth_health": (),
"external_public_documents": (),
"frontend_admin_settings": ("login_required", "admin_required"),
"frontend_agents": ("login_required", "user_required"),
"frontend_terms_of_use": (),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

EXTERNAL_BEARER_PATH_PREFIXES = (
"/external/public_documents",
"/external/testaccesstoken",
)

INBOUND_MCP_BEARER_PATH_PREFIXES = (
Expand Down
110 changes: 110 additions & 0 deletions functional_tests/test_external_public_documents_route_registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# test_external_public_documents_route_registration.py
"""
Functional test for external public document route registration.
Version: 0.261.062
Implemented in: 0.261.062

This test ensures the bearer-token protected external public document routes
and the access token preflight route are registered, so external bulk-loading
clients keep working.
"""

import re
import sys
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
APP_DIR = ROOT / "application" / "single_app"
APP_FILE = APP_DIR / "app.py"
HEALTH_FILE = APP_DIR / "route_external_health.py"
PUBLIC_DOCS_FILE = APP_DIR / "route_external_public_documents.py"

BEARER_PROTECTED_ROUTES = [
("/external/public_documents", "GET"),
("/external/public_documents/upload", "POST"),
("/external/public_documents/<document_id>", "DELETE"),
]


def read_text(path: Path) -> str:
"""Return UTF-8 text for a repo file."""
return path.read_text(encoding="utf-8")


def route_block(content: str, path: str, method: str) -> str:
"""Return the decorator and signature lines for one route."""
pattern = re.compile(
r"@bp\.route\('" + re.escape(path) + r"', methods=\['" + method + r"'\]\)(.*?)\n\s*def ",
re.S,
)
match = pattern.search(content)
if not match:
raise AssertionError(f"Route {method} {path} not found")
return match.group(1)


def test_app_registers_external_public_documents() -> bool:
"""Validate app.py imports and registers the external public document routes."""
print("Testing external public document registration in app.py...")

try:
app_content = read_text(APP_FILE)
required_snippets = [
"from route_external_public_documents import *",
"register_route_blueprint('external_public_documents', register_route_external_public_documents)",
]
for snippet in required_snippets:
if snippet not in app_content:
raise AssertionError(f"Missing in app.py: {snippet}")

print("Passed")
return True
except Exception as ex:
print(f"Test failed: {ex}")
import traceback
traceback.print_exc()
return False


def test_external_routes_require_bearer_token() -> bool:
"""Validate the preflight and document routes use the bearer-token decorator."""
print("Testing bearer-token protection on external routes...")

try:
health_content = read_text(HEALTH_FILE)
docs_content = read_text(PUBLIC_DOCS_FILE)

preflight = route_block(health_content, "/external/testaccesstoken", "POST")
if "@accesstoken_required" not in preflight:
raise AssertionError("/external/testaccesstoken must use @accesstoken_required")

for path, method in BEARER_PROTECTED_ROUTES:
block = route_block(docs_content, path, method)
if "@accesstoken_required" not in block:
raise AssertionError(f"{method} {path} must use @accesstoken_required")

print("Passed")
return True
except Exception as ex:
print(f"Test failed: {ex}")
import traceback
traceback.print_exc()
return False


if __name__ == "__main__":
tests = [
test_app_registers_external_public_documents,
test_external_routes_require_bearer_token,
]

results = []
for test in tests:
print(f"\nRunning {test.__name__}...")
results.append(test())

success = all(results)
print(f"\nResults: {sum(results)}/{len(results)} tests passed")
sys.exit(0 if success else 1)