Skip to content

283 external mode web api contract config fetch external result ingestion#295

Merged
ArthurCRodrigues merged 6 commits intomainfrom
283-external-mode-web-api-contract-config-fetch-external-result-ingestion
Apr 21, 2026
Merged

283 external mode web api contract config fetch external result ingestion#295
ArthurCRodrigues merged 6 commits intomainfrom
283-external-mode-web-api-contract-config-fetch-external-result-ingestion

Conversation

@matheusmra
Copy link
Copy Markdown
Member

[External Mode] Web API contract + M2M authentication for cloud integration

Summary

Adds two new protected endpoints to support GitHub Action external/private mode, along with a mandatory bearer-token authentication layer for machine-to-machine cloud integration.


What changed

New endpoints

GET /api/v1/configs/id/{config_id}

Fetches a full grading configuration by its internal database ID.
Required by the GitHub Action when operating in external mode, where only the grading_config_id is available.

Returns all fields needed to call build_pipeline(...):
template_name, criteria_config, setup_config, feedback_config, include_feedback, languages, external_assignment_id, version, is_active.

POST /api/v1/submissions/external-results

Ingests a grading result computed externally (e.g. inside the GitHub Action runner).

Creates a submissions row and a matching submission_results row. The result is immediately visible through the existing GET /submissions/{id} and GET /submissions/user/{external_user_id} endpoints.

Request payload:

Field Required Notes
grading_config_id Internal config ID
external_user_id
username
language Must be supported by the config
status completed or failed
final_score >= 0
execution_time_ms >= 0
feedback Optional
result_tree Optional
focus Optional
pipeline_execution Optional
error_message Optional, for failed runs
submission_metadata Optional repo/run metadata

M2M authentication

Both new endpoints are protected with a Bearer token dependency.

  • Token is read from AUTOGRADER_INTEGRATION_TOKEN environment variable (required — server raises ValueError at startup if unset)
  • Validation uses hmac.compare_digest to prevent timing attacks
  • Secrets are never logged
  • Returns 401 for missing or invalid token
  • All existing public endpoints are unaffected

New schemas

  • ExternalResultStatuscompleted | failed
  • ExternalResultCreate — request body with Pydantic validation
  • ExternalResultResponse — response body

Docs

docs/API.md updated with:

  • Authentication section with setup instructions and usage examples
  • New endpoint documentation with request/response examples
  • markers on protected endpoints in the overview table

Tests

File Coverage
tests/web/test_external_results.py 13 tests — success paths, 404, 400, 422
tests/web/test_integration_auth.py 13 tests — missing token, wrong token, valid token, public endpoints unaffected

All 51 web tests pass (test_external_results, test_integration_auth, test_routes, test_api_endpoints).


Files changed

web/config/auth.py                        new
web/api/deps.py                           modified
web/api/v1/grading_configs.py             modified
web/api/v1/submissions.py                 modified
web/schemas/submission.py                 modified
web/schemas/__init__.py                   modified
tests/web/test_external_results.py        new
tests/web/test_integration_auth.py        new
docs/API.md                               modified

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds M2M-protected Web API support for “external/private mode” integrations (e.g., GitHub Action) by introducing bearer-token auth plus endpoints to fetch configs by internal ID and ingest externally computed grading results.

Changes:

  • Added Bearer-token auth dependency for integration-only endpoints.
  • Added GET /api/v1/configs/id/{config_id} for internal-ID config fetch.
  • Added POST /api/v1/submissions/external-results plus Pydantic schemas and web tests/docs for external result ingestion.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
web/config/auth.py Adds integration token config loader/caching.
web/api/deps.py Introduces require_integration_token FastAPI dependency using HTTPBearer.
web/api/v1/grading_configs.py Adds protected config fetch endpoint by internal DB ID.
web/api/v1/submissions.py Adds protected endpoint to ingest externally computed results into submissions + submission_results.
web/schemas/submission.py Adds ExternalResult* request/response schemas and status enum.
web/schemas/__init__.py Exports the new external-result schemas.
tests/web/test_external_results.py Adds tests for config-by-id and external result ingestion behavior.
tests/web/test_integration_auth.py Adds tests ensuring protected endpoints require token and public endpoints remain public.
docs/API.md Documents auth setup and the new protected endpoints.

Comment on lines +43 to +47
cfg = IntegrationAuthConfig.__new__(IntegrationAuthConfig)
cfg.token = TEST_TOKEN
auth_module.integration_auth_config = cfg
yield
auth_module.integration_auth_config = cfg
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _set_integration_token autouse fixture sets auth_module.integration_auth_config but does not restore the previous value in teardown (it reassigns the same cfg). This can leak state across test modules and mask failures around missing/invalid env configuration; capture the old value before overriding and restore it after yield (often back to None).

Suggested change
cfg = IntegrationAuthConfig.__new__(IntegrationAuthConfig)
cfg.token = TEST_TOKEN
auth_module.integration_auth_config = cfg
yield
auth_module.integration_auth_config = cfg
old_integration_auth_config = getattr(auth_module, "integration_auth_config", None)
cfg = IntegrationAuthConfig.__new__(IntegrationAuthConfig)
cfg.token = TEST_TOKEN
auth_module.integration_auth_config = cfg
yield
auth_module.integration_auth_config = old_integration_auth_config

