Add admin required middleware and webhook endpoint

* Add a new middleware that tests for admin access
  * Add a new controller ID suffixed webhook endpoint. This will be used
    to accept webhook events on a webhook URL that is suffixed with our own
    controller ID.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2023-08-13 06:01:28 +00:00
parent aa2b42fddb
commit f2796f1d5a
No known key found for this signature in database
GPG key ID: 7D073DCC2C074CB5
5 changed files with 57 additions and 7 deletions

View file

@ -30,11 +30,16 @@ import (
"github.com/cloudbase/garm/runner"
wsWriter "github.com/cloudbase/garm/websocket"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
func NewAPIController(r *runner.Runner, authenticator *auth.Authenticator, hub *wsWriter.Hub) (*APIController, error) {
controllerInfo, err := r.GetControllerInfo(auth.GetAdminContext())
if err != nil {
return nil, errors.Wrap(err, "failed to get controller info")
}
return &APIController{
r: r,
auth: authenticator,
@ -43,14 +48,16 @@ func NewAPIController(r *runner.Runner, authenticator *auth.Authenticator, hub *
ReadBufferSize: 1024,
WriteBufferSize: 16384,
},
controllerID: controllerInfo.ShortControllerID(),
}, nil
}
type APIController struct {
r *runner.Runner
auth *auth.Authenticator
hub *wsWriter.Hub
upgrader websocket.Upgrader
r *runner.Runner
auth *auth.Authenticator
hub *wsWriter.Hub
upgrader websocket.Upgrader
controllerID string
}
func handleError(w http.ResponseWriter, err error) {
@ -138,7 +145,19 @@ func (a *APIController) handleWorkflowJobEvent(w http.ResponseWriter, r *http.Re
labelValues = a.webhookMetricLabelValues("true", "")
}
func (a *APIController) CatchAll(w http.ResponseWriter, r *http.Request) {
func (a *APIController) WebhookHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
controllerID, ok := vars["controllerID"]
// If the webhook URL includes a controller ID, we validate that it's meant for us. We still
// support bare webhook URLs, which are tipically configured manually by the user.
// The controllerID suffixed webhook URL is useful when configuring the webhook for an entity
// via garm. We cannot tag a webhook URL on github, so there is no way to determine ownership.
// Using a controllerID suffix is a simple way to denote ownership.
if ok && controllerID != a.controllerID {
log.Printf("ignoring webhook meant for controller %s", util.SanitizeLogEntry(controllerID))
return
}
headers := r.Header.Clone()
event := runnerParams.Event(headers.Get("X-Github-Event"))

View file

@ -89,8 +89,9 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
// Handles github webhooks
webhookRouter := router.PathPrefix("/webhooks").Subrouter()
webhookRouter.PathPrefix("/").Handler(http.HandlerFunc(han.CatchAll))
webhookRouter.PathPrefix("").Handler(http.HandlerFunc(han.CatchAll))
webhookRouter.Handle("/", http.HandlerFunc(han.WebhookHandler))
webhookRouter.Handle("", http.HandlerFunc(han.WebhookHandler))
webhookRouter.Handle("/{controllerID:controllerID\\/?}", http.HandlerFunc(han.WebhookHandler))
// Handles API calls
apiSubRouter := router.PathPrefix("/api/v1").Subrouter()
@ -118,6 +119,7 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
apiRouter := apiSubRouter.PathPrefix("").Subrouter()
apiRouter.Use(initMiddleware.Middleware)
apiRouter.Use(authMiddleware.Middleware)
apiRouter.Use(auth.AdminRequiredMiddleware)
// Metrics Token
apiRouter.Handle("/metrics-token/", http.HandlerFunc(han.MetricsTokenHandler)).Methods("GET", "OPTIONS")

14
auth/admin_required.go Normal file
View file

@ -0,0 +1,14 @@
package auth
import "net/http"
func AdminRequiredMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !IsAdmin(ctx) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}

View file

@ -110,6 +110,9 @@ type Default struct {
// MetadataURL is the URL where instances can fetch information they may need
// to set themselves up.
MetadataURL string `toml:"metadata_url" json:"metadata-url"`
// WebhookURL is the URL that will be installed as a webhook target in github.
WebhookURL string `toml:"webhook_url" json:"webhook-url"`
// LogFile is the location of the log file.
LogFile string `toml:"log_file,omitempty" json:"log-file"`
EnableLogStreamer bool `toml:"enable_log_streamer"`
@ -128,6 +131,7 @@ func (d *Default) Validate() error {
if d.MetadataURL == "" {
return fmt.Errorf("missing metadata-url")
}
if _, err := url.Parse(d.MetadataURL); err != nil {
return errors.Wrap(err, "validating metadata_url")
}

View file

@ -16,6 +16,7 @@ package params
import (
"encoding/json"
"math/big"
"time"
commonParams "github.com/cloudbase/garm-provider-common/params"
@ -385,6 +386,16 @@ type ControllerInfo struct {
CallbackURL string `json:"callback_url"`
}
func (c ControllerInfo) ShortControllerID() string {
var i big.Int
i.SetBytes(c.ControllerID[:])
id := i.Text(62)
if id == "0" {
return ""
}
return id
}
type GithubCredentials struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`