**feat(webhook):** add WEBHOOK_SECRET for token authentication support
All checks were successful
CI / build-test (push) Successful in 1m28s

- Protect `/webhook` endpoint using the `Authorization` header
- Update `README.md` with setup instructions and examples for authentication
- Warn when `WEBHOOK_SECRET` is not configured
- Add tests for valid, missing, and invalid token scenarios
- Update `docker-compose.yml` to support `WEBHOOK_SECRET` configuration
This commit is contained in:
2026-02-27 14:58:43 +01:00
parent db9f47649d
commit c0746a7f02
7 changed files with 114 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
package diunwebhook
import (
"crypto/subtle"
"database/sql"
"encoding/json"
"log"
@@ -45,10 +46,15 @@ type UpdateEntry struct {
}
var (
mu sync.Mutex
db *sql.DB
mu sync.Mutex
db *sql.DB
webhookSecret string
)
func SetWebhookSecret(secret string) {
webhookSecret = secret
}
func InitDB(path string) error {
var err error
db, err = sql.Open("sqlite", path)
@@ -155,6 +161,14 @@ func GetUpdates() (map[string]UpdateEntry, error) {
}
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
if webhookSecret != "" {
auth := r.Header.Get("Authorization")
if subtle.ConstantTimeCompare([]byte(auth), []byte(webhookSecret)) != 1 {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return

View File

@@ -76,6 +76,67 @@ func TestWebhookHandler(t *testing.T) {
}
}
func TestWebhookHandler_Unauthorized(t *testing.T) {
diun.UpdatesReset()
diun.SetWebhookSecret("my-secret")
defer diun.ResetWebhookSecret()
event := diun.DiunEvent{Image: "nginx:latest"}
body, _ := json.Marshal(event)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
rec := httptest.NewRecorder()
diun.WebhookHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}
func TestWebhookHandler_WrongToken(t *testing.T) {
diun.UpdatesReset()
diun.SetWebhookSecret("my-secret")
defer diun.ResetWebhookSecret()
event := diun.DiunEvent{Image: "nginx:latest"}
body, _ := json.Marshal(event)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
req.Header.Set("Authorization", "wrong-token")
rec := httptest.NewRecorder()
diun.WebhookHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}
func TestWebhookHandler_ValidToken(t *testing.T) {
diun.UpdatesReset()
diun.SetWebhookSecret("my-secret")
defer diun.ResetWebhookSecret()
event := diun.DiunEvent{Image: "nginx:latest"}
body, _ := json.Marshal(event)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
req.Header.Set("Authorization", "my-secret")
rec := httptest.NewRecorder()
diun.WebhookHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rec.Code)
}
}
func TestWebhookHandler_NoSecretConfigured(t *testing.T) {
diun.UpdatesReset()
diun.ResetWebhookSecret()
event := diun.DiunEvent{Image: "nginx:latest"}
body, _ := json.Marshal(event)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
rec := httptest.NewRecorder()
diun.WebhookHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200 (no secret configured), got %d", rec.Code)
}
}
func TestWebhookHandler_BadRequest(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader([]byte("not-json")))
rec := httptest.NewRecorder()

View File

@@ -13,3 +13,7 @@ func ResetTags() {
db.Exec(`DELETE FROM tag_assignments`)
db.Exec(`DELETE FROM tags`)
}
func ResetWebhookSecret() {
SetWebhookSecret("")
}