Skip to content

Commit 7aea2e3

Browse files
Add gitops endpoints to api_endpoints catalog (#44291)
Resolves #44279 Add gitops endpoints to api_endpoints catalog
1 parent 9f2edbd commit 7aea2e3

2 files changed

Lines changed: 201 additions & 1 deletion

File tree

server/api_endpoints/api_endpoints.yml

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
- method: "GET"
4141
path: "/api/v1/fleet/config/certificate"
4242
display_name: "Get Fleet certificate"
43+
- method: "PATCH"
44+
path: "/api/v1/fleet/config"
45+
display_name: "Updates Fleet configuration"
4346
- method: "GET"
4447
path: "/api/v1/fleet/config"
4548
display_name: "Get Fleet configuration"
@@ -406,6 +409,9 @@
406409
- method: "GET"
407410
path: "/api/v1/fleet/scripts/results/:execution_id"
408411
display_name: "Get script result"
412+
- method: "POST"
413+
path: "/api/v1/fleet/scripts/batch"
414+
display_name: "Batch-run script"
409415
- method: "POST"
410416
path: "/api/v1/fleet/scripts/run/batch"
411417
display_name: "Batch-run script"
@@ -510,4 +516,83 @@
510516
display_name: "Delete custom variable"
511517
- method: "GET"
512518
path: "/api/v1/fleet/configuration_profiles/:profile_uuid/status"
513-
display_name: "Get OS setting (configuration profile) status"
519+
display_name: "Get OS setting (configuration profile) status"
520+
- method: "GET"
521+
path: "/api/v1/fleet/me"
522+
display_name: "Get current user"
523+
- method: "GET"
524+
path: "/api/v1/fleet/spec/labels"
525+
display_name: "Get labels spec"
526+
- method: "POST"
527+
path: "/api/v1/fleet/spec/labels"
528+
display_name: "Apply labels spec"
529+
- method: "GET"
530+
path: "/api/v1/fleet/abm_tokens/count"
531+
display_name: "Count Apple Business Manager (ABM) tokens"
532+
- method: "GET"
533+
path: "/api/v1/fleet/spec/certificate_authorities"
534+
display_name: "Get certificate authorities spec"
535+
- method: "POST"
536+
path: "/api/v1/fleet/spec/certificate_authorities"
537+
display_name: "Batch-apply certificate authorities"
538+
- method: "POST"
539+
path: "/api/v1/fleet/spec/certificates"
540+
display_name: "Apply certificate templates spec"
541+
- method: "DELETE"
542+
path: "/api/v1/fleet/spec/certificates"
543+
display_name: "Delete certificate templates spec"
544+
- method: "PUT"
545+
path: "/api/v1/fleet/spec/secret_variables"
546+
display_name: "Save secret variables"
547+
- method: "POST"
548+
path: "/api/v1/fleet/spec/reports"
549+
display_name: "Apply reports spec"
550+
- method: "POST"
551+
path: "/api/v1/fleet/spec/policies"
552+
display_name: "Apply policies spec"
553+
- method: "POST"
554+
path: "/api/v1/fleet/spec/fleets"
555+
display_name: "Batch-apply fleets spec"
556+
- method: "GET"
557+
path: "/api/v1/fleet/policies"
558+
display_name: "List policies"
559+
- method: "POST"
560+
path: "/api/v1/fleet/policies/delete"
561+
display_name: "Delete policies"
562+
- method: "POST"
563+
path: "/api/v1/fleet/mdm/profiles/batch"
564+
display_name: "Batch-apply MDM configuration profiles"
565+
- method: "GET"
566+
path: "/api/v1/fleet/mdm/profiles/:profile_uuid"
567+
display_name: "Download MDM configuration profile"
568+
deprecated: true
569+
- method: "GET"
570+
path: "/api/v1/fleet/mdm/bootstrap/:fleet_id/metadata"
571+
display_name: "Get MDM bootstrap package metadata"
572+
- method: "DELETE"
573+
path: "/api/v1/fleet/mdm/bootstrap/:fleet_id"
574+
display_name: "Delete MDM bootstrap package"
575+
- method: "POST"
576+
path: "/api/v1/fleet/software/batch"
577+
display_name: "Batch-apply software installers"
578+
- method: "GET"
579+
path: "/api/v1/fleet/software/batch/:request_uuid"
580+
display_name: "Get batch software installers result"
581+
- method: "POST"
582+
path: "/api/v1/fleet/software/app_store_apps/batch"
583+
display_name: "Batch-associate App Store apps"
584+
- method: "GET"
585+
path: "/api/v1/fleet/software/fleet_maintained_apps"
586+
display_name: "List Fleet-maintained apps"
587+
- method: "GET"
588+
path: "/api/v1/fleet/software/fleet_maintained_apps/:app_id"
589+
display_name: "Get Fleet-maintained app"
590+
- method: "GET"
591+
path: "/api/v1/fleet/software/titles/:title_id/icon"
592+
display_name: "Get software title icon"
593+
- method: "PUT"
594+
path: "/api/v1/fleet/software/titles/:title_id/icon"
595+
display_name: "Upload or update software title icon"
596+
- method: "DELETE"
597+
path: "/api/v1/fleet/software/titles/:title_id/icon"
598+
display_name: "Delete software title icon"

server/service/integration_enterprise_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29188,3 +29188,118 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetInstallersScriptByHash() {
2918829188
})
2918929189
require.Equal(t, string(scriptBytes), installScript)
2919029190
}
29191+
29192+
// TestAPIOnlyUserCanReachGitOpsEndpoints verifies that the API endpoint
29193+
// catalog includes every route invoked by fleetctl gitops and
29194+
// fleetctl generate-gitops. The test runs as an api-only admin user so any
29195+
// 403 observed here comes from the api_only middleware (catalog miss), never
29196+
// from service-level authz. Regression test for #44279.
29197+
func (s *integrationEnterpriseTestSuite) TestAPIOnlyUserCanReachGitOpsEndpoints() {
29198+
t := s.T()
29199+
defer func() { s.token = s.getTestAdminToken() }()
29200+
29201+
assertNot403 := func(verb, path string, body any) {
29202+
t.Helper()
29203+
var raw []byte
29204+
if body != nil {
29205+
j, err := json.Marshal(body)
29206+
require.NoError(t, err)
29207+
raw = j
29208+
}
29209+
req, err := http.NewRequest(verb, s.server.URL+path, bytes.NewReader(raw))
29210+
require.NoError(t, err)
29211+
req.Header.Set("Authorization", "Bearer "+s.token)
29212+
resp, err := http.DefaultClient.Do(req)
29213+
require.NoError(t, err)
29214+
defer resp.Body.Close()
29215+
require.NotEqualf(t, http.StatusForbidden, resp.StatusCode,
29216+
"%s %s returned 403 for an api-only admin user; the route is likely missing from server/api_endpoints/api_endpoints.yml",
29217+
verb, path)
29218+
}
29219+
29220+
// Create an api-only admin (no endpoint restrictions). Admins pass every
29221+
// service-level authz check, so the only remaining source of 403 is the
29222+
// api_only middleware's catalog check — exactly what we want to verify.
29223+
var createResp struct {
29224+
Token string `json:"token"`
29225+
}
29226+
s.DoJSON("POST", "/api/latest/fleet/users/api_only", map[string]any{
29227+
"name": "api-only-admin-gitops-catalog",
29228+
"global_role": fleet.RoleAdmin,
29229+
}, http.StatusOK, &createResp)
29230+
require.NotEmpty(t, createResp.Token)
29231+
s.token = createResp.Token
29232+
29233+
// Endpoints invoked by fleetctl gitops / generate-gitops. Status codes vary
29234+
// per endpoint based on payload validity; we only care that 403 never comes
29235+
// from the catalog check.
29236+
emptySpecs := map[string]any{"specs": []any{}}
29237+
assertNot403("GET", "/api/latest/fleet/me", nil)
29238+
assertNot403("GET", "/api/latest/fleet/config", nil)
29239+
assertNot403("GET", "/api/latest/fleet/version", nil)
29240+
assertNot403("GET", "/api/latest/fleet/fleets", nil)
29241+
assertNot403("GET", "/api/latest/fleet/spec/labels", nil)
29242+
assertNot403("POST", "/api/latest/fleet/spec/labels", emptySpecs)
29243+
assertNot403("GET", "/api/latest/fleet/spec/enroll_secret", nil)
29244+
assertNot403("GET", "/api/latest/fleet/spec/certificate_authorities", nil)
29245+
assertNot403("POST", "/api/latest/fleet/spec/certificate_authorities", emptySpecs)
29246+
assertNot403("GET", "/api/latest/fleet/abm_tokens/count", nil)
29247+
assertNot403("POST", "/api/latest/fleet/spec/policies", emptySpecs)
29248+
assertNot403("POST", "/api/latest/fleet/spec/reports", emptySpecs)
29249+
assertNot403("POST", "/api/latest/fleet/spec/fleets", emptySpecs)
29250+
assertNot403("PUT", "/api/latest/fleet/spec/secret_variables", map[string]any{"secret_variables": []any{}})
29251+
assertNot403("GET", "/api/latest/fleet/policies", nil)
29252+
assertNot403("GET", "/api/latest/fleet/configuration_profiles", nil)
29253+
assertNot403("GET", "/api/latest/fleet/scripts", nil)
29254+
assertNot403("GET", "/api/latest/fleet/software/titles", nil)
29255+
assertNot403("GET", "/api/latest/fleet/software/fleet_maintained_apps", nil)
29256+
assertNot403("GET", "/api/latest/fleet/setup_experience/script", nil)
29257+
assertNot403("GET", "/api/latest/fleet/vpp_tokens", nil)
29258+
assertNot403("GET", "/api/latest/fleet/certificates", nil)
29259+
}
29260+
29261+
func (s *integrationEnterpriseTestSuite) TestAPIOnlyGitOpsUserWithEndpointRestrictions() {
29262+
t := s.T()
29263+
defer func() { s.token = s.getTestAdminToken() }()
29264+
29265+
allowedEndpoints := []map[string]any{
29266+
{"method": "GET", "path": "/api/v1/fleet/me"},
29267+
{"method": "GET", "path": "/api/v1/fleet/config"},
29268+
{"method": "GET", "path": "/api/v1/fleet/spec/labels"},
29269+
{"method": "GET", "path": "/api/v1/fleet/abm_tokens/count"},
29270+
{"method": "POST", "path": "/api/v1/fleet/spec/policies"},
29271+
}
29272+
29273+
var createResp struct {
29274+
Token string `json:"token"`
29275+
}
29276+
s.DoJSON("POST", "/api/latest/fleet/users/api_only", map[string]any{
29277+
"name": "api-only-gitops-restricted",
29278+
"global_role": fleet.RoleGitOps,
29279+
"api_endpoints": allowedEndpoints,
29280+
}, http.StatusOK, &createResp)
29281+
require.NotEmpty(t, createResp.Token)
29282+
s.token = createResp.Token
29283+
29284+
// Allowed endpoints reach the handler.
29285+
s.Do("GET", "/api/latest/fleet/me", nil, http.StatusOK)
29286+
s.Do("GET", "/api/latest/fleet/config", nil, http.StatusOK)
29287+
s.Do("GET", "/api/latest/fleet/spec/labels", nil, http.StatusOK)
29288+
s.Do("GET", "/api/latest/fleet/abm_tokens/count", nil, http.StatusOK)
29289+
// Apply a real policy spec; an empty specs list short-circuits before authz
29290+
// in checkPolicySpecAuthorization and surfaces as 500, which would mask the
29291+
// middleware behavior we want to verify.
29292+
s.Do("POST", "/api/latest/fleet/spec/policies", map[string]any{
29293+
"specs": []map[string]any{
29294+
{"name": t.Name() + "-policy", "query": "SELECT 1;", "platform": "darwin"},
29295+
},
29296+
}, http.StatusOK)
29297+
29298+
// Other gitops endpoints are in the catalog but not in the allow list, so
29299+
// the middleware rejects them with 403.
29300+
s.Do("GET", "/api/latest/fleet/version", nil, http.StatusForbidden)
29301+
s.Do("GET", "/api/latest/fleet/fleets", nil, http.StatusForbidden)
29302+
s.Do("POST", "/api/latest/fleet/spec/labels", map[string]any{"specs": []any{}}, http.StatusForbidden)
29303+
s.Do("POST", "/api/latest/fleet/spec/reports", map[string]any{"specs": []any{}}, http.StatusForbidden)
29304+
s.Do("POST", "/api/latest/fleet/spec/fleets", map[string]any{"specs": []any{}}, http.StatusForbidden)
29305+
}

0 commit comments

Comments
 (0)