mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Implement GET /api/v1/fleet/rest_api (#42883)
**Related issue:** Resolves #42883 Added a new premium GET /api/_version_/fleet/rest_api endpoint that returns the contents of the embedded `api_endpoints.yml` artifact.
This commit is contained in:
parent
d8bd213e4c
commit
1bc32467a7
21 changed files with 1211 additions and 238 deletions
|
|
@ -0,0 +1 @@
|
|||
* Added a new premium GET /api/_version_/fleet/rest_api endpoint that returns the contents of the embedded `api_endpoints.yml` artifact.
|
||||
|
|
@ -41,6 +41,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/acl/activityacl"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
activity_bootstrap "github.com/fleetdm/fleet/v4/server/activity/bootstrap"
|
||||
apiendpoints "github.com/fleetdm/fleet/v4/server/api_endpoints"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
configpkg "github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
|
|
@ -1483,8 +1484,8 @@ func runServeCmd(cmd *cobra.Command, configManager configpkg.Manager, debug, dev
|
|||
apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore, redisPool, carveStore,
|
||||
[]endpointer.HandlerRoutesFunc{android_service.GetRoutes(svc, androidSvc), activityRoutes, acmeRoutes}, extra...)
|
||||
|
||||
if err := service.ValidateAPIEndpoints(apiHandler); err != nil {
|
||||
panic(fmt.Sprintf("invalid api_endpoints.yml: %v", err))
|
||||
if err := apiendpoints.Init(apiHandler); err != nil {
|
||||
panic(fmt.Sprintf("error initializing API endpoints: %v", err))
|
||||
}
|
||||
|
||||
if serveCSP {
|
||||
|
|
|
|||
16
ee/server/service/api_endpoints.go
Normal file
16
ee/server/service/api_endpoints.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiendpoints "github.com/fleetdm/fleet/v4/server/api_endpoints"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
func (svc *Service) ListAPIEndpoints(ctx context.Context) ([]fleet.APIEndpoint, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.APIEndpoint{}, fleet.ActionRead); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "authorize list API endpoints")
|
||||
}
|
||||
return apiendpoints.GetAPIEndpoints(), nil
|
||||
}
|
||||
85
server/api_endpoints/api_endpoints.go
Normal file
85
server/api_endpoints/api_endpoints.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package apiendpoints
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
//go:embed api_endpoints.yml
|
||||
var apiEndpointsYAML []byte
|
||||
|
||||
var apiEndpoints []fleet.APIEndpoint
|
||||
|
||||
// GetAPIEndpoints returns a copy of the embedded API endpoints slice.
|
||||
func GetAPIEndpoints() []fleet.APIEndpoint {
|
||||
result := make([]fleet.APIEndpoint, len(apiEndpoints))
|
||||
copy(result, apiEndpoints)
|
||||
return result
|
||||
}
|
||||
|
||||
func Init(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
|
||||
}
|
||||
for _, m := range meths {
|
||||
val := fleet.NewAPIEndpointFromTpl(m, tpl)
|
||||
registered[val.Fingerprint()] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
loadedApiEndpoints, err := loadGetAPIEndpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for _, e := range loadedApiEndpoints {
|
||||
if _, ok := registered[e.Fingerprint()]; !ok {
|
||||
missing = append(missing, e.Method+" "+e.Path)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("the following API endpoints are unknown: %v", missing)
|
||||
}
|
||||
|
||||
apiEndpoints = loadedApiEndpoints
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadGetAPIEndpoints() ([]fleet.APIEndpoint, error) {
|
||||
endpoints := make([]fleet.APIEndpoint, 0)
|
||||
|
||||
if err := yaml.Unmarshal(apiEndpointsYAML, &endpoints); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(endpoints))
|
||||
for _, e := range endpoints {
|
||||
fp := e.Fingerprint()
|
||||
if _, ok := seen[fp]; ok {
|
||||
panic(fmt.Errorf("duplicate entry (%s, %s)", e.Method, e.Path))
|
||||
}
|
||||
seen[fp] = struct{}{}
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
668
server/api_endpoints/api_endpoints.yml
Normal file
668
server/api_endpoints/api_endpoints.yml
Normal file
|
|
@ -0,0 +1,668 @@
|
|||
- method: "POST"
|
||||
path: "/api/v1/fleet/trigger"
|
||||
display_name: "Trigger scheduled tasks"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/me"
|
||||
display_name: "Get current user"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/sessions/:id"
|
||||
display_name: "Get session info"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/sessions/:id"
|
||||
display_name: "Delete session"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/config/certificate"
|
||||
display_name: "Get config certificate"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/config"
|
||||
display_name: "Get app config"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/config"
|
||||
display_name: "Modify app config"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/spec/enroll_secret"
|
||||
display_name: "Apply enroll secret spec"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/spec/enroll_secret"
|
||||
display_name: "Get enroll secret spec"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/version"
|
||||
display_name: "Get version"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/translate"
|
||||
display_name: "Translate"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/spec/fleets"
|
||||
display_name: "Apply fleet specs"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/secrets"
|
||||
display_name: "Modify fleet enroll secrets"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/fleets"
|
||||
display_name: "Create fleet"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/fleets"
|
||||
display_name: "List fleets"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/fleets/:id"
|
||||
display_name: "Get fleet"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/fleets/:id"
|
||||
display_name: "Modify fleet"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/fleets/:id"
|
||||
display_name: "Delete fleet"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/fleets/:id/agent_options"
|
||||
display_name: "Modify fleet agent options"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/fleets/:id/users"
|
||||
display_name: "List fleet users"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/users"
|
||||
display_name: "List users"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/users/:id"
|
||||
display_name: "Get user"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/targets"
|
||||
display_name: "Search targets"
|
||||
deprecated: true
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/targets/count"
|
||||
display_name: "Count targets"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/invites"
|
||||
display_name: "Create invite"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/invites"
|
||||
display_name: "List invites"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/invites/:id"
|
||||
display_name: "Delete invite"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/invites/:id"
|
||||
display_name: "Update invite"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/global/policies"
|
||||
display_name: "Create global policy"
|
||||
deprecated: true
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/policies"
|
||||
display_name: "Create policy"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/global/policies"
|
||||
display_name: "List global policies"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/policies"
|
||||
display_name: "List policies"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/policies/count"
|
||||
display_name: "Count policies"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/global/policies/:policy_id"
|
||||
display_name: "Get global policy"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/policies/:policy_id"
|
||||
display_name: "Get policy"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/global/policies/delete"
|
||||
display_name: "Delete global policies"
|
||||
deprecated: true
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/policies/delete"
|
||||
display_name: "Delete policies"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/global/policies/:policy_id"
|
||||
display_name: "Modify global policy"
|
||||
deprecated: true
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/policies/:policy_id"
|
||||
display_name: "Modify policy"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/automations/reset"
|
||||
display_name: "Reset automations"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/policies"
|
||||
display_name: "Create fleet policy"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/policies"
|
||||
display_name: "List fleet policies"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/policies/count"
|
||||
display_name: "Count fleet policies"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/policies/:policy_id"
|
||||
display_name: "Get fleet policy"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/policies/delete"
|
||||
display_name: "Delete fleet policies"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/policies/:policy_id"
|
||||
display_name: "Modify fleet policy"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/spec/policies"
|
||||
display_name: "Apply policy specs"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/certificates"
|
||||
display_name: "Create certificate template"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/certificates"
|
||||
display_name: "List certificate templates"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/certificates/:id"
|
||||
display_name: "Get certificate template"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/certificates/:id"
|
||||
display_name: "Delete certificate template"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/spec/certificates"
|
||||
display_name: "Apply certificate template specs"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/spec/certificates"
|
||||
display_name: "Delete certificate template specs"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/reports/:id"
|
||||
display_name: "Get report"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/reports"
|
||||
display_name: "List reports"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/reports/:id/report"
|
||||
display_name: "Get report results"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/reports"
|
||||
display_name: "Create report"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/reports/:id"
|
||||
display_name: "Modify report"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/reports/:name"
|
||||
display_name: "Delete report"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/reports/id/:id"
|
||||
display_name: "Delete report by ID"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/reports/delete"
|
||||
display_name: "Delete reports"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/spec/reports"
|
||||
display_name: "Apply report specs"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/spec/reports"
|
||||
display_name: "Get report specs"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/spec/reports/:name"
|
||||
display_name: "Get report spec"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/packs/:id"
|
||||
display_name: "Get pack"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/packs"
|
||||
display_name: "Create pack"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/packs/:id"
|
||||
display_name: "Modify pack"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/packs"
|
||||
display_name: "List packs"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/packs/:name"
|
||||
display_name: "Delete pack"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/packs/id/:id"
|
||||
display_name: "Delete pack by ID"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/spec/packs"
|
||||
display_name: "Apply pack specs"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/spec/packs"
|
||||
display_name: "Get pack specs"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/spec/packs/:name"
|
||||
display_name: "Get pack spec"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/versions"
|
||||
display_name: "List software versions"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/versions/:id"
|
||||
display_name: "Get software version"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software"
|
||||
display_name: "List software"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/:id"
|
||||
display_name: "Get software"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/count"
|
||||
display_name: "Count software"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/titles"
|
||||
display_name: "List software titles"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/titles/:id"
|
||||
display_name: "Get software title"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:host_id/software/:software_title_id/install"
|
||||
display_name: "Install software"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:host_id/software/:software_title_id/uninstall"
|
||||
display_name: "Uninstall software"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/titles/:title_id/package"
|
||||
display_name: "Get software installer"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/software/titles/:title_id/package/token"
|
||||
display_name: "Get software installer token"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/software/package"
|
||||
display_name: "Upload software installer"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/software/titles/:id/name"
|
||||
display_name: "Update software name"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/software/titles/:id/package"
|
||||
display_name: "Update software installer"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/software/titles/:title_id/available_for_install"
|
||||
display_name: "Delete software installer"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/install/:install_uuid/results"
|
||||
display_name: "Get software install results"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/software/batch"
|
||||
display_name: "Batch set software installers"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/batch/:request_uuid"
|
||||
display_name: "Get batch software installers result"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/titles/:title_id/icon"
|
||||
display_name: "Get software title icon"
|
||||
- method: "PUT"
|
||||
path: "/api/v1/fleet/software/titles/:title_id/icon"
|
||||
display_name: "Set software title icon"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/software/titles/:title_id/icon"
|
||||
display_name: "Delete software title icon"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/app_store_apps"
|
||||
display_name: "Get app store apps"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/software/app_store_apps"
|
||||
display_name: "Add app store app"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/software/titles/:title_id/app_store_app"
|
||||
display_name: "Update app store app"
|
||||
- method: "PUT"
|
||||
path: "/api/v1/fleet/setup_experience/software"
|
||||
display_name: "Set setup experience software"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/setup_experience/software"
|
||||
display_name: "Get setup experience software"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/setup_experience/script"
|
||||
display_name: "Get setup experience script"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/setup_experience/script"
|
||||
display_name: "Set setup experience script"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/setup_experience/script"
|
||||
display_name: "Delete setup experience script"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/software/fleet_maintained_apps"
|
||||
display_name: "Add fleet-maintained app"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/fleet_maintained_apps"
|
||||
display_name: "List fleet-maintained apps"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/software/fleet_maintained_apps/:app_id"
|
||||
display_name: "Get fleet-maintained app"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/vulnerabilities"
|
||||
display_name: "List vulnerabilities"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/vulnerabilities/:cve"
|
||||
display_name: "Get vulnerability"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/host_summary"
|
||||
display_name: "Get host summary"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts"
|
||||
display_name: "List hosts"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/delete"
|
||||
display_name: "Delete hosts"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id"
|
||||
display_name: "Get host"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/count"
|
||||
display_name: "Count hosts"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/search"
|
||||
display_name: "Search hosts"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/identifier/:identifier"
|
||||
display_name: "Get host by identifier"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/identifier/:identifier/query"
|
||||
display_name: "Run live query on host by identifier"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/query"
|
||||
display_name: "Run live query on host"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/hosts/:id"
|
||||
display_name: "Delete host"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/transfer"
|
||||
display_name: "Transfer hosts to a team"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/transfer/filter"
|
||||
display_name: "Transfer hosts to a team by filter"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/refetch"
|
||||
display_name: "Refetch host"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/device_mapping"
|
||||
display_name: "List host device mapping"
|
||||
deprecated: true
|
||||
- method: "PUT"
|
||||
path: "/api/v1/fleet/hosts/:id/device_mapping"
|
||||
display_name: "Set host device mapping"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/hosts/:id/device_mapping/idp"
|
||||
display_name: "Delete host IDP mapping"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/report"
|
||||
display_name: "Get hosts report"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/os_versions"
|
||||
display_name: "List OS versions"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/os_versions/:id"
|
||||
display_name: "Get OS version"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/reports/:report_id"
|
||||
display_name: "Get host report"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/reports"
|
||||
display_name: "List host reports"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/queries"
|
||||
display_name: "List host queries"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/health"
|
||||
display_name: "Get host health"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/labels"
|
||||
display_name: "Add labels to host"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/hosts/:id/labels"
|
||||
display_name: "Remove labels from host"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/software"
|
||||
display_name: "Get host software"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/certificates"
|
||||
display_name: "List host certificates"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/certificates/:template_id/resend"
|
||||
display_name: "Resend host certificate"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/recovery_lock_password"
|
||||
display_name: "Get host recovery lock password"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/summary/mdm"
|
||||
display_name: "Get host MDM summary"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/mdm"
|
||||
display_name: "Get host MDM"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/dep_assignment"
|
||||
display_name: "Get host DEP assignment"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/labels"
|
||||
display_name: "Create label"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/labels/:id"
|
||||
display_name: "Modify label"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/labels/:id"
|
||||
display_name: "Get label"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/labels"
|
||||
display_name: "List labels"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/labels/summary"
|
||||
display_name: "Get labels summary"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/labels/:id/hosts"
|
||||
display_name: "List hosts in label"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/labels/:name"
|
||||
display_name: "Delete label"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/labels/id/:id"
|
||||
display_name: "Delete label by ID"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/spec/labels"
|
||||
display_name: "Apply label specs"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/spec/labels"
|
||||
display_name: "Get label specs"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/spec/labels/:name"
|
||||
display_name: "Get label spec"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/reports/:id/run"
|
||||
display_name: "Run live query"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/reports/run"
|
||||
display_name: "Run live query"
|
||||
deprecated: true
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/reports/run"
|
||||
display_name: "Create distributed query campaign"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/reports/run_by_identifiers"
|
||||
display_name: "Create distributed query campaign by identifier"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/reports/run_by_names"
|
||||
display_name: "Create distributed query campaign by names"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/packs/:id/scheduled"
|
||||
display_name: "Get scheduled queries in pack"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/packs/schedule"
|
||||
display_name: "Schedule query"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/schedule/:id"
|
||||
display_name: "Get scheduled query"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/packs/schedule/:id"
|
||||
display_name: "Modify scheduled query"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/packs/schedule/:id"
|
||||
display_name: "Delete scheduled query"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/schedule"
|
||||
display_name: "Get global schedule"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/schedule"
|
||||
display_name: "Add query to global schedule"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/schedule/:id"
|
||||
display_name: "Modify global scheduled query"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/schedule/:id"
|
||||
display_name: "Delete global scheduled query"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/global/schedule"
|
||||
display_name: "Get global schedule"
|
||||
deprecated: true
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/global/schedule"
|
||||
display_name: "Add query to global schedule"
|
||||
deprecated: true
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/global/schedule/:id"
|
||||
display_name: "Modify global scheduled query"
|
||||
deprecated: true
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/global/schedule/:id"
|
||||
display_name: "Delete global scheduled query"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/schedule"
|
||||
display_name: "Get fleet schedule"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/schedule"
|
||||
display_name: "Add query to fleet schedule"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/schedule/:report_id"
|
||||
display_name: "Modify fleet scheduled query"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/fleets/:fleet_id/schedule/:report_id"
|
||||
display_name: "Delete fleet scheduled query"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/carves"
|
||||
display_name: "List carves"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/carves/:id"
|
||||
display_name: "Get carve"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/carves/:id/block/:block_id"
|
||||
display_name: "Get carve block"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/macadmins"
|
||||
display_name: "Get host macadmins data"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/macadmins"
|
||||
display_name: "Get aggregated macadmins data"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/status/result_store"
|
||||
display_name: "Get result store status"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/status/live_query"
|
||||
display_name: "Get live query status"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/scripts/run"
|
||||
display_name: "Run script"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/scripts/run/sync"
|
||||
display_name: "Run script sync"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/scripts/run/batch"
|
||||
display_name: "Batch run scripts"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scripts/results/:execution_id"
|
||||
display_name: "Get script results"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/scripts"
|
||||
display_name: "Create script"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scripts"
|
||||
display_name: "List scripts"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scripts/:script_id"
|
||||
display_name: "Get script"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/scripts/:script_id"
|
||||
display_name: "Update script"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/scripts/:script_id"
|
||||
display_name: "Delete script"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/scripts/batch"
|
||||
display_name: "Batch set scripts"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/scripts/batch/:batch_execution_id/cancel"
|
||||
display_name: "Cancel batch script execution"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scripts/batch/summary/:batch_execution_id"
|
||||
display_name: "Get batch script execution summary"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scripts/batch/:batch_execution_id/host_results"
|
||||
display_name: "Get batch script host results"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scripts/batch/:batch_execution_id/host-results"
|
||||
display_name: "Get batch script host results"
|
||||
deprecated: true
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scripts/batch/:batch_execution_id"
|
||||
display_name: "Get batch script execution status"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scripts/batch"
|
||||
display_name: "List batch script executions"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/scripts"
|
||||
display_name: "Get host script details"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts/:id/activities/upcoming"
|
||||
display_name: "List host upcoming activities"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/hosts/:id/activities/upcoming/:activity_id"
|
||||
display_name: "Cancel host upcoming activity"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/lock"
|
||||
display_name: "Lock host"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/unlock"
|
||||
display_name: "Unlock host"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/wipe"
|
||||
display_name: "Wipe host"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/clear_passcode"
|
||||
display_name: "Clear host passcode"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/recovery_lock_password/rotate"
|
||||
display_name: "Rotate host recovery lock password"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/autofill/policy"
|
||||
display_name: "Autofill policy"
|
||||
- method: "PUT"
|
||||
path: "/api/v1/fleet/spec/secret_variables"
|
||||
display_name: "Create secret variables"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/custom_variables"
|
||||
display_name: "Create custom variable"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/custom_variables"
|
||||
display_name: "List custom variables"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/custom_variables/:id"
|
||||
display_name: "Delete custom variable"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/scim/details"
|
||||
display_name: "Get SCIM details"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/conditional-access/microsoft"
|
||||
display_name: "Create Microsoft conditional access"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/conditional-access/microsoft/confirm"
|
||||
display_name: "Confirm Microsoft conditional access"
|
||||
- method: "DELETE"
|
||||
path: "/api/v1/fleet/conditional-access/microsoft"
|
||||
display_name: "Delete Microsoft conditional access"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/conditional_access/idp/signing_cert"
|
||||
display_name: "Get IdP signing certificate"
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/conditional_access/idp/apple/profile"
|
||||
display_name: "Get IdP Apple profile"
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/mdm/apple/setup"
|
||||
display_name: "Update MDM Apple setup"
|
||||
deprecated: true
|
||||
- method: "PATCH"
|
||||
path: "/api/v1/fleet/setup_experience"
|
||||
display_name: "Update setup experience"
|
||||
65
server/api_endpoints/api_endpoints_test.go
Normal file
65
server/api_endpoints/api_endpoints_test.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package apiendpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateAPIEndpoints(t *testing.T) {
|
||||
originalYAML := apiEndpointsYAML
|
||||
t.Cleanup(func() {
|
||||
apiEndpointsYAML = originalYAML
|
||||
})
|
||||
|
||||
endpoints := []fleet.APIEndpoint{
|
||||
fleet.NewAPIEndpointFromTpl("GET", "/api/v1/fleet/hosts"),
|
||||
fleet.NewAPIEndpointFromTpl("POST", "/api/v1/fleet/hosts/:id/refetch"),
|
||||
}
|
||||
apiEndpointsYAML = []byte(`
|
||||
- method: "GET"
|
||||
path: "/api/v1/fleet/hosts"
|
||||
display_name: "Route 1"
|
||||
- method: "POST"
|
||||
path: "/api/v1/fleet/hosts/:id/refetch"
|
||||
display_name: "Route 2"`)
|
||||
|
||||
routerWithEndpoints := func(endpoints []fleet.APIEndpoint) *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
for _, e := range endpoints {
|
||||
path := strings.Replace(e.Path, "/_version_/", "/{fleetversion:(?:v1|latest)}/", 1)
|
||||
r.Handle(path, http.NotFoundHandler()).Methods(e.Method)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
t.Run("all routes present", func(t *testing.T) {
|
||||
err := Init(routerWithEndpoints(endpoints))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("missing route returns error", func(t *testing.T) {
|
||||
err := Init(routerWithEndpoints(endpoints[:1]))
|
||||
require.ErrorContains(t, err, endpoints[1].Method+" "+endpoints[1].Path)
|
||||
})
|
||||
|
||||
t.Run("no routes registered returns error listing all missing", func(t *testing.T) {
|
||||
err := Init(mux.NewRouter())
|
||||
require.ErrorContains(t, err, "the following API endpoints are unknown")
|
||||
})
|
||||
|
||||
t.Run("non-mux handler returns error", func(t *testing.T) {
|
||||
err := Init(http.NewServeMux())
|
||||
require.ErrorContains(t, err, "expected *mux.Router")
|
||||
})
|
||||
|
||||
t.Run("empty endpoint list always passes", func(t *testing.T) {
|
||||
apiEndpointsYAML = []byte(``)
|
||||
err := Init(mux.NewRouter())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
@ -1298,3 +1298,21 @@ allow {
|
|||
team_role(subject, object.team_id) == [admin, maintainer, gitops][_]
|
||||
action == [read, write][_]
|
||||
}
|
||||
|
||||
##
|
||||
# API Endpoints
|
||||
##
|
||||
|
||||
# Global admins can read API endpoints.
|
||||
allow {
|
||||
object.type == "api_endpoint"
|
||||
subject.global_role == admin
|
||||
action == read
|
||||
}
|
||||
|
||||
# Any team admin can read API endpoints.
|
||||
allow {
|
||||
object.type == "api_endpoint"
|
||||
team_role(subject, subject.teams[_].id) == admin
|
||||
action == read
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3009,3 +3009,31 @@ func TestAuthorizeSecretVariables(t *testing.T) {
|
|||
{user: test.UserTeamTechnicianTeam1, object: secretVariable, action: write, allow: false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizeAPIEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
endpoint := &fleet.APIEndpoint{}
|
||||
runTestCases(t, []authTestCase{
|
||||
// Unauthenticated always denied.
|
||||
{user: nil, object: endpoint, action: read, allow: false},
|
||||
|
||||
// Global roles: only admin is allowed.
|
||||
{user: test.UserNoRoles, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserObserver, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserObserverPlus, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserMaintainer, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserTechnician, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserGitOps, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserAdmin, object: endpoint, action: read, allow: true},
|
||||
|
||||
// Team roles: only team admins are allowed.
|
||||
{user: test.UserTeamObserverTeam1, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserTeamObserverPlusTeam1, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserTeamMaintainerTeam1, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserTeamTechnicianTeam1, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam1, object: endpoint, action: read, allow: false},
|
||||
{user: test.UserTeamAdminTeam1, object: endpoint, action: read, allow: true},
|
||||
{user: test.UserTeamAdminTeam2, object: endpoint, action: read, allow: true},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,12 @@ func Up_20260409153714(tx *sql.Tx) error {
|
|||
path VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
method VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
|
||||
is_allowed BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
created_by_id INT UNSIGNED,
|
||||
author_id INT UNSIGNED,
|
||||
|
||||
PRIMARY KEY (user_id, path, method),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2999,13 +2999,12 @@ CREATE TABLE `user_api_endpoints` (
|
|||
`user_id` int unsigned NOT NULL,
|
||||
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`is_allowed` tinyint(1) NOT NULL DEFAULT '1',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`created_by_id` int unsigned DEFAULT NULL,
|
||||
`author_id` int unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`user_id`,`path`,`method`),
|
||||
KEY `created_by_id` (`created_by_id`),
|
||||
KEY `author_id` (`author_id`),
|
||||
CONSTRAINT `user_api_endpoints_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `user_api_endpoints_ibfk_2` FOREIGN KEY (`created_by_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
CONSTRAINT `user_api_endpoints_ibfk_2` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
|
|
|
|||
99
server/fleet/api_endpoints.go
Normal file
99
server/fleet/api_endpoints.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
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 fmt.Sprintf("|%s|%s|", 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()
|
||||
}
|
||||
114
server/fleet/api_endpoints_test.go
Normal file
114
server/fleet/api_endpoints_test.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package fleet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIdIsConstructedCorrectly(t *testing.T) {
|
||||
tests := []struct {
|
||||
method string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
method: "GET",
|
||||
path: "/api/v1/fleet/trigger",
|
||||
want: "|GET|/api/v1/fleet/trigger|",
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
path: "/software/titles/:title_id/icon/:id",
|
||||
want: "|GET|/software/titles/:placeholder_1/icon/:placeholder_2|",
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
path: "/api/v1/fleet/hosts/:id",
|
||||
want: "|GET|/api/v1/fleet/hosts/:placeholder_1|",
|
||||
},
|
||||
{
|
||||
method: "post",
|
||||
path: "/a/:b/:c/:d",
|
||||
want: "|POST|/a/:placeholder_1/:placeholder_2/:placeholder_3|",
|
||||
},
|
||||
{
|
||||
method: "pAtCh",
|
||||
path: "/no/placeholders/here",
|
||||
want: "|PATCH|/no/placeholders/here|",
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
path: "/:single",
|
||||
want: "|GET|/:placeholder_1|",
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
path: "/UPPER/CASE",
|
||||
want: "|GET|/upper/case|",
|
||||
},
|
||||
// gorilla/mux brace-style placeholders
|
||||
{
|
||||
method: "post",
|
||||
path: "/api/v1/fleet/hosts/{id:[0-9]+}",
|
||||
want: "|POST|/api/v1/fleet/hosts/:placeholder_1|",
|
||||
},
|
||||
{
|
||||
method: "get",
|
||||
path: "/api/v1/fleet/hosts/{id:[0-9]+}/reports/{report_id:[0-9]+}",
|
||||
want: "|GET|/api/v1/fleet/hosts/:placeholder_1/reports/:placeholder_2|",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
sut := NewAPIEndpointFromTpl(tt.method, tt.path)
|
||||
require.Equal(t, tt.want, sut.Fingerprint())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIEndpointValidate(t *testing.T) {
|
||||
base := APIEndpoint{Method: "GET", Path: "/api/v1/fleet/foo", DisplayName: "foo"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
modify func(APIEndpoint) APIEndpoint
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid endpoint",
|
||||
modify: func(e APIEndpoint) APIEndpoint { return e },
|
||||
},
|
||||
{
|
||||
name: "missing display_name",
|
||||
modify: func(e APIEndpoint) APIEndpoint { e.DisplayName = ""; return e },
|
||||
wantErr: "display_name is required",
|
||||
},
|
||||
{
|
||||
name: "whitespace display_name",
|
||||
modify: func(e APIEndpoint) APIEndpoint { e.DisplayName = " "; return e },
|
||||
wantErr: "display_name is required",
|
||||
},
|
||||
{
|
||||
name: "invalid method",
|
||||
modify: func(e APIEndpoint) APIEndpoint { e.Method = "GTE"; return e },
|
||||
wantErr: "invalid HTTP method",
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
modify: func(e APIEndpoint) APIEndpoint { e.Path = " "; return e },
|
||||
wantErr: "path is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.modify(base).validate()
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1426,6 +1426,9 @@ type Service interface {
|
|||
// Returns a NotFoundError error if there's no secret variable with such ID.
|
||||
DeleteSecretVariable(ctx context.Context, id uint) error
|
||||
|
||||
// ListAPIEndpoints returns all API endpoints
|
||||
ListAPIEndpoints(ctx context.Context) (endpoints []APIEndpoint, err error)
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// SCIM
|
||||
|
||||
|
|
|
|||
|
|
@ -876,6 +876,8 @@ type ListSecretVariablesFunc func(ctx context.Context, opts fleet.ListOptions) (
|
|||
|
||||
type DeleteSecretVariableFunc func(ctx context.Context, id uint) error
|
||||
|
||||
type ListAPIEndpointsFunc func(ctx context.Context) (endpoints []fleet.APIEndpoint, err error)
|
||||
|
||||
type ScimDetailsFunc func(ctx context.Context) (fleet.ScimDetails, error)
|
||||
|
||||
type ConditionalAccessMicrosoftCreateIntegrationFunc func(ctx context.Context, tenantID string) (adminConsentURL string, err error)
|
||||
|
|
@ -2193,6 +2195,9 @@ type Service struct {
|
|||
DeleteSecretVariableFunc DeleteSecretVariableFunc
|
||||
DeleteSecretVariableFuncInvoked bool
|
||||
|
||||
ListAPIEndpointsFunc ListAPIEndpointsFunc
|
||||
ListAPIEndpointsFuncInvoked bool
|
||||
|
||||
ScimDetailsFunc ScimDetailsFunc
|
||||
ScimDetailsFuncInvoked bool
|
||||
|
||||
|
|
@ -5240,6 +5245,13 @@ func (s *Service) DeleteSecretVariable(ctx context.Context, id uint) error {
|
|||
return s.DeleteSecretVariableFunc(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) ListAPIEndpoints(ctx context.Context) (endpoints []fleet.APIEndpoint, err error) {
|
||||
s.mu.Lock()
|
||||
s.ListAPIEndpointsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ListAPIEndpointsFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) ScimDetails(ctx context.Context) (fleet.ScimDetails, error) {
|
||||
s.mu.Lock()
|
||||
s.ScimDetailsFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -1,111 +1,34 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"context"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
//go:embed api_endpoints.yml
|
||||
var apiEndpointsYAML []byte
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// List API endpoints
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var apiEndpoints = mustParseAPIEndpoints()
|
||||
type listAPIEndpointsRequest struct{}
|
||||
|
||||
// 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"`
|
||||
type listAPIEndpointsResponse struct {
|
||||
APIEndpoints []fleet.APIEndpoint `json:"api_endpoints"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var validHTTPMethods = map[string]struct{}{
|
||||
http.MethodGet: {},
|
||||
http.MethodPost: {},
|
||||
http.MethodPut: {},
|
||||
http.MethodPatch: {},
|
||||
http.MethodDelete: {},
|
||||
func (r listAPIEndpointsResponse) Error() error { return r.Err }
|
||||
|
||||
func listAPIEndpointsEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
endpoints, err := svc.ListAPIEndpoints(ctx)
|
||||
return listAPIEndpointsResponse{
|
||||
APIEndpoints: endpoints,
|
||||
Err: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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
|
||||
func (svc *Service) ListAPIEndpoints(ctx context.Context) ([]fleet.APIEndpoint, error) {
|
||||
// skipauth: No authorization check, this is a premium feature only
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
- method: POST
|
||||
path: "/api/_version_/fleet/trigger"
|
||||
name: "Some wild description goes here"
|
||||
deprecated: false
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetAPIEndpoints(t *testing.T) {
|
||||
routes := GetAPIEndpoints()
|
||||
require.NotEmpty(t, routes)
|
||||
for _, r := range routes {
|
||||
require.NotEmpty(t, r.Method, "route method should not be empty")
|
||||
require.NotEmpty(t, r.Path, "route path should not be empty")
|
||||
require.NotEmpty(t, r.Name, "route name should not be empty")
|
||||
require.True(t, strings.HasPrefix(r.Path, "/"), "route path should start with /")
|
||||
_, validMethod := validHTTPMethods[r.Method]
|
||||
require.True(t, validMethod, "route method %q should be a valid HTTP method", r.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIEndpointValidate(t *testing.T) {
|
||||
base := APIEndpoint{Method: "GET", Path: "/api/_version_/fleet/foo", Name: "foo"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
modify func(APIEndpoint) APIEndpoint
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid endpoint",
|
||||
modify: func(e APIEndpoint) APIEndpoint { return e },
|
||||
},
|
||||
{
|
||||
name: "missing name",
|
||||
modify: func(e APIEndpoint) APIEndpoint { e.Name = ""; return e },
|
||||
wantErr: "name is required",
|
||||
},
|
||||
{
|
||||
name: "whitespace name",
|
||||
modify: func(e APIEndpoint) APIEndpoint { e.Name = " "; return e },
|
||||
wantErr: "name is required",
|
||||
},
|
||||
{
|
||||
name: "invalid method",
|
||||
modify: func(e APIEndpoint) APIEndpoint { e.Method = "GTE"; return e },
|
||||
wantErr: "invalid HTTP method",
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
modify: func(e APIEndpoint) APIEndpoint { e.Path = " "; return e },
|
||||
wantErr: "path is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.modify(base).validate()
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAPIEndpoints(t *testing.T) {
|
||||
allRoutes := GetAPIEndpoints()
|
||||
|
||||
routerWithRoutes := func(routes []APIEndpoint) *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
for _, route := range routes {
|
||||
path := strings.Replace(route.Path, "/_version_/", "/{fleetversion:(?:v1|latest)}/", 1)
|
||||
r.Handle(path, http.NotFoundHandler()).Methods(route.Method)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
handler http.Handler
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "all routes present",
|
||||
handler: routerWithRoutes(allRoutes),
|
||||
},
|
||||
{
|
||||
name: "no routes registered",
|
||||
handler: mux.NewRouter(),
|
||||
wantErr: "the following API endpoints are missing",
|
||||
},
|
||||
{
|
||||
name: "non-mux handler returns error",
|
||||
handler: http.NewServeMux(),
|
||||
wantErr: "expected *mux.Router, got *http.ServeMux",
|
||||
},
|
||||
}
|
||||
|
||||
if len(allRoutes) >= 2 {
|
||||
last := allRoutes[len(allRoutes)-1]
|
||||
tests = append(tests, struct {
|
||||
name string
|
||||
handler http.Handler
|
||||
wantErr string
|
||||
}{
|
||||
name: "last route missing",
|
||||
handler: routerWithRoutes(allRoutes[:len(allRoutes)-1]),
|
||||
wantErr: last.Method + " " + last.Path,
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateAPIEndpoints(tt.handler)
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -579,6 +579,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ue.GET("/api/_version_/fleet/custom_variables", listSecretVariablesEndpoint, listSecretVariablesRequest{})
|
||||
ue.DELETE("/api/_version_/fleet/custom_variables/{id:[0-9]+}", deleteSecretVariableEndpoint, deleteSecretVariableRequest{})
|
||||
|
||||
// API end-points
|
||||
ue.GET("/api/_version_/fleet/rest_api", listAPIEndpointsEndpoint, listAPIEndpointsRequest{})
|
||||
|
||||
// Scim details
|
||||
ue.GET("/api/_version_/fleet/scim/details", getScimDetailsEndpoint, nil)
|
||||
|
||||
|
|
|
|||
|
|
@ -7587,6 +7587,10 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() {
|
|||
}`), http.StatusUnprocessableEntity)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "missing or invalid license")
|
||||
|
||||
// list API endpoints requires premium license
|
||||
var listAPIEndpointsResp listAPIEndpointsResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/rest_api", nil, http.StatusPaymentRequired, &listAPIEndpointsResp)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestScriptsEndpointsWithoutLicense() {
|
||||
|
|
|
|||
|
|
@ -28256,3 +28256,65 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersDeletesOb
|
|||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), fleet.ListTeamPoliciesRequest{}, http.StatusOK, &listPolResp, "page", "0")
|
||||
require.Empty(t, listPolResp.Policies)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestListAPIEndpoints() {
|
||||
t := s.T()
|
||||
defer func() { s.token = s.getTestAdminToken() }()
|
||||
|
||||
// unauthenticated request is rejected
|
||||
s.DoRawNoAuth("GET", "/api/latest/fleet/rest_api", nil, http.StatusUnauthorized)
|
||||
|
||||
// non-admin global roles are forbidden
|
||||
for _, role := range []string{fleet.RoleObserver, fleet.RoleObserverPlus, fleet.RoleMaintainer, fleet.RoleGitOps, fleet.RoleTechnician} {
|
||||
u := &fleet.User{
|
||||
Name: "test " + role,
|
||||
Email: role + "-api-endpoints@example.com",
|
||||
GlobalRole: ptr.String(role),
|
||||
}
|
||||
require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
|
||||
_, err := s.ds.NewUser(context.Background(), u)
|
||||
require.NoError(t, err)
|
||||
s.token = s.getTestToken(u.Email, test.GoodPassword)
|
||||
s.Do("GET", "/api/latest/fleet/rest_api", nil, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// non-admin team roles are forbidden
|
||||
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "api-endpoints-test-team"})
|
||||
require.NoError(t, err)
|
||||
for _, role := range []string{fleet.RoleObserver, fleet.RoleObserverPlus, fleet.RoleMaintainer, fleet.RoleGitOps, fleet.RoleTechnician} {
|
||||
u := &fleet.User{
|
||||
Name: "team " + role,
|
||||
Email: "team-" + role + "-api-endpoints@example.com",
|
||||
Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team.ID}, Role: role}},
|
||||
}
|
||||
require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
|
||||
_, err := s.ds.NewUser(context.Background(), u)
|
||||
require.NoError(t, err)
|
||||
s.token = s.getTestToken(u.Email, test.GoodPassword)
|
||||
s.Do("GET", "/api/latest/fleet/rest_api", nil, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// team admin is allowed
|
||||
teamAdmin := &fleet.User{
|
||||
Name: "team admin",
|
||||
Email: "team-admin-api-endpoints@example.com",
|
||||
Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team.ID}, Role: fleet.RoleAdmin}},
|
||||
}
|
||||
require.NoError(t, teamAdmin.SetPassword(test.GoodPassword, 10, 10))
|
||||
_, err = s.ds.NewUser(context.Background(), teamAdmin)
|
||||
require.NoError(t, err)
|
||||
s.token = s.getTestToken(teamAdmin.Email, test.GoodPassword)
|
||||
s.Do("GET", "/api/latest/fleet/rest_api", nil, http.StatusOK)
|
||||
|
||||
// restore admin token
|
||||
s.token = s.getTestAdminToken()
|
||||
|
||||
// global admin can list API endpoints
|
||||
var resp listAPIEndpointsResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/rest_api", nil, http.StatusOK, &resp)
|
||||
require.NotEmpty(t, resp.APIEndpoints)
|
||||
require.NoError(t, resp.Err)
|
||||
require.NotEmpty(t, resp.APIEndpoints[0].Method)
|
||||
require.NotEmpty(t, resp.APIEndpoints[0].Path)
|
||||
require.NotEmpty(t, resp.APIEndpoints[0].DisplayName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/acl/activityacl"
|
||||
activity_api "github.com/fleetdm/fleet/v4/server/activity/api"
|
||||
activity_bootstrap "github.com/fleetdm/fleet/v4/server/activity/bootstrap"
|
||||
apiendpoints "github.com/fleetdm/fleet/v4/server/api_endpoints"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
|
|
@ -628,6 +629,9 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl
|
|||
}
|
||||
var carveStore fleet.CarveStore = ds // In tests, we use MySQL as storage for carves.
|
||||
apiHandler := MakeHandler(svc, cfg, logger, limitStore, redisPool, carveStore, featureRoutes, extra...)
|
||||
if err := apiendpoints.Init(apiHandler); err != nil {
|
||||
t.Fatalf("error initializing API endpoints: %v", err)
|
||||
}
|
||||
rootMux.Handle("/api/", apiHandler)
|
||||
var errHandler *errorstore.Handler
|
||||
ctxErrHandler := ctxerr.FromContext(ctx)
|
||||
|
|
|
|||
Loading…
Reference in a new issue