Skip to content

Added middleware for api-only users auth#43772

Merged
juan-fdz-hawa merged 10 commits intomainfrom
42885-api-only-perm-middleware
Apr 21, 2026
Merged

Added middleware for api-only users auth#43772
juan-fdz-hawa merged 10 commits intomainfrom
42885-api-only-perm-middleware

Conversation

@juan-fdz-hawa
Copy link
Copy Markdown
Contributor

@juan-fdz-hawa juan-fdz-hawa commented Apr 20, 2026

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.

Related issue: Resolves #

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

Testing

Summary by CodeRabbit

  • New Features

    • Enforced access control for API-only users with configurable endpoint-level restrictions.
    • API-only users requesting unauthorized endpoints now receive 403 Forbidden responses.
  • Bug Fixes

    • Resolved privilege escalation vulnerability affecting API-only user access.

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.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

❌ Patch coverage is 93.87755% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.92%. Comparing base (101858e) to head (fcf2282).
⚠️ Report is 28 commits behind head on main.

Files with missing lines Patch % Lines
server/platform/endpointer/route_template.go 84.61% 1 Missing and 1 partial ⚠️
cmd/fleet/serve.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #43772    +/-   ##
========================================
  Coverage   66.91%   66.92%            
========================================
  Files        2600     2602     +2     
  Lines      208643   209035   +392     
  Branches     9342     9342            
========================================
+ Hits       139608   139890   +282     
- Misses      56326    56399    +73     
- Partials    12709    12746    +37     
Flag Coverage Δ
backend 68.70% <93.87%> (+<0.01%) ⬆️
backend-activity 86.37% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@juan-fdz-hawa
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

Walkthrough

This PR implements HTTP 403 denial enforcement for API-only users when their requests do not match configured endpoint restrictions. A new middleware component checks whether a request exists in the API endpoint catalog and validates against the user's allowed endpoint list if configured. The implementation includes utilities to capture Gorilla/mux route templates in request context, catalog membership checking via endpoint fingerprint, and updates to endpoint wiring to integrate the new middleware before authentication. Tests validate middleware behavior, pass-through logic for non-API-only users, and enforcement of endpoint restrictions.

Possibly related PRs

  • PR #43077: Implements the API-endpoints catalog feature (catalog parsing, validation, and membership checks) that this PR's middleware depends on for authorization enforcement.
  • PR #36484: Introduces endpointer utilities and route-template handling that this PR extends to capture matched mux routes in request context.
  • PR #43440: Adds API-only user endpoints and the User.APIEndpoints fields that this PR's middleware uses to enforce per-user endpoint restrictions.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Added middleware for api-only users auth' clearly summarizes the main change—introducing the APIOnlyEndpointCheck middleware for API-only user authentication.
Description check ✅ Passed The PR description provides a concise summary of the changes and includes checkmarks for key requirements: changes file added, automated tests added/updated, and manual QA performed.
Linked Issues check ✅ Passed The PR successfully implements the primary objective from issue #42885: enforcing HTTP 403 via middleware when API-only users with configured endpoints attempt unauthorized access.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the APIOnlyEndpointCheck middleware and associated infrastructure needed for API-only user authorization enforcement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 42885-api-only-perm-middleware

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
server/service/integration_enterprise_test.go (1)

28989-29041: Consider adding coverage for a catalog-miss 403 case.

