Merge branch 'main' into trustedproxies

This commit is contained in:
eikendev 2022-04-24 18:14:10 +02:00
commit 69d92f52c8
No known key found for this signature in database
GPG key ID: A1BDB1B28C8EF694
12 changed files with 176 additions and 28 deletions

View file

@ -82,7 +82,7 @@ func main() {
log.L.Fatal(err)
}
engine, err := router.Create(c.Debug, c.HTTP.TrustedProxies, cm, db, dp)
engine, err := router.Create(c.Debug, c.HTTP.TrustedProxies, cm, db, dp, &c.Alertmanager)
if err != nil {
log.L.Fatal(err)
}

View file

@ -63,3 +63,10 @@ crypto:
formatting:
# Whether to use colored titles based on the message priority (<0: grey, 0-3: default, 4-10: yellow, 10-20: orange, >20: red).
coloredtitle: false
# This settings are only relevant if you want to use PushBits with alertmanager
alertmanager:
# The name of the entry in the alerts annotations or lables that should be used for the title
annotationtitle: title
# The name of the entry in the alerts annotations or labels that should be used for the message
annotationmessage: message

View file

@ -0,0 +1,59 @@
package alertmanager
import (
"net/http"
"net/url"
"github.com/gin-gonic/gin"
"github.com/pushbits/server/internal/api"
"github.com/pushbits/server/internal/authentication"
"github.com/pushbits/server/internal/log"
"github.com/pushbits/server/internal/model"
)
type AlertmanagerHandler struct {
DP api.NotificationDispatcher
Settings AlertmanagerHandlerSettings
}
type AlertmanagerHandlerSettings struct {
TitleAnnotation string
MessageAnnotation string
}
// CreateAlert godoc
// @Summary Create an Alert
// @Description Creates an alert that is send to the channel as a notification. This endpoint is compatible with alertmanager webhooks.
// @ID post-alert
// @Tags Alertmanager
// @Accept json
// @Produce json
// @Param token query string true "Channels token, can also be provieded in the header"
// @Param data body model.AlertmanagerWebhook true "alertmanager webhook call"
// @Success 200 {object} []model.Notification
// @Failure 500,404,403 ""
// @Router /alert [post]
func (h *AlertmanagerHandler) CreateAlert(ctx *gin.Context) {
application := authentication.GetApplication(ctx)
log.L.Printf("Sending alert notification for application %s.", application.Name)
var hook model.AlertmanagerWebhook
if err := ctx.Bind(&hook); err != nil {
return
}
notifications := make([]model.Notification, len(hook.Alerts))
for i, alert := range hook.Alerts {
notification := alert.ToNotification(h.Settings.TitleAnnotation, h.Settings.MessageAnnotation)
notification.Sanitize(application)
messageID, err := h.DP.SendNotification(application, &notification)
if success := api.SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return
}
notification.ID = messageID
notification.UrlEncodedID = url.QueryEscape(messageID)
notifications[i] = notification
}
ctx.JSON(http.StatusOK, &notifications)
}

View file

