fix: access checks that returned utils.Forbidden never blocked anything

utils.Forbidden and utils.NotFound write the response and return c.JSON's
nil. Any helper that signalled refusal by returning one of them handed its
caller a nil error, so every `if err != nil { return err }` guard passed and
the handler carried straight on.

The observable result: GET /admin/milers?tenantid=14 as a DailyGrubs login
returned HTTP 403 with all 30 of the network's riders in the body. Status
line correct, payload leaked.

Three helpers were affected:
  effectiveTenantID    (yesterday, mine) — cross-tenant read returned the
                       unfiltered list under a 403
  canAccessBooking     (was assertBookingAccess, shipped in 6d9232f) — four
                       mutating booking handlers were unguarded
  findMilerForConsole  (was assertMilerAccess) — worse, callers went on to
                       dereference the nil profile

All three now return a bool and the caller writes the refusal itself, so the
control flow is visible at the call site instead of hiding in a helper.

Adds a test that pins utils.Forbidden/NotFound returning nil, so if that ever
changes the assumption breaks loudly rather than silently, plus table tests
for effectiveTenantID.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-08-06 12:32:13 +05:30
parent 42f2a41ff6
commit f44e8fa3b4
3 changed files with 199 additions and 84 deletions

View File

@@ -26,45 +26,51 @@ import (
"github.com/redis/go-redis/v9"
)
// assertMilerAccess resolves a miler profile by its profile id and proves the
// caller is allowed to see that rider. A rider belongs to a client through the
// tenantid on their appusers row — the same link GetMilers filters on. Doormile
// staff (tenant 0) skip the check.
func assertMilerAccess(c *fiber.Ctx, milerProfileID int) (*models.MilerProfile, error) {
// findMilerForConsole resolves a miler profile by its profile id, but only if
// the caller is allowed to see that rider. A rider belongs to a client through
// the tenantid on their appusers row — the same link GetMilers filters on.
// Doormile staff (tenant 0) skip the check.
//
// The second return is "found", not an error, for the reason on
// effectiveTenantID: a version that returned utils.NotFound(...) as its error
// hands the caller a nil, so the guard passes and the handler goes on to
// dereference a nil profile.
//
// Callers report a miss as "not found" rather than "forbidden" — a client
// should not be able to probe which rider ids exist outside their own fleet.
func findMilerForConsole(c *fiber.Ctx, milerProfileID int) (*models.MilerProfile, bool) {
var profile models.MilerProfile
if err := db.DB.Where("milerprofileid = ?", milerProfileID).First(&profile).Error; err != nil {
return nil, utils.NotFound(c, "miler not found")
return nil, false
}
own := consoleTenantID(c)
if own == 0 {
return &profile, nil
return &profile, true
}
var user models.AppUser
if err := db.DB.Select("userid", "tenantid").Where("userid = ?", profile.Userid).First(&user).Error; err != nil {
return nil, utils.NotFound(c, "miler not found")
return nil, false
}
// Deliberately "not found" rather than "forbidden": a client should not be
// able to probe which rider ids exist outside their own fleet.
if user.Tenantid != own {
return nil, utils.NotFound(c, "miler not found")
return nil, false
}
return &profile, nil
return &profile, true
}
// visibleMilerUserIDs lists the appusers.userid of every rider the request
// should cover — the caller's own fleet for a client login, or the tenant
// Doormile staff asked for with ?tenantid=. Returns nil for "no restriction".
func visibleMilerUserIDs(c *fiber.Ctx) ([]int, error) {
tenantID, err := effectiveTenantID(c)
if err != nil {
return nil, err
func visibleMilerUserIDs(c *fiber.Ctx) (ids []int, allowed bool) {
tenantID, allowed := effectiveTenantID(c)
if !allowed {
return nil, false
}
if tenantID == 0 {
return nil, nil
return nil, true
}
return milerUserIDsForTenant(tenantID), nil
return milerUserIDsForTenant(tenantID), true
}
// milerSummaryRow is one line of the roster table — the shape the console's
@@ -115,9 +121,9 @@ func GetMilerSummary(c *fiber.Ctx) error {
if hubID := c.Query("hubid"); hubID != "" {
query = query.Where("hubid = ?", hubID)
}
visible, verr := visibleMilerUserIDs(c)
if verr != nil {
return verr
visible, allowed := visibleMilerUserIDs(c)
if !allowed {
return utils.Forbidden(c, "you can only view your own tenant")
}
if visible != nil {
if len(visible) == 0 {
@@ -266,9 +272,9 @@ func GetMilerLogs(c *fiber.Ctx) error {
if err != nil {
return utils.BadRequest(c, "invalid miler ID")
}
profile, aerr := assertMilerAccess(c, id)
if aerr != nil {
return aerr
profile, ok := findMilerForConsole(c, id)
if !ok {
return utils.NotFound(c, "miler not found")
}
if db.Rdb == nil {
return utils.Internal(c, "cache service unavailable")
@@ -376,9 +382,9 @@ func GetMilerActivity(c *fiber.Ctx) error {
if err != nil {
return utils.BadRequest(c, "invalid miler ID")
}
profile, aerr := assertMilerAccess(c, id)
if aerr != nil {
return aerr
profile, ok := findMilerForConsole(c, id)
if !ok {
return utils.NotFound(c, "miler not found")
}
from, to, err := parseHubDateRange(c)
if err != nil {