The current subtests exercise: (a) no restrictions + catalog endpoint = 200, (b) restrictions + allow-listed endpoint = 200 / non-allow-listed catalog endpoint = 403, and (c) non-api-only user unaffected. A useful additional case would be an api-only user (with or without api_endpoints) hitting a path that is NOT in the API endpoint catalog at all, to lock in the "not in catalog → 403" behavior called out in the PR objectives. Without it, a regression that accidentally allows non-catalog paths through for api-only users would not be caught here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/service/integration_enterprise_test.go` around lines 28989 - 29041,
Add a new subtest inside TestAPIOnlyUserEndpointMiddleware to assert that
requests to paths not present in the API endpoint catalog return 403 for
api-only users: use the existing createAPIOnlyUser helper to create a token
(with and/or without api_endpoints) and call s.Do with a clearly non-catalog
path (e.g. "/api/latest/fleet/nonexistent" or "/no/such/endpoint") expecting
http.StatusForbidden; place this alongside the other t.Run cases so the
catalog-miss → 403 behavior is covered by the test suite and references the
TestAPIOnlyUserEndpointMiddleware, createAPIOnlyUser, and s.Do helpers.
server/service/middleware/auth/api_only.go (1)

77-82: Optional: precompute allow-list fingerprints.

fleet.NewAPIEndpointFromTpl runs a regex replace and string split per user endpoint on every request. For users with non-trivial allow-lists this recomputes the same fingerprints on each call. If fleet.User.APIEndpoints is loaded once per request (or cached), consider computing the fingerprint set once upstream (e.g., on user load) and exposing a membership check, then this loop becomes an O(1) map lookup. Non-blocking for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/service/middleware/auth/api_only.go` around lines 77 - 82, The loop
repeatedly calls fleet.NewAPIEndpointFromTpl(...).Fingerprint() for every
request which is expensive; modify the user loading code (where v.User is
constructed) to precompute and store a set/map of endpoint fingerprints (e.g.,
User.APIEndpointFingerprints map[string]struct{}) derived from User.APIEndpoints
using fleet.NewAPIEndpointFromTpl(...).Fingerprint(), and then change this
middleware loop to perform an O(1 membership check against that map (check fp in
v.User.APIEndpointFingerprints) instead of recomputing fingerprints; keep
function names like NewAPIEndpointFromTpl, Fingerprint, and the middleware
variables v.User and fp to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/service/middleware/auth/api_only_test.go`:
- Line 137: The tests pass a value-typed target (&fleet.PermissionError{}) to
require.ErrorAs but PermissionError implements error only on *PermissionError,
which causes errors.As to panic; fix each occurrence (the require.ErrorAs calls
at the locations noted) by using a pointer-to-pointer target (e.g., declare var
perr *fleet.PermissionError and pass &perr) or by passing
(*fleet.PermissionError)(nil) as the target so errors.As can match the
*fleet.PermissionError type; update the require.ErrorAs calls referencing
fleet.PermissionError accordingly.

---

Nitpick comments:
In `@server/service/integration_enterprise_test.go`:
- Around line 28989-29041: Add a new subtest inside
TestAPIOnlyUserEndpointMiddleware to assert that requests to paths not present
in the API endpoint catalog return 403 for api-only users: use the existing
createAPIOnlyUser helper to create a token (with and/or without api_endpoints)
and call s.Do with a clearly non-catalog path (e.g.
"/api/latest/fleet/nonexistent" or "/no/such/endpoint") expecting
http.StatusForbidden; place this alongside the other t.Run cases so the
catalog-miss → 403 behavior is covered by the test suite and references the
TestAPIOnlyUserEndpointMiddleware, createAPIOnlyUser, and s.Do helpers.

In `@server/service/middleware/auth/api_only.go`:
- Around line 77-82: The loop repeatedly calls
fleet.NewAPIEndpointFromTpl(...).Fingerprint() for every request which is
expensive; modify the user loading code (where v.User is constructed) to
precompute and store a set/map of endpoint fingerprints (e.g.,
User.APIEndpointFingerprints map[string]struct{}) derived from User.APIEndpoints
using fleet.NewAPIEndpointFromTpl(...).Fingerprint(), and then change this
middleware loop to perform an O(1 membership check against that map (check fp in
v.User.APIEndpointFingerprints) instead of recomputing fingerprints; keep
function names like NewAPIEndpointFromTpl, Fingerprint, and the middleware
variables v.User and fp to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64710039-c1cd-464e-b203-02ca86e0ff30

📥 Commits

Reviewing files that changed from the base of the PR and between 81ea743 and 8ab9b69.

📒 Files selected for processing (10)
  • changes/42885-api-only-endpoints-middleware
  • cmd/fleet/serve.go
  • server/activity/internal/service/endpoint_utils.go
  • server/api_endpoints/api_endpoints.go
  • server/mdm/android/service/endpoint_utils.go
  • server/service/endpoint_utils.go
  • server/service/integration_core_test.go
  • server/service/integration_enterprise_test.go
  • server/service/middleware/auth/api_only.go
  • server/service/middleware/auth/api_only_test.go

Comment thread server/service/middleware/auth/api_only_test.go Outdated
@juan-fdz-hawa juan-fdz-hawa marked this pull request as ready for review April 20, 2026 18:37
@juan-fdz-hawa juan-fdz-hawa requested a review from a team as a code owner April 20, 2026 18:37
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/service/middleware/auth/api_only.go (1)

49-52: Minor: consider denying explicitly on missing method/template rather than relying on catalog miss.

Today, a missing kithttp.ContextKeyRequestMethod or missing route template produces an empty-path fingerprint that happens not to be in the catalog, which is then denied. This is fine defensively, but it couples correctness of a security check to the contents of the catalog. A short explicit guard (e.g., if requestMethod == "" || routeTemplate == "" { return nil, permissionDenied(ctx) }) makes intent clear and produces a better debugging signal if wiring regresses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/service/middleware/auth/api_only.go` around lines 49 - 52, The code
currently derives requestMethod via kithttp.ContextKeyRequestMethod and
routeTemplate via eu.RouteTemplateFromContext and then computes fp with
fleet.NewAPIEndpointFromTpl(...).Fingerprint(); add an explicit guard that
checks if requestMethod == "" || routeTemplate == "" and in that case
immediately return the existing permissionDenied(ctx) result instead of relying
on a catalog miss—update the block around where requestMethod, routeTemplate and
fp are computed (the call sites of kithttp.ContextKeyRequestMethod,
eu.RouteTemplateFromContext and fleet.NewAPIEndpointFromTpl) to perform this
check and early-return to make the intent and error signal explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/fleet/users.go`:
- Around line 83-108: The apiEndpointFingerprintSet cache can become stale and
suffers a data race on lazy init; fix by making the cache always-in-sync and
removing the unsynchronized lazy-write: stop writing u.apiEndpointFingerprintSet
in APIEndpointFingerprintSet(), instead ensure every mutation of APIEndpoints
goes through a single setter that updates both u.APIEndpoints and
u.apiEndpointFingerprintSet (e.g., add SetAPIEndpoints([]APIEndpoint) or make
APIEndpoints private and provide a setter), update call sites that currently
assign user.APIEndpoints directly (UserPayload.User() and ModifyAPIOnlyUser) to
use that setter, and change APIEndpointFingerprintSet() to only return the
precomputed map (no lazy init/write). This ensures no concurrent writes and no
stale cache.

---

Nitpick comments:
In `@server/service/middleware/auth/api_only.go`:
- Around line 49-52: The code currently derives requestMethod via
kithttp.ContextKeyRequestMethod and routeTemplate via
eu.RouteTemplateFromContext and then computes fp with
fleet.NewAPIEndpointFromTpl(...).Fingerprint(); add an explicit guard that
checks if requestMethod == "" || routeTemplate == "" and in that case
immediately return the existing permissionDenied(ctx) result instead of relying
on a catalog miss—update the block around where requestMethod, routeTemplate and
fp are computed (the call sites of kithttp.ContextKeyRequestMethod,
eu.RouteTemplateFromContext and fleet.NewAPIEndpointFromTpl) to perform this
check and early-return to make the intent and error signal explicit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b1dadb6-e695-4362-8c7f-c4ca79b4391f

📥 Commits

Reviewing files that changed from the base of the PR and between 8ab9b69 and 78d2f01.

📒 Files selected for processing (8)
  • cmd/fleet/serve.go
  • server/activity/internal/service/endpoint_utils.go
  • server/datastore/mysql/users.go
  • server/fleet/users.go
  • server/platform/endpointer/route_template.go
  • server/service/integration_enterprise_test.go
  • server/service/middleware/auth/api_only.go
  • server/service/middleware/auth/api_only_test.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/fleet/serve.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/service/integration_enterprise_test.go

Comment thread server/fleet/users.go Outdated
Copy link
Copy Markdown
Member

@lucasmrod lucasmrod left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Just some nit comments.

Comment thread server/fleet/users.go Outdated
Comment on lines +84 to +86
// apiEndpointFingerprintSet is a pre-computed fingerprint set of APIEndpoints,
// populated by SetAPIEndpointFingerprintSet at load time.
apiEndpointFingerprintSet map[string]struct{}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need a mutex for protection?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove the map lookup - I think it's a premature optimization not worth the added complexity.

Comment thread server/datastore/mysql/users.go Outdated
for _, u := range users {
if u.APIOnly {
u.APIEndpoints = byUserID[u.ID]
u.SetAPIEndpointFingerprintSet()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed given APIEndpointFingerprintSet will pre-compute before returning if empty?

Comment thread server/activity/internal/service/endpoint_utils.go
Comment thread server/service/integration_core_test.go
Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/service/middleware/auth/api_only.go (1)

63-68: Optional: precompute allow-list fingerprints to avoid O(n) work per request.

For API-only users with a non-empty APIEndpoints, every request rebuilds a fingerprint for each allow-listed endpoint via NewAPIEndpointFromTpl(...).Fingerprint(). If users commonly have more than a handful of entries, caching the fingerprint set on the User (or computing it once at load time) turns this into an O(1) map lookup and removes repeated string allocation/normalization on the hot path.

♻️ Sketch
-    // Check whether the requested endpoint matches any of the user's allowed endpoints.
-    for _, ep := range v.User.APIEndpoints {
-        if fleet.NewAPIEndpointFromTpl(ep.Method, ep.Path).Fingerprint() == fp {
-            return next(ctx, request)
-        }
-    }
+    // Prefer a prebuilt set on the user, e.g. v.User.APIEndpointFingerprints()
+    // that memoizes the map once per user load.
+    if _, allowed := v.User.APIEndpointFingerprints()[fp]; allowed {
+        return next(ctx, request)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/service/middleware/auth/api_only.go` around lines 63 - 68, The loop
that calls fleet.NewAPIEndpointFromTpl(...).Fingerprint() for every request is
costly; change it to use a precomputed fingerprint set on the user (e.g., add a
field like User.APIEndpointFingerprints map[string]struct{} or []string
populated once when the User is loaded or first validated) and replace the O(n)
loop in the middleware with a constant-time map lookup against that set; update
the code paths that construct User.APIEndpoints to also populate the fingerprint
set (or lazily populate it on first access with synchronization) and then have
the middleware compare the request fingerprint (from Fingerprint()) against
User.APIEndpointFingerprints instead of rebuilding fingerprints each request.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/service/middleware/auth/api_only.go`:
- Around line 63-68: The loop that calls
fleet.NewAPIEndpointFromTpl(...).Fingerprint() for every request is costly;
change it to use a precomputed fingerprint set on the user (e.g., add a field
like User.APIEndpointFingerprints map[string]struct{} or []string populated once
when the User is loaded or first validated) and replace the O(n) loop in the
middleware with a constant-time map lookup against that set; update the code
paths that construct User.APIEndpoints to also populate the fingerprint set (or
lazily populate it on first access with synchronization) and then have the
middleware compare the request fingerprint (from Fingerprint()) against
User.APIEndpointFingerprints instead of rebuilding fingerprints each request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2834b334-e334-4991-ab07-33c1726bff3c

📥 Commits

Reviewing files that changed from the base of the PR and between 78d2f01 and fcf2282.

📒 Files selected for processing (4)
  • server/activity/internal/service/endpoint_utils.go
  • server/fleet/api_endpoints.go
  • server/service/integration_core_test.go
  • server/service/middleware/auth/api_only.go
✅ Files skipped from review due to trivial changes (1)
  • server/fleet/api_endpoints.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API-only + API-endpoints] Reject requests (403) on unauthorized API-only users with configured API endpoints

2 participants