Add OAuth2/OIDC per-user Gitea authentication
docker / lint (push) Has been cancelled
docker / test (push) Has been cancelled
docker / docker-build (push) Has been cancelled
lint / lint (push) Has been cancelled
test / test (push) Has been cancelled

Introduce a GiteaOAuthValidator for JWT and userinfo validation and
fallbacks, add /oauth/token proxy, and thread per-user tokens through
the
request context and automation paths. Update config and .env.example for
OAuth-first mode, add OpenAPI, extensive unit/integration tests,
GitHub/Gitea CI workflows, docs, and lint/test enforcement (>=80% cov).
This commit is contained in:
2026-02-25 16:54:01 +01:00
parent a00b6a0ba2
commit 59e1ea53a8
31 changed files with 2575 additions and 660 deletions
+13 -11
View File
@@ -19,7 +19,7 @@ class GiteaAuthenticationError(GiteaError):
class GiteaAuthorizationError(GiteaError):
"""Raised when bot user lacks permission for an operation."""
"""Raised when the authenticated user lacks permission for an operation."""
class GiteaNotFoundError(GiteaError):
@@ -27,19 +27,21 @@ class GiteaNotFoundError(GiteaError):
class GiteaClient:
"""Client for interacting with Gitea API as a bot user."""
"""Client for interacting with Gitea API as the authenticated end-user."""
def __init__(self, base_url: str | None = None, token: str | None = None) -> None:
def __init__(self, token: str, base_url: str | None = None) -> None:
"""Initialize Gitea client.
Args:
token: OAuth access token for the authenticated user.
base_url: Optional base URL override.
token: Optional token override.
"""
self.settings = get_settings()
self.audit = get_audit_logger()
self.base_url = (base_url or self.settings.gitea_base_url).rstrip("/")
self.token = token or self.settings.gitea_token
self.token = token.strip()
if not self.token:
raise ValueError("GiteaClient requires a non-empty per-user OAuth token")
self.client: AsyncClient | None = None
async def __aenter__(self) -> GiteaClient:
@@ -47,7 +49,7 @@ class GiteaClient:
self.client = AsyncClient(
base_url=self.base_url,
headers={
"Authorization": f"token {self.token}",
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
},
timeout=self.settings.request_timeout_seconds,
@@ -79,15 +81,15 @@ class GiteaClient:
severity="high",
metadata={"correlation_id": correlation_id},
)
raise GiteaAuthenticationError("Authentication failed - check bot token")
raise GiteaAuthenticationError("Authentication failed - user token rejected")
if response.status_code == 403:
self.audit.log_access_denied(
tool_name="gitea_api",
reason="bot user lacks permission",
reason="authenticated user lacks permission",
correlation_id=correlation_id,
)
raise GiteaAuthorizationError("Bot user lacks permission for this operation")
raise GiteaAuthorizationError("Authenticated user lacks permission for this operation")
if response.status_code == 404:
raise GiteaNotFoundError("Resource not found")
@@ -123,7 +125,7 @@ class GiteaClient:
return self._handle_response(response, correlation_id)
async def get_current_user(self) -> dict[str, Any]:
"""Get current bot user profile."""
"""Get current authenticated user profile."""
correlation_id = self.audit.log_tool_invocation(
tool_name="get_current_user",
result_status="pending",
@@ -146,7 +148,7 @@ class GiteaClient:
raise
async def list_repositories(self) -> list[dict[str, Any]]:
"""List all repositories visible to the bot user."""
"""List repositories visible to the authenticated user."""
correlation_id = self.audit.log_tool_invocation(
tool_name="list_repositories",
result_status="pending",