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

@@ -46,20 +46,25 @@ func isDoormileConsoleStaff(c *fiber.Ctx) bool {
// one client's slice of the network. Returns 0 for "no restriction", which only
// Doormile staff can reach.
//
// A client passing ?tenantid= for someone else is refused rather than silently
// ignored — a filter that quietly returns the wrong tenant's data is worse than
// an error either way.
func effectiveTenantID(c *fiber.Ctx) (int, error) {
// The second return is "allowed", not an error, and deliberately so: the
// utils.* response helpers all return c.JSON's nil, so a helper that signalled
// refusal by returning utils.Forbidden(...) would hand its caller a nil error.
// Every `if err != nil` guard built that way silently passes and the handler
// carries on to write real data into a response already stamped 403. Callers
// must write the refusal themselves.
func effectiveTenantID(c *fiber.Ctx) (tenantID int, allowed bool) {
own := consoleTenantID(c)
requested := c.QueryInt("tenantid", 0)
if own != 0 {
// A client asking for someone else's tenant is refused rather than
// silently given their own data back under the wrong label.
if requested != 0 && requested != own {
return 0, utils.Forbidden(c, "you can only view your own tenant")
return 0, false
}
return own, nil
return own, true
}
return requested, nil
return requested, true
}
// scopeToTenant restricts a query to one tenant by its own tenant column,
@@ -130,25 +135,26 @@ func canAccessTenant(c *fiber.Ctx, tenantID int) bool {
return own == 0 || own == tenantID
}
// assertBookingAccess checks that the caller may act on a booking addressed by
// id. Returns nil for Doormile staff. Mutating handlers take the booking id
// canAccessBooking reports whether the caller may act on a booking addressed by
// id. Always true for Doormile staff. Mutating handlers take the booking id
// straight from the path, so a scoped SELECT elsewhere in the handler does not
// protect them — this has to run before the write.
func assertBookingAccess(c *fiber.Ctx, bookingID int) error {
//
// Returns a bool rather than an error for the reason spelled out on
// effectiveTenantID: utils.NotFound returns nil, so the previous
// error-returning version never actually blocked anything.
func canAccessBooking(c *fiber.Ctx, bookingID int) bool {
own := consoleTenantID(c)
if own == 0 {
return nil
return true
}
var booking models.PickupBooking
if err := db.DB.Select("bookingid", "tenantid").First(&booking, bookingID).Error; err != nil {
return utils.NotFound(c, "booking not found")
return false
}
// A booking with no tenant predates tenant attribution and can't be proven
// to belong to this client, so it stays invisible to them.
if booking.Tenantid == nil || *booking.Tenantid != own {
return utils.NotFound(c, "booking not found")
}
return nil
return booking.Tenantid != nil && *booking.Tenantid == own
}
// Helper to generate tripsheet number
@@ -229,9 +235,9 @@ func LoginAdmin(cfg *config.Config) fiber.Handler {
func GetAdminDashboard(c *fiber.Ctx) error {
// Pinned to the caller's own tenant for a client login; Doormile staff pass
// ?tenantid= to see one client's numbers, or omit it for the whole network.
tenantID, terr := effectiveTenantID(c)
if terr != nil {
return terr
tenantID, allowed := effectiveTenantID(c)
if !allowed {
return utils.Forbidden(c, "you can only view your own tenant")
}
var totalTenants int64
@@ -299,9 +305,9 @@ func GetAdminReports(c *fiber.Ctx) error {
// ownTenant is 0 for a Doormile-staff whole-network view, the client's own
// tenant for a client login, or the tenant Doormile staff asked for with
// ?tenantid=. Every figure below is restricted to it.
ownTenant, terr := effectiveTenantID(c)
if terr != nil {
return terr
ownTenant, allowed := effectiveTenantID(c)
if !allowed {
return utils.Forbidden(c, "you can only view your own tenant")
}
var totalBookings int64
@@ -862,9 +868,9 @@ func GetAdminCustomers(c *fiber.Ctx) error {
// A client sees only the customers they have actually delivered to, not
// Doormile's whole B2C address book. Doormile staff can ask for one
// client's customers with ?tenantid=.
tenantID, terr := effectiveTenantID(c)
if terr != nil {
return terr
tenantID, allowed := effectiveTenantID(c)
if !allowed {
return utils.Forbidden(c, "you can only view your own tenant")
}
if tenantID != 0 {
query = query.Where("appcustomerid IN (?)",
@@ -1406,9 +1412,9 @@ func DeleteVehicle(c *fiber.Ctx) error {
// --------------------
func GetMilers(c *fiber.Ctx) error {
tenantID, terr := effectiveTenantID(c)
if terr != nil {
return terr
tenantID, allowed := effectiveTenantID(c)
if !allowed {
return utils.Forbidden(c, "you can only view your own tenant")
}
var profiles []models.MilerProfile
@@ -1511,9 +1517,9 @@ func GetMilerDetails(c *fiber.Ctx) error {
// GetMilers scopes the roster to the caller's own fleet, but reading one
// rider by id did not — a client login could walk the whole network's riders
// by incrementing the id.
profile, err := assertMilerAccess(c, id)
if err != nil {
return err
profile, ok := findMilerForConsole(c, id)
if !ok {
return utils.NotFound(c, "miler not found")
}
return utils.OK(c, profile)
}
@@ -1540,9 +1546,9 @@ func AdminNotifyMiler(c *fiber.Ctx) error {
return utils.BadRequest(c, "title and message are required")
}
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 profile.Devicetoken == "" {
@@ -1558,9 +1564,9 @@ func AdminNotifyMiler(c *fiber.Ctx) error {
func UpdateMiler(c *fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("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")
}
type MilerUpdate struct {
@@ -1593,9 +1599,9 @@ func UpdateMiler(c *fiber.Ctx) error {
func BlockMiler(c *fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("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")
}
tx := db.DB.Begin()
@@ -1630,9 +1636,9 @@ func AssignMilerVehicle(c *fiber.Ctx) error {
return utils.BadRequest(c, "invalid request body")
}
profile, aerr := assertMilerAccess(c, id)
if aerr != nil {
return aerr
profile, ok := findMilerForConsole(c, id)
if !ok {
return utils.NotFound(c, "miler not found")
}
// A vehicle can only be handed to a rider the caller owns, and only from
@@ -1661,9 +1667,9 @@ func GetAdminBookings(c *fiber.Ctx) error {
pagesize := min(100, max(1, c.QueryInt("pagesize", 20)))
offset := (pageno - 1) * pagesize
tenantID, terr := effectiveTenantID(c)
if terr != nil {
return terr
tenantID, allowed := effectiveTenantID(c)
if !allowed {
return utils.Forbidden(c, "you can only view your own tenant")
}
query := scopeToTenant(db.DB.Model(&models.PickupBooking{}), "tenantid", tenantID)
if status := c.Query("status"); status != "" {
@@ -2058,8 +2064,8 @@ func GetAdminBookingDetails(c *fiber.Ctx) error {
func AdminAssignMiler(c *fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("id"))
if err := assertBookingAccess(c, id); err != nil {
return err
if !canAccessBooking(c, id) {
return utils.NotFound(c, "booking not found")
}
type MilerAssign struct {
@@ -2082,8 +2088,8 @@ func AdminAssignMiler(c *fiber.Ctx) error {
func AdminAssignVehicle(c *fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("id"))
if err := assertBookingAccess(c, id); err != nil {
return err
if !canAccessBooking(c, id) {
return utils.NotFound(c, "booking not found")
}
type VehicleAssign struct {
@@ -2111,8 +2117,8 @@ func AdminAssignVehicle(c *fiber.Ctx) error {
func AdminUpdateBookingStatus(c *fiber.Ctx) error {
id, _ := strconv.Atoi(c.Params("id"))
if err := assertBookingAccess(c, id); err != nil {
return err
if !canAccessBooking(c, id) {
return utils.NotFound(c, "booking not found")
}
type StatusUpdate struct {
@@ -2148,8 +2154,8 @@ func AdminCancelBooking(c *fiber.Ctx) error {
if err != nil {
return utils.BadRequest(c, "invalid booking ID")
}
if err := assertBookingAccess(c, id); err != nil {
return err
if !canAccessBooking(c, id) {
return utils.NotFound(c, "booking not found")
}
var booking models.PickupBooking
@@ -2281,9 +2287,9 @@ func AdminBulkCancelBookings(c *fiber.Ctx) error {
func GetAdminConsignments(c *fiber.Ctx) error {
page := utils.ParsePage(c)
tenantID, terr := effectiveTenantID(c)
if terr != nil {
return terr
tenantID, allowed := effectiveTenantID(c)
if !allowed {
return utils.Forbidden(c, "you can only view your own tenant")
}
var total int64