update controlplane router, router test, and runtime
This commit is contained in:
parent
73929de2bd
commit
1685e493dc
3 changed files with 86 additions and 0 deletions
|
|
@ -33,6 +33,7 @@ func newRouterWithStore(cfg config.Config, logger *slog.Logger, store storage.St
|
|||
HeartbeatWaitSec: cfg.ProtoSocketHeartbeatWait,
|
||||
}, logger)
|
||||
runtime := NewRuntimeWithStore(protoServer, store)
|
||||
runtime.WithSecretResolver(EnvWebhookSecretResolver{})
|
||||
registerProtoSocketHandlers(protoServer.Dispatcher(), runtime, protoServer)
|
||||
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ package controlplane
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
|
@ -321,6 +324,64 @@ func TestWebhookSubscriptionRegistersFilterWithoutLeakingSecretRef(t *testing.T)
|
|||
}
|
||||
}
|
||||
|
||||
func TestWebhookSubscriptionEnvSecretRefSignsDelivery(t *testing.T) {
|
||||
const secret = "router-secret"
|
||||
t.Setenv("GITO_WEBHOOK_SECRET", secret)
|
||||
|
||||
consumer := &fakeWebhookConsumer{}
|
||||
server := fakeWebhookServer(t, consumer)
|
||||
defer server.Close()
|
||||
|
||||
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
||||
registerBranchListener(t, router, `{"repo_id":"nomadcode","branch":"develop","provider":"forgejo"}`)
|
||||
|
||||
subReq := httptest.NewRequest(http.MethodPost, "/api/webhook-subscriptions", bytes.NewBufferString(`{
|
||||
"name": "nomadcode-develop-wakeup",
|
||||
"target_url": "`+server.URL+`",
|
||||
"events": ["branch.updated"],
|
||||
"repo_id": "nomadcode",
|
||||
"branch": "develop",
|
||||
"secret_ref": "env:GITO_WEBHOOK_SECRET"
|
||||
}`))
|
||||
subRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(subRec, subReq)
|
||||
if subRec.Code != http.StatusCreated {
|
||||
t.Fatalf("subscription status: got %d body=%s", subRec.Code, subRec.Body.String())
|
||||
}
|
||||
|
||||
pushReq := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push?repo_id=nomadcode", bytes.NewBufferString(`{
|
||||
"ref": "refs/heads/develop",
|
||||
"before": "111",
|
||||
"after": "222",
|
||||
"repository": {"name": "nomadcode", "full_name": "toki/nomadcode"},
|
||||
"commits": [{"id": "222", "modified": ["agent-roadmap/phase/a/milestones/b.md"]}]
|
||||
}`))
|
||||
pushReq.Header.Set("X-Forgejo-Event", "push")
|
||||
pushReq.Header.Set("X-Forgejo-Delivery", "delivery-1")
|
||||
pushRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(pushRec, pushReq)
|
||||
if pushRec.Code != http.StatusAccepted {
|
||||
t.Fatalf("push status: got %d body=%s", pushRec.Code, pushRec.Body.String())
|
||||
}
|
||||
|
||||
consumer.mu.Lock()
|
||||
received := append([]webhookReception(nil), consumer.received...)
|
||||
consumer.mu.Unlock()
|
||||
if len(received) != 1 {
|
||||
t.Fatalf("expected one webhook delivery, got %d", len(received))
|
||||
}
|
||||
rec := received[0]
|
||||
if rec.eventType != "branch.updated" || rec.deliveryID == "" {
|
||||
t.Fatalf("unexpected delivery headers: event=%q delivery=%q", rec.eventType, rec.deliveryID)
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = mac.Write(rec.bodyRaw)
|
||||
wantSignature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
||||
if rec.signature != wantSignature {
|
||||
t.Fatalf("signature: got %q want %q", rec.signature, wantSignature)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookSubscriptionValidatesTargetAndEvents(t *testing.T) {
|
||||
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -34,6 +35,29 @@ type WebhookSecretResolver interface {
|
|||
ResolveWebhookSecret(ctx context.Context, secretRef string) (string, bool, error)
|
||||
}
|
||||
|
||||
// EnvWebhookSecretResolver resolves subscription secret refs of the form
|
||||
// `env:VAR_NAME` from the process environment.
|
||||
type EnvWebhookSecretResolver struct{}
|
||||
|
||||
func (EnvWebhookSecretResolver) ResolveWebhookSecret(_ context.Context, secretRef string) (string, bool, error) {
|
||||
secretRef = strings.TrimSpace(secretRef)
|
||||
if !strings.HasPrefix(secretRef, "env:") {
|
||||
return "", false, nil
|
||||
}
|
||||
name := strings.TrimSpace(strings.TrimPrefix(secretRef, "env:"))
|
||||
if name == "" {
|
||||
return "", false, fmt.Errorf("env secret ref missing variable name")
|
||||
}
|
||||
if strings.ContainsAny(name, " \t\r\n=") {
|
||||
return "", false, fmt.Errorf("invalid env secret ref %q", secretRef)
|
||||
}
|
||||
secret, ok := os.LookupEnv(name)
|
||||
if !ok || secret == "" {
|
||||
return "", false, nil
|
||||
}
|
||||
return secret, true, nil
|
||||
}
|
||||
|
||||
type Runtime struct {
|
||||
mu sync.Mutex
|
||||
watches map[string]BranchWatch
|
||||
|
|
|
|||
Loading…
Reference in a new issue