Copilot uses AI. Check for mistakes.
Comment thread docs/API.md Outdated
Comment on lines +39 to +40
When the variable is **not set or empty**, the server will reject all requests to protected endpoints with `401 Unauthorized`. The token **must** be configured before using integration endpoints.

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph states that when AUTOGRADER_INTEGRATION_TOKEN is unset or empty the server will reject protected requests with 401, but the implementation currently raises a ValueError when the env var is missing (which surfaces as a 500 unless handled) and it does not explicitly treat empty values as misconfiguration. Please update the docs to match actual behavior, or adjust the implementation to match the docs.

Copilot uses AI. Check for mistakes.
Comment thread web/config/auth.py Outdated
raise ValueError(
"AUTOGRADER_INTEGRATION_TOKEN environment variable must be set"
)
self.token: str = os.environ["AUTOGRADER_INTEGRATION_TOKEN"]
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IntegrationAuthConfig currently only checks that AUTOGRADER_INTEGRATION_TOKEN exists, but it allows an empty string. An empty token effectively locks out all integration requests and is easy to misconfigure; consider validating that the env var is non-empty (e.g., after .strip()), and fail fast with a clear error.

Suggested change
self.token: str = os.environ["AUTOGRADER_INTEGRATION_TOKEN"]
token = os.environ["AUTOGRADER_INTEGRATION_TOKEN"].strip()
if not token:
raise ValueError(
"AUTOGRADER_INTEGRATION_TOKEN environment variable must be non-empty"
)
self.token: str = token

Copilot uses AI. Check for mistakes.
Comment thread web/api/deps.py Outdated
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme),
) -> None:
"""Enforce Bearer-token auth on integration endpoints."""
config = get_integration_auth_config()
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_integration_auth_config() can raise ValueError when the integration token env var is unset, but require_integration_token does not handle this. That will surface as a 500 error on the protected endpoints and contradicts the documented behavior; either initialize/validate the auth config during app startup so misconfiguration fails fast, or catch the exception here and return a deterministic HTTP error with a clear message.

Suggested change
config = get_integration_auth_config()
try:
config = get_integration_auth_config()
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Integration authentication is not configured",
) from exc

Copilot uses AI. Check for mistakes.
Comment thread web/api/v1/submissions.py Outdated
Comment on lines +293 to +306
now = datetime.now(timezone.utc)

# Create submission record (no files — externally graded)
submission_repo = SubmissionRepository(session)
db_submission = await submission_repo.create(
grading_config_id=grading_config.id,
external_user_id=payload.external_user_id,
username=payload.username,
submission_files={},
language=payload.language,
status=submission_status,
submission_metadata=payload.submission_metadata,
)
db_submission.graded_at = now
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now = datetime.now(timezone.utc) produces a timezone-aware datetime, but elsewhere the code persists graded_at as a naive UTC timestamp (e.g., grading_service uses datetime.now(timezone.utc).replace(tzinfo=None)). Storing/returning mixed naive/aware datetimes can cause inconsistent API responses and can error on some DB backends; align this endpoint with the existing persistence convention for graded_at.

Copilot uses AI. Check for mistakes.
Comment thread tests/web/test_integration_auth.py Outdated
Comment on lines +44 to +48
cfg = IntegrationAuthConfig.__new__(IntegrationAuthConfig)
cfg.token = TEST_TOKEN
auth_module.integration_auth_config = cfg
yield
auth_module.integration_auth_config = cfg
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _set_integration_token autouse fixture sets auth_module.integration_auth_config but does not restore the previous value in teardown (it reassigns the same cfg). This can leak state across test modules and mask failures around missing/invalid env configuration; capture the old value before overriding and restore it after yield (often back to None).

Suggested change
cfg = IntegrationAuthConfig.__new__(IntegrationAuthConfig)
cfg.token = TEST_TOKEN
auth_module.integration_auth_config = cfg
yield
auth_module.integration_auth_config = cfg
old_integration_auth_config = auth_module.integration_auth_config
cfg = IntegrationAuthConfig.__new__(IntegrationAuthConfig)
cfg.token = TEST_TOKEN
auth_module.integration_auth_config = cfg
yield
auth_module.integration_auth_config = old_integration_auth_config

Copilot uses AI. Check for mistakes.
Comment thread tests/web/test_external_results.py Outdated

import pytest
from httpx import AsyncClient, ASGITransport
from unittest.mock import Mock, patch, AsyncMock
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsyncMock is imported but not used in this test module. Removing unused imports keeps tests easier to maintain and avoids lint failures in stricter CI setups.

Suggested change
from unittest.mock import Mock, patch, AsyncMock
from unittest.mock import Mock, patch

Copilot uses AI. Check for mistakes.
@matheusmra
Copy link
Copy Markdown
Member Author

@ArthurCRodrigues ready for review :D

@ArthurCRodrigues
Copy link
Copy Markdown
Member

Will review soon.

@ArthurCRodrigues ArthurCRodrigues merged commit 7ea1265 into main Apr 21, 2026
3 checks passed
@ArthurCRodrigues ArthurCRodrigues deleted the 283-external-mode-web-api-contract-config-fetch-external-result-ingestion branch April 21, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[External Mode] M2M auth for integration endpoints [External Mode] Web API contract: config fetch + external result ingestion

3 participants