mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 08:17:26 +01:00
144 lines
5.4 KiB
Go
144 lines
5.4 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
)
|
|
|
|
func runWebhookIntegration(t *testing.T, base *Server) {
|
|
t.Run("signed webhook admission and complete grant reconciliation", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
db := base.Store.DB.Client().Database("webhooks_" + bson.NewObjectID().Hex())
|
|
defer db.Drop(ctx)
|
|
s := &Server{Config: base.Config, Store: &Store{DB: db, Redis: base.Store.Redis}}
|
|
s.Config.AppEnabled = true
|
|
s.Config.AppID = "123"
|
|
s.Config.AppWebhookSecret = "synthetic-webhook-secret"
|
|
calls := 0
|
|
s.HTTP = &http.Client{Transport: roundTrip(func(r *http.Request) (*http.Response, error) {
|
|
if r.Method != "GET" || r.URL.Path != "/user" {
|
|
t.Fatal("unexpected upstream request", r.Method, r.URL)
|
|
}
|
|
calls++
|
|
return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"id":789}`))}, nil
|
|
})}
|
|
send := func(event string, payload M, tampered bool) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(s.Config.AppWebhookSecret))
|
|
mac.Write(body)
|
|
signature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
if tampered {
|
|
body = append(body, ' ')
|
|
}
|
|
r := httptest.NewRequest("POST", "/github/app/webhook", bytes.NewReader(body))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
r.Header.Set("X-Hub-Signature-256", signature)
|
|
r.Header.Set("X-GitHub-Event", event)
|
|
w := httptest.NewRecorder()
|
|
s.webhook(w, r)
|
|
return w
|
|
}
|
|
for _, event := range []string{"installation", "installation_repositories"} {
|
|
for i, id := range []any{nil, "42", true, 1.5, 9007199254740992.0, -9007199254740992.0, 0, -1, 42, 9007199254740991.0} {
|
|
payload := M{"action": "deleted", "installation": M{"id": id, "app_id": 123}}
|
|
w := send(event, payload, false)
|
|
if w.Code != 204 {
|
|
t.Fatal(event, id, w.Code, w.Body.String())
|
|
}
|
|
count, err := db.Collection("githubinstallations").CountDocuments(ctx, M{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected := int64(0)
|
|
if i >= 6 {
|
|
expected = 1
|
|
}
|
|
if count != expected {
|
|
t.Fatal("unsafe installation ID accepted or safe ID skipped", id, count)
|
|
}
|
|
if expected == 1 {
|
|
row, err := s.Store.one(ctx, "githubinstallations", M{})
|
|
if err != nil || !boolean(row["blocked"]) || boolean(row["reconciliationPending"]) || str(row["revision"]) == "" {
|
|
t.Fatal("deletion did not durably block installation", row, err)
|
|
}
|
|
}
|
|
if _, err := db.Collection("githubinstallations").DeleteMany(ctx, M{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
// A forged payload and an installation belonging to another App cannot mutate state.
|
|
for _, tampered := range []bool{false, true} {
|
|
w := send("installation", M{"action": "deleted", "installation": M{"id": 42, "app_id": 456}}, tampered)
|
|
expected := 204
|
|
if tampered {
|
|
expected = 401
|
|
}
|
|
if w.Code != expected {
|
|
t.Fatal("signature/App isolation", w.Code)
|
|
}
|
|
count, err := db.Collection("githubinstallations").CountDocuments(ctx, M{})
|
|
if err != nil || count != 0 {
|
|
t.Fatal("foreign installation persisted", count, err)
|
|
}
|
|
}
|
|
// GitHub permits payloads above the former Go-only 1 MiB boundary.
|
|
if w := send("ping", M{"padding": strings.Repeat("x", 1536*1024)}, false); w.Code != 204 {
|
|
t.Fatal("valid large payload rejected", w.Code)
|
|
}
|
|
if w := send("ping", M{"padding": strings.Repeat("x", 2*1024*1024)}, false); w.Code < 400 {
|
|
t.Fatal("oversized payload accepted")
|
|
}
|
|
// Missing/falsy sender IDs must not reconcile users with empty provider IDs.
|
|
owner := bson.NewObjectID()
|
|
if _, err := db.Collection("users").InsertOne(ctx, M{"_id": owner, "status": "active", "externalIDs": M{"github": ""}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Collection("credentials").InsertOne(ctx, M{"ownerId": owner, "provider": "github-app-user", "revision": "invalid-envelope"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, id := range []any{nil, false, 0, ""} {
|
|
if w := send("github_app_authorization", M{"action": "revoked", "sender": M{"id": id}}, false); w.Code != 204 {
|
|
t.Fatal("falsy sender processed", id, w.Code)
|
|
}
|
|
}
|
|
for i := 0; i < 101; i++ {
|
|
owner := bson.NewObjectID()
|
|
if _, err := db.Collection("users").InsertOne(ctx, M{"_id": owner, "username": owner.Hex(), "status": "active", "externalIDs": M{"github": "789"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
q := &request{Server: s, R: httptest.NewRequest("GET", "/", nil)}
|
|
if err := s.saveGrant(q, owner, "github-app-user", M{"access_token": "synthetic-token", "refresh_token": "synthetic-refresh", "expires_in": float64(3600), "refresh_token_expires_in": float64(7200)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for replay := 0; replay < 2; replay++ {
|
|
if w := send("github_app_authorization", M{"action": "revoked", "sender": M{"id": 789}}, false); w.Code != 204 {
|
|
t.Fatal("grant reconciliation failed", w.Code, w.Body.String())
|
|
}
|
|
if calls != 101*(replay+1) {
|
|
t.Fatal("grant reconciliation truncated", calls)
|
|
}
|
|
count, err := db.Collection("credentials").CountDocuments(ctx, M{"revoked": false, "reconciliationPending": false})
|
|
if err != nil || count != 101 {
|
|
t.Fatal("working grants invalidated by replay", count, err)
|
|
}
|
|
}
|
|
})
|
|
}
|