diff --git a/apiserver/controllers/controllers.go b/apiserver/controllers/controllers.go index 70a56bd5..d19b82b4 100644 --- a/apiserver/controllers/controllers.go +++ b/apiserver/controllers/controllers.go @@ -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")) diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index f14ffef4..2a93980c 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -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") diff --git a/auth/admin_required.go b/auth/admin_required.go new file mode 100644 index 00000000..8ab6cbac --- /dev/null +++ b/auth/admin_required.go @@ -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) + }) +} diff --git a/config/config.go b/config/config.go index a65c4668..f0751599 100644 --- a/config/config.go +++ b/config/config.go @@ -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") } diff --git a/params/params.go b/params/params.go index 2f0f7371..5eb04e89 100644 --- a/params/params.go +++ b/params/params.go @@ -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"`