forgejo.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. """Forgejo backend — diverges from Gitea on token-scope validation (v15+)."""
  2. import logging
  3. import httpx
  4. from backend.app.services.git_providers.gitea import GiteaBackend
  5. logger = logging.getLogger(__name__)
  6. class ForgejoBackend(GiteaBackend):
  7. """Backend for Forgejo instances.
  8. Forgejo v15+ returns 404 (not 403) for private repositories when the token
  9. lacks repository scope, requiring a /user pre-check to distinguish bad tokens
  10. from inaccessible repos. test_connection is overridden to handle this.
  11. Other methods are inherited from GiteaBackend unchanged.
  12. """
  13. async def test_connection(self, repo_url: str, token: str, client: httpx.AsyncClient) -> dict:
  14. try:
  15. owner, repo = self.parse_repo_url(repo_url)
  16. api_base = self.get_api_base(repo_url)
  17. headers = self.get_headers(token)
  18. # Verify token validity before hitting the repo. On Forgejo v15+,
  19. # private repos return 404 (not 403) when the token lacks repo scope,
  20. # so we must distinguish "bad token" from "token OK but repo not visible".
  21. user_resp = await client.get(f"{api_base}/user", headers=headers)
  22. if user_resp.status_code == 401:
  23. return {"success": False, "message": "Invalid access token", "repo_name": None, "permissions": None}
  24. if user_resp.status_code == 403:
  25. return {
  26. "success": False,
  27. "message": "Token has no read:user scope; cannot validate identity",
  28. "repo_name": None,
  29. "permissions": None,
  30. }
  31. if user_resp.status_code != 200:
  32. return {
  33. "success": False,
  34. "message": f"Forgejo API error on /user: {user_resp.status_code}",
  35. "repo_name": None,
  36. "permissions": None,
  37. }
  38. repo_resp = await client.get(f"{api_base}/repos/{owner}/{repo}", headers=headers)
  39. if repo_resp.status_code == 404:
  40. return {
  41. "success": False,
  42. "message": (
  43. "Repository not found or token cannot access it. "
  44. "On Forgejo v15+, private repositories return 404 (not 403) "
  45. "when the token lacks repository scope."
  46. ),
  47. "repo_name": None,
  48. "permissions": None,
  49. }
  50. if repo_resp.status_code != 200:
  51. return {
  52. "success": False,
  53. "message": f"API error: {repo_resp.status_code}",
  54. "repo_name": None,
  55. "permissions": None,
  56. }
  57. data = repo_resp.json()
  58. permissions = data.get("permissions", {})
  59. if not permissions.get("push", False):
  60. return {
  61. "success": False,
  62. "message": "Token does not have push permission to this repository",
  63. "repo_name": data.get("full_name"),
  64. "permissions": permissions,
  65. }
  66. return {
  67. "success": True,
  68. "message": "Connection successful",
  69. "repo_name": data.get("full_name"),
  70. "permissions": permissions,
  71. }
  72. except Exception as e:
  73. logger.exception("Forgejo connection test failed")
  74. detail = str(e)[:200]
  75. message = (
  76. f"Connection failed: {type(e).__name__}: {detail}"
  77. if detail
  78. else f"Connection failed: {type(e).__name__}"
  79. )
  80. return {
  81. "success": False,
  82. "message": message,
  83. "repo_name": None,
  84. "permissions": None,
  85. }