Repository navigation
feat(integrations): open GitHub issues through an installable GitHub App instead of a personal token - #75
feat(integrations): open GitHub issues through an installable GitHub App instead of a personal token#75YJack0000 wants to merge 10 commits into
Conversation
Signs the app JWT with the private key, mints installation tokens scoped to one repository with issues:write, and caches them until five minutes before they expire. Also lists the installation's repositories, exchanges an OAuth code for a user token, and lists the installations that user can see. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The processor now authenticates with an installation token instead of a personal access token pasted into the hook settings. It skips hooks that have no installation (left over from the token setup), no repository, or a pending reconnect. When GitHub answers 401, 403 without rate-limit headers, or 404, it flags the hook for reconnect instead of raising. The hook settings schema drops access_token and the generic settings form; reference_id holds the installation id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The integration card links to the app's install page with a signed, 15-minute state naming the account. GitHub returns the browser to /github/callback, which only checks the state and forwards code, installation_id and state to the settings page. The page posts them to POST /integrations/github from the admin's own session. Binding happens there, not in the callback, for two reasons. The state must name the current account, so an admin cannot hand their state to another organization's owner and capture that installation. The user token from the code must list the installation, because installation_id comes through a browser redirect and anyone can forge it. Reconnecting keeps the label, and keeps the repository only if the new installation still grants it. Administrators can list the installation's repositories, choose one plus an optional label (422 for a repository outside the installation), and unbind the installation. Hook JSON now reports reauthorization_required. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /webhooks/github verifies X-Hub-Signature-256 against GITHUB_APP_WEBHOOK_SECRET and answers 401 without it. An installation that is deleted or suspended, or that loses the hook's repository, marks every hook bound to it as needing a reconnect; unsuspend clears the flag. Replays converge on the same state, and other events are acknowledged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GitHub card now opens a dedicated page instead of the generic hook form. The page derives one status from the account's hook (not connected, reconnect, choose repository, connected), links Connect to the GitHub App install URL, lets an administrator pick the repository and optional issue label, and shows the install redirect's setup_action/error notice once before clearing the query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The install callback no longer binds the installation, because doing it server-side allowed a cross-account CSRF. The settings page now posts the redirect's code, installation_id and state to the integrations endpoint itself, clears the single-use query, and then loads the hook. A refused install shows the notice for the returned reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY and GITHUB_APP_WEBHOOK_SECRET as installation configs with a GitHub card in super admin. The private key is a code field, because a password input drops the PEM's newlines. The card stays hidden until all six are set; a missing webhook secret would leave uninstalls unnoticed. The integration description now says to connect the GitHub App. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
✅ SonarQube Quality Gate passed — pathorsAI_inbox0 open issues on this PR. |
ReviewThis is a strong PR. The security design is the standout: binding the installation from the admin's authenticated session instead of the callback (with the cross-account capture scenario documented in the code), verifying Findings, roughly by importance: 1. Stale
|
…bles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…count The connect state now lives an hour, and a genuine but expired one returns the admin to the GitHub settings page with a notice to press Connect again instead of the app root. A leftover personal-token hook can only be deleted, and a non-positive token TTL is no longer written to the cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Description
GitHub 整合(#31,建工單時開 issue)原本用某個人的 Personal Access Token,現在改成可安裝的 GitHub App「Pathors Inbox」。administrator 在整合頁按連接,到 GitHub 把 App 裝到自己的 org,回來後從下拉選單挑 repo、可選填 label,之後 issue 都由 App 開。系統裡不存任何長期的 GitHub token,PAT 的設定方式整個移除。對應票 pathorsAI/pathors#3521。
流程:
github.com/apps/<slug>/installations/new?state=<JWT>,state 用 HS256 簽{sub: account_id, exp: +15min},驗證時一定要帶expGET /github/callback。這裡不綁定,只驗 state,再把code、installation_id、state轉給/app/accounts/<id>/settings/integrations/githubPOST /api/v1/accounts/:id/integrations/github:state 的 account 必須等於Current.account→ 用 code 換 user token →GET /user/installations必須包含這個installation_id→ 寫進 hookissues: write,存在Rails.cache,到期前 5 分鐘換新)→POST /repos/{repo}/issues為什麼不在 callback 綁定:如果在 callback 綁定,account X 的 administrator 可以把自己的安裝連結(裡面有合法的 state)丟給別的 org 的 owner V;V 一裝,V 的 installation 就被綁到 X,X 就能列出 V 的 private repo、在那邊開 issue。改成在設定頁綁定之後,V 不是 X 的成員,在他的瀏覽器上這一步做不了。
新增:
Integrations::Github::AppClient:App JWT、installation token、repo 清單、OAuth code exchange、/user/installationsGithub::CallbacksController、Api::V1::Accounts::Integrations::GithubController(create/repositories/update/destroy,限 administrator)POST /webhooks/github:驗X-Hub-Signature-256。installation.deleted/suspend,或已選的 repo 被移出 installation →prompt_reauthorization!;unsuspend→reauthorized!reference_id存installation_id,settings只有{repository, label}Github.vue:未連接 / 等 org owner 核准 / 選 repo / 已連接 / 需要重新連接GITHUB_APP_ID、GITHUB_APP_SLUG、GITHUB_APP_CLIENT_ID、GITHUB_APP_CLIENT_SECRET、GITHUB_APP_PRIVATE_KEY(textarea;密碼欄位會吃掉換行,PEM 會壞掉)、GITHUB_APP_WEBHOOK_SECRET。六個都要有,整合卡片才會出現行為:
error=state_expired導回該 account 的 GitHub 設定頁,提示再按一次連接(GitHub 那邊可能已經裝好了);簽章不對的 state 一律導回首頁reference_id)會顯示成「未連接」,processor 直接跳過、不呼叫 GitHub。重新連接時會蓋掉 settings,把access_token清掉已知限制:
/user/installations只要使用者看得到 installation 裡任何一個 repo 就會列出,不限 org owner。所以 org 的一般成員也能把自己 org 的 installation 綁到他管理的 account;org 以外的人擋得住。GitHub 這個 endpoint 沒有提供 org 角色可以判斷Reauthorizable只對 Slack、Dialogflow 寄信),只有設定頁的提示上線前
https://inbox.pathors.com/github/callback;Webhookhttps://inbox.pathors.com/webhooks/github,Content type 選application/json(GitHub 預設是 form-urlencoded,簽章仍會通過,但解析會失敗,每次都回 500,解除安裝就不會被發現),只訂閱 Installation 和 Installation repositoriespathorsAI/pathors→ 在整合頁重新連接 → 到 GitHub 撤銷舊的 PATType of change
How Has This Been Tested?
RAILS_ENV=test):386 examples, 0 failures;修完 review 後再跑 GitHub 相關與 integrations 全部,260 examples, 0 failures。涵蓋spec/lib/integrations/github、spec/controllers/github、webhook controller、spec/controllers/api/v1/accounts/integrations全部、spec/models/integrations、spec/helpers、hook listener / jobinstallation_id不會寫入 hook;把驗證那段故意改成永遠通過,這個測試會失敗(1 failure),確認它真的有在檢查installation.deleted會讓 hook 需要重新連接;GitHub 回 404 時標記重新連接、不丟例外;沒選 repo 時不發任何 request;選了不在 installation 裡的 repo 回 422InstallationConfig,再從GlobalConfigService讀出來,能 parse 回同一把 keyspec/controllers/super_admin/app_config_controller_spec.rb在本機容器跑到一半被 OOM kill,交給 CI;設定頁還沒在瀏覽器實際打開過;GitHub App 還沒建,所以還沒對真的 GitHub 跑過Checklist:
🤖 Generated with Claude Code