@ -30,14 +30,14 @@ func (h *ApplicationHandler) registerApplication(ctx *gin.Context, a *model.Appl
log.L.Printf("Registering application %s.", a.Name)
channelID, err := h.DP.RegisterApplication(a.ID, a.Name, a.Token, u.MatrixID)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
a.MatrixID = channelID
err = h.DB.UpdateApplication(a)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
@ -53,13 +53,13 @@ func (h *ApplicationHandler) createApplication(ctx *gin.Context, u *model.User,
application.UserID = u.ID
err := h.DB.CreateApplication(&application)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return nil, err
}
if err := h.registerApplication(ctx, &application, u); err != nil {
err := h.DB.DeleteApplication(&application)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
log.L.Printf("Cannot delete application with ID %d.", application.ID)
}
@ -73,12 +73,12 @@ func (h *ApplicationHandler) deleteApplication(ctx *gin.Context, a *model.Applic
log.L.Printf("Deleting application %s (ID %d).", a.Name, a.ID)
err := h.DP.DeregisterApplication(a, u)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
err = h.DB.DeleteApplication(a)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
@ -100,12 +100,12 @@ func (h *ApplicationHandler) updateApplication(ctx *gin.Context, a *model.Applic
}
err := h.DB.UpdateApplication(a)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
err = h.DP.UpdateApplication(a)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
@ -164,7 +164,7 @@ func (h *ApplicationHandler) GetApplications(ctx *gin.Context) {
}
applications, err := h.DB.GetApplications(user)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return
}

View file

@ -38,7 +38,7 @@ func getApplication(ctx *gin.Context, db Database) (*model.Application, error) {
}
application, err := db.GetApplicationByID(id)
if success := successOrAbort(ctx, http.StatusNotFound, err); !success {
if success := SuccessOrAbort(ctx, http.StatusNotFound, err); !success {
return nil, err
}
@ -52,7 +52,7 @@ func getUser(ctx *gin.Context, db Database) (*model.User, error) {
}
user, err := db.GetUserByID(id)
if success := successOrAbort(ctx, http.StatusNotFound, err); !success {
if success := SuccessOrAbort(ctx, http.StatusNotFound, err); !success {
return nil, err
}

View file

@ -54,7 +54,7 @@ func (h *NotificationHandler) CreateNotification(ctx *gin.Context) {
notification.Sanitize(application)
messageID, err := h.DP.SendNotification(application, &notification)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return
}
@ -81,7 +81,7 @@ func (h *NotificationHandler) DeleteNotification(ctx *gin.Context) {
log.L.Printf("Deleting notification for application %s.", application.Name)
id, err := getMessageID(ctx)
if success := successOrAbort(ctx, http.StatusUnprocessableEntity, err); !success {
if success := SuccessOrAbort(ctx, http.StatusUnprocessableEntity, err); !success {
return
}
@ -90,7 +90,7 @@ func (h *NotificationHandler) DeleteNotification(ctx *gin.Context) {
Date: time.Now(),
}
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DP.DeleteNotification(application, &n)); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, h.DP.DeleteNotification(application, &n)); !success {
return
}

View file

@ -39,7 +39,7 @@ func (h *UserHandler) requireMultipleAdmins(ctx *gin.Context) error {
func (h *UserHandler) deleteApplications(ctx *gin.Context, u *model.User) error {
applications, err := h.DB.GetApplications(u)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
@ -56,7 +56,7 @@ func (h *UserHandler) deleteApplications(ctx *gin.Context, u *model.User) error
func (h *UserHandler) updateChannels(ctx *gin.Context, u *model.User, matrixID string) error {
applications, err := h.DB.GetApplications(u)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
@ -64,7 +64,7 @@ func (h *UserHandler) updateChannels(ctx *gin.Context, u *model.User, matrixID s
application := application // See https://stackoverflow.com/a/68247837
err := h.DP.DeregisterApplication(&application, u)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
}
@ -97,7 +97,7 @@ func (h *UserHandler) updateUser(ctx *gin.Context, u *model.User, updateUser mod
}
if updateUser.Password != nil {
hash, err := h.CM.CreatePasswordHash(*updateUser.Password)
if success := successOrAbort(ctx, http.StatusBadRequest, err); !success {
if success := SuccessOrAbort(ctx, http.StatusBadRequest, err); !success {
return err
}
@ -111,7 +111,7 @@ func (h *UserHandler) updateUser(ctx *gin.Context, u *model.User, updateUser mod
}
err := h.DB.UpdateUser(u)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return err
}
@ -150,7 +150,7 @@ func (h *UserHandler) CreateUser(ctx *gin.Context) {
user, err := h.DB.CreateUser(createUser)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return
}
@ -171,7 +171,7 @@ func (h *UserHandler) CreateUser(ctx *gin.Context) {
// @Router /user [get]
func (h *UserHandler) GetUsers(ctx *gin.Context) {
users, err := h.DB.GetUsers()
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, err); !success {
return
}
@ -238,7 +238,7 @@ func (h *UserHandler) DeleteUser(ctx *gin.Context) {
return
}
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DB.DeleteUser(user)); !success {
if success := SuccessOrAbort(ctx, http.StatusInternalServerError, h.DB.DeleteUser(user)); !success {
return
}

View file

@ -10,7 +10,7 @@ import (
"github.com/gin-gonic/gin"
)
func successOrAbort(ctx *gin.Context, code int, err error) bool {
func SuccessOrAbort(ctx *gin.Context, code int, err error) bool {
if err != nil {
// If we know the error force error code
switch err {

View file

@ -26,7 +26,7 @@ func TestApi_SuccessOrAbort(t *testing.T) {
w, c, err := testCase.GetRequest()
require.NoErrorf(err, "(Test case %s) Could not make request", testCase.Name)
aborted := successOrAbort(c, testCase.ShouldStatus, forcedErr)
aborted := SuccessOrAbort(c, testCase.ShouldStatus, forcedErr)
if forcedErr != nil {
assert.Equalf(testCase.ShouldStatus, w.Code, "(Test case %s) Expected status code %d but have %d", testCase.Name, testCase.ShouldStatus, w.Code)

View file

@ -33,6 +33,12 @@ type Matrix struct {
Password string `required:"true"`
}
// Alertmanager holds information on how to parse alertmanager calls
type Alertmanager struct {
AnnotationTitle string `default:"title"`
AnnotationMessage string `default:"message"`
}
// Configuration holds values that can be configured by the user.
type Configuration struct {
Debug bool `default:"false"`
@ -54,8 +60,9 @@ type Configuration struct {
Security struct {
CheckHIBP bool `default:"false"`
}
Crypto CryptoConfig
Formatting Formatting
Crypto CryptoConfig
Formatting Formatting
Alertmanager Alertmanager
}
func configFiles() []string {

View file

@ -0,0 +1,67 @@
package model
import "strings"
type AlertmanagerWebhook struct {
Version string `json:"version"`
GroupKey string `json:"groupKey"`
Receiver string `json:"receiver"`
GroupLabels map[string]string `json:"groupLabels"`
CommonLabels map[string]string `json:"commonLabels"`
CommonAnnotations map[string]string `json:"commonAnnotations"`
ExternalURL string `json:"externalURL"`
Alerts []AlertmanagerAlert `json:"alerts"`
}
type AlertmanagerAlert struct {
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
StartsAt string `json:"startsAt"`
EndsAt string `json:"endsAt"`
Status string `json:"status"`
}
func (alert *AlertmanagerAlert) ToNotification(titleAnnotation, messageAnnotation string) Notification {
title := strings.Builder{}
message := strings.Builder{}
switch alert.Status {
case "firing":
title.WriteString("[FIR] ")
case "resolved":
title.WriteString("[RES] ")
}
message.WriteString("STATUS: ")
message.WriteString(alert.Status)
message.WriteString("\n\n")
if titleString, ok := alert.Annotations[titleAnnotation]; ok {
title.WriteString(titleString)
} else if titleString, ok := alert.Labels[titleAnnotation]; ok {
title.WriteString(titleString)
} else {
title.WriteString("Unknown Title")
}
if messageString, ok := alert.Annotations[messageAnnotation]; ok {
message.WriteString(messageString)
} else if messageString, ok := alert.Labels[messageAnnotation]; ok {
message.WriteString(messageString)
} else {
message.WriteString("Unknown Message")
}
message.WriteString("\n")
for labelName, labelValue := range alert.Labels {
message.WriteString("\n")
message.WriteString(labelName)
message.WriteString(": ")
message.WriteString(labelValue)
}
return Notification{
Message: message.String(),
Title: title.String(),
}
}

View file

@ -5,15 +5,17 @@ import (
"github.com/gin-gonic/gin"
"github.com/pushbits/server/internal/api"
"github.com/pushbits/server/internal/api/alertmanager"
"github.com/pushbits/server/internal/authentication"
"github.com/pushbits/server/internal/authentication/credentials"
"github.com/pushbits/server/internal/configuration"
"github.com/pushbits/server/internal/database"
"github.com/pushbits/server/internal/dispatcher"
"github.com/pushbits/server/internal/log"
)
// Create a Gin engine and setup all routes.
func Create(debug bool, trustedProxies []string, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher) (*gin.Engine, error) {
func Create(debug bool, trustedProxies []string, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, alertmanagerConfig *configuration.Alertmanager) (*gin.Engine, error) {
log.L.Println("Setting up HTTP routes.")
if !debug {
@ -26,6 +28,10 @@ func Create(debug bool, trustedProxies []string, cm *credentials.Manager, db *da
healthHandler := api.HealthHandler{DB: db}
notificationHandler := api.NotificationHandler{DB: db, DP: dp}
userHandler := api.UserHandler{AH: &applicationHandler, CM: cm, DB: db, DP: dp}
alertmanagerHandler := alertmanager.AlertmanagerHandler{DP: dp, Settings: alertmanager.AlertmanagerHandlerSettings{
TitleAnnotation: alertmanagerConfig.AnnotationTitle,
MessageAnnotation: alertmanagerConfig.AnnotationMessage,
}}
r := gin.New()
r.Use(log.GinLogger(log.L), gin.Recovery())
@ -69,5 +75,7 @@ func Create(debug bool, trustedProxies []string, cm *credentials.Manager, db *da
userGroup.PUT("/:id", api.RequireIDInURI(), userHandler.UpdateUser)
}
r.POST("/alert", auth.RequireApplicationToken(), alertmanagerHandler.CreateAlert)
return r, nil
}