Files
fleet/server/fleet/api_endpoints.go
T
Juan Fernandez 2b35eabd5d Added middleware for api-only users auth (#43772)
Fixes #42885

Added new middleware (APIOnlyEndpointCheck) that enforces 403 for
API-only users whose request either isn't in the API endpoint catalog or
falls outside their configured per-user endpoint restrictions.
2026-04-21 07:11:33 -04:00

100 lines
2.6 KiB
Go

package fleet
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
)
// APIEndpoint represents an API endpoint that we can attach permissions to
type APIEndpoint struct {
Method string `json:"method" yaml:"method"`
Path string `json:"path" yaml:"path"`
NormalizedPath string `json:"-"`
DisplayName string `json:"display_name" yaml:"display_name"`
Deprecated bool `json:"deprecated" yaml:"deprecated"`
}
// AuthzType implements authz.AuthzTyper.
func (e *APIEndpoint) AuthzType() string {
return "api_endpoint"
}
var validHTTPMethods = map[string]struct{}{
http.MethodGet: {},
http.MethodPost: {},
http.MethodPut: {},
http.MethodPatch: {},
http.MethodDelete: {},
}
// 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:[^}]+\}/`)
// NewAPIEndpointFromTpl creates a new APIEndpoint from the provided params.
// tpl is meant to be a route template as usually defined in the mux router,
// or a path using the /_version_/ placeholder convention.
func NewAPIEndpointFromTpl(method string, tpl string) APIEndpoint {
path := versionSegmentRe.ReplaceAllString(tpl, "/v1/")
path = strings.ReplaceAll(path, "/_version_/", "/v1/")
val := APIEndpoint{
Method: method,
Path: path,
}
val.normalize()
return val
}
// normalize method and path properties
func (e *APIEndpoint) normalize() {
e.Method = strings.ToUpper(e.Method)
segments := strings.Split(e.Path, "/")
n := 0
for i, seg := range segments {
if strings.HasPrefix(seg, ":") ||
(strings.HasPrefix(seg, "{") && strings.HasSuffix(seg, "}")) {
n++
segments[i] = fmt.Sprintf(":placeholder_%d", n)
}
}
e.NormalizedPath = strings.ToLower(strings.Join(segments, "/"))
}
// Fingerprint return a string that uniquely identifies
// the APIEndpoint
func (e APIEndpoint) Fingerprint() string {
return "|" + e.Method + "|" + e.NormalizedPath + "|"
}
func (e APIEndpoint) validate() error {
if strings.TrimSpace(e.DisplayName) == "" {
return errors.New("display_name is required")
}
if _, ok := validHTTPMethods[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 (e *APIEndpoint) UnmarshalJSON(data []byte) error {
// Use an alias to prevent infinite recursion.
type Alias APIEndpoint
var alias Alias
if err := json.Unmarshal(data, &alias); err != nil {
return err
}
*e = APIEndpoint(alias)
e.normalize()
return e.validate()
}