fleet/server/service/api_endpoints.go
Juan Fernandez 3df6449426
API endpoints initial models (#42881)
**Related issue:** Resolves #42881

- Added user_api_endpoints table to track per user API endpoint
permissions.
- Added service/api_endpoints, used to handle service/api_endpoints.yml
artifact.
- Added check on server start that makes sure that
service/apin_endpoints.yml is a subset of router routes.
2026-04-07 10:40:39 -04:00

111 lines
2.8 KiB
Go

package service
import (
_ "embed"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"github.com/gorilla/mux"
"gopkg.in/yaml.v2"
)
//go:embed api_endpoints.yml
var apiEndpointsYAML []byte
var apiEndpoints = mustParseAPIEndpoints()
// APIEndpoint represents an API endpoint that we can attach permissions to.
type APIEndpoint struct {
Method string `yaml:"method"`
Path string `yaml:"path"`
Name string `yaml:"name"`
Deprecated bool `yaml:"deprecated"`
}
var validHTTPMethods = map[string]struct{}{
http.MethodGet: {},
http.MethodPost: {},
http.MethodPut: {},
http.MethodPatch: {},
http.MethodDelete: {},
}
func (e APIEndpoint) validate() error {
if strings.TrimSpace(e.Name) == "" {
return errors.New("name is required")
}
if _, ok := validHTTPMethods[strings.ToUpper(e.Method)]; !ok {
return fmt.Errorf("invalid HTTP method %q", e.Method)
}
if strings.TrimSpace(e.Path) == "" {
return errors.New("path is required")
}
return nil
}
func mustParseAPIEndpoints() []APIEndpoint {
var routes []APIEndpoint
if err := yaml.Unmarshal(apiEndpointsYAML, &routes); err != nil {
panic(fmt.Sprintf("api_endpoints.yml: failed to parse: %v", err))
}
for i, r := range routes {
if err := r.validate(); err != nil {
panic(fmt.Sprintf("api_endpoints.yml: entry %d: %v", i, err))
}
// Normalise method to upper-case so callers don't have to.
routes[i].Method = strings.ToUpper(r.Method)
}
return routes
}
// GetAPIEndpoints returns all routes defined in api_endpoints.yml.
func GetAPIEndpoints() []APIEndpoint {
return apiEndpoints
}
// versionSegmentRe matches the gorilla/mux version segment that attachFleetAPIRoutes
// inserts in place of /_version_/ (e.g. /{fleetversion:(?:v1|2022-04|latest)}/).
var versionSegmentRe = regexp.MustCompile(`/\{fleetversion:[^}]+\}/`)
// ValidateAPIEndpoints checks that every route declared in api_endpoints.yml is
// registered in h.
func ValidateAPIEndpoints(h http.Handler) error {
r, ok := h.(*mux.Router)
if !ok {
return fmt.Errorf("expected *mux.Router, got %T", h)
}
registered := make(map[string]struct{})
_ = r.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
tpl, err := route.GetPathTemplate()
if err != nil {
return nil
}
meths, err := route.GetMethods()
if err != nil || len(meths) == 0 {
return nil
}
normalized := versionSegmentRe.ReplaceAllString(tpl, "/_version_/")
for _, m := range meths {
registered[m+":"+normalized] = struct{}{}
}
return nil
})
var missing []string
for _, route := range GetAPIEndpoints() {
key := route.Method + ":" + route.Path
if _, ok := registered[key]; !ok {
missing = append(missing, route.Method+" "+route.Path)
}
}
if len(missing) > 0 {
return fmt.Errorf("the following API endpoints are missing: %v", missing)
}
return nil
}