# Part 12: Authorization: roles, ownership, and the bug class behind most API breaches

*We know who the caller is. Now: what may they touch? Roles, ownership checks enforced in SQL, and why a 404 is sometimes more correct than a 403.*

---

**Series:** Build a production-grade REST API in Go · **Part 12 of 17** **Where we are:** the authenticated API from Part 11, with its ownership hole still open.

At the end of Part 11 we proved the problem: every endpoint demanded a valid token, yet Bob, logged in as himself, could read and delete Ann's order and list every user. "Has a valid token" is not "may do this". The bug looks like this:

```plaintext
GET /orders/7        ← order 7 belongs to Alice. Bob is logged in. What happens?
```

If the code just fetches order 7 and returns it, Bob reads Alice's order. If he iterates `/orders/1`, `/orders/2`, `/orders/3`..., he reads everyone's. This is the **IDOR** (Insecure Direct Object Reference) bug, also called Broken Object Level Authorization. It is consistently the **#1 risk on the OWASP API Security Top 10**, because it's easy to write and invisible in normal testing: every request returns 200, and each *individual* handler looks fine.

The defense is to make the safe thing the *only* thing you can write. Here's our model.

## The rules

| Resource | Who can do what |
| --- | --- |
| `GET /users` (everyone's emails) | admin only |
| `GET/PUT/DELETE /users/{id}` | that user, or an admin |
| `POST /orders` | any logged-in user, creates an order **for themselves** |
| `GET/DELETE /orders/{id}` | the order's owner, or an admin |
| `GET /orders` | your own orders; admins see everyone's |

And roles can't be self-assigned: there's no API call that makes you an admin (Part 11).

## Rule 1: the owner comes from the token, never from the body

In earlier versions, `POST /orders` took a `user_id` in the JSON. That would let any caller create orders *as* someone else. Now:

`internal/handlers/orders.go (excerpt)`

```go
// orderInput has NO user_id. The owner is always the authenticated caller,
// taken from the token, never from the request body. Accepting an owner id from
// the client is how "create an order as someone else" bugs happen.
// (DisallowUnknownFields also rejects a client that sends one anyway.)
type orderInput struct {
	Item     string `json:"item"`
	Quantity int    `json:"quantity"`
}

func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
	var in orderInput
	if !h.decodeJSON(w, r, &in) {
		return
	}
	in.Item = strings.TrimSpace(in.Item)
	switch {
	case in.Item == "" || utf8.RuneCountInString(in.Item) > h.limits.MaxNameLen:
		h.writeError(w, r, http.StatusUnprocessableEntity, fmt.Sprintf("item is required and at most %d characters", h.limits.MaxNameLen))
		return
	case in.Quantity < 1 || in.Quantity > maxQuantity:
		h.writeError(w, r, http.StatusUnprocessableEntity, fmt.Sprintf("quantity must be between 1 and %d", maxQuantity))
		return
	}

	p, _ := auth.FromContext(r.Context())
	o, err := h.store.Create(r.Context(), p.UserID, in.Item, in.Quantity)
	if errors.Is(err, storage.ErrUserNotFound) {
		// The token is valid but its user was deleted since: treat as unauthenticated.
		h.writeError(w, r, http.StatusUnauthorized, "account no longer exists")
		return
	}
	if err != nil {
		h.internalError(w, r, "create order", err)
		return
	}
	h.writeJSON(w, r, http.StatusCreated, o)
}
```

`orderInput` has **no** `user_id` **field**, and because decoding rejects unknown fields (Part 3), a client that sends one gets a `400`. The owner is `p.UserID`, taken from the verified token via the context.

The general principle: **anything the client sends is a claim; anything the server derives from a verified credential is a fact.** Identity must come from the second kind.

## Rule 2: enforce ownership inside the SQL

The tempting implementation of `GET /orders/{id}`:

```go
order, _ := store.Get(ctx, id)        // fetch
if order.UserID != caller.UserID {    // check
    return forbidden
}
```

It works, but it's fragile: every new endpoint must remember the check, one forgetful path creates a hole, and fetch-then-compare is racy. Instead, we push the rule into the query so the unsafe version can't be written:

`internal/storage/orders.go (excerpt)`

```go
// Get fetches one order. ownerID > 0 restricts the lookup to that user's
// orders, so another user's order is simply "not found". Doing the ownership
// check INSIDE the SQL (rather than fetch-then-compare in Go) is atomic and
// makes it impossible to forget the check on a code path.
func (s *OrderStore) Get(ctx context.Context, id, ownerID int) (*Order, error) {
	o, err := scanOrder(s.pool.QueryRow(ctx,
		"SELECT "+orderCols+" FROM orders WHERE id = $1 AND ($2::bigint = 0 OR user_id = $2)", id, ownerID))
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("get order %d: %w", id, err)
	}
	return o, nil
}

// Delete removes an order; ownerID > 0 restricts it to that user's orders (see Get).
func (s *OrderStore) Delete(ctx context.Context, id, ownerID int) error {
	tag, err := s.pool.Exec(ctx,
		"DELETE FROM orders WHERE id = $1 AND ($2::bigint = 0 OR user_id = $2)", id, ownerID)
	if err != nil {
		return fmt.Errorf("delete order %d: %w", id, err)
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}
```

```sql
SELECT ... FROM orders WHERE id = $1 AND ($2::bigint = 0 OR user_id = $2)
```

The second argument is the **owner scope**: a regular user's id, or `0` meaning "any owner" for admins. If Bob asks for Alice's order, the query simply finds no row, so the store returns `ErrNotFound` and the handler answers 404. There's no "fetch then forget to check" path, because there's no fetch without the scope. Where does the scope come from?

`internal/handlers/orders.go (excerpt)`

```go
// scope returns the owner filter every query must apply: the caller's own id,
// or 0 ("any owner") for admins.
func scope(r *http.Request) int {
	p, _ := auth.FromContext(r.Context())
	if p.IsAdmin() {
		return 0
	}
	return p.UserID
}

// List returns the caller's orders. Admins see everyone's and may filter with
// ?user_id=. Regular users cannot ask for another user's orders (403).
func (h *OrderHandler) List(w http.ResponseWriter, r *http.Request) {
	userID := scope(r)
	if v := r.URL.Query().Get("user_id"); v != "" {
		id, err := strconv.Atoi(v)
		if err != nil || id <= 0 {
			h.writeError(w, r, http.StatusBadRequest, "invalid user_id")
			return
		}
		if userID != 0 && id != userID {
			h.writeError(w, r, http.StatusForbidden, "forbidden")
			return
		}
		userID = id
	}
	after, limit, ok := h.pageParams(w, r)
	if !ok {
		return
	}
	orders, more, err := h.store.List(r.Context(), userID, after, limit)
	if err != nil {
		h.internalError(w, r, "list orders", err)
		return
	}
	writePage(h.base, w, r, orders, more, func(o *storage.Order) int { return o.ID })
}
```

`scope(r)` reads the principal from the context: `0` for admins, the user's id otherwise. Every order handler passes it to the store. Changing `Get`'s signature to *require* an `ownerID` argument means the compiler forces every caller to decide. That's the best kind of security: one the type system enforces.

## Rule 3: the right status code when access is denied

Both `403 Forbidden` and `404 Not Found` are defensible; the choice leaks information:

- **404 for orders** when you don't own them. A `403` would confirm "that order exists, just not for you", which lets an attacker enumerate valid ids. With `404`, "doesn't exist" and "isn't yours" are indistinguishable.
- **403 for** `/users/{id}` **and** `GET /users`, because user ids aren't secret and the caller is authenticated; hiding the existence of a user id protects nothing, and a clear `403` helps legitimate clients debug.

`internal/handlers/users.go (excerpt)`

```go
// authorizeSelf allows the user themselves or an admin, otherwise answers 403.
// 403 (not 404) is fine here: user ids are not secret, and the caller is
// authenticated; they just may not act on someone else's account.
func (h *UserHandler) authorizeSelf(w http.ResponseWriter, r *http.Request, id int) bool {
	p, _ := auth.FromContext(r.Context())
	if p.UserID == id || p.IsAdmin() {
		return true
	}
	h.writeError(w, r, http.StatusForbidden, "forbidden")
	return false
}

// List is admin-only: it exposes every user's name and email.
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
	if p, _ := auth.FromContext(r.Context()); !p.IsAdmin() {
		h.writeError(w, r, http.StatusForbidden, "forbidden")
		return
	}
	after, limit, ok := h.pageParams(w, r)
	if !ok {
		return
	}
	users, more, err := h.store.List(r.Context(), after, limit)
	if err != nil {
		h.internalError(w, r, "list users", err)
		return
	}
	writePage(h.base, w, r, users, more, func(u *storage.User) int { return u.ID })
}
```

Whichever you choose, choose it **deliberately and consistently**, and write the reasoning down (as these comments do).

For `GET /orders?user_id=X`, a regular user asking for someone else's id gets `403` (an explicit request for something forbidden), while the default (no filter) quietly returns only their own.

## Rule 4: separate "who can ask" from "what exists"

The `User` JSON includes `role`, but update requests don't accept it:

```json
PUT /users/5  {"name":"x","role":"admin"}   → 400 (unknown field "role")
```

Because `userInput` only has `name`, privilege escalation through the update endpoint is structurally impossible, not "checked and rejected". This is the same trick as Rule 1: don't validate dangerous input, **don't accept it at all**.

## Testing authorization

Authorization bugs are invisible to happy-path tests, so test the *attacks*. The test below plays two users and an admin:

`internal/handlers/handlers_test.go (excerpt)`

```go
func TestOrderOwnership(t *testing.T) {
	e := newEnv(t)
	ann, bob := e.register("ann@example.com"), e.register("bob@example.com")
	admin := e.admin("root@example.com")

	// Validation, and the owner can NOT be chosen by the client.
	for _, bad := range []string{`{"item":"","quantity":1}`, `{"item":"b","quantity":0}`, `{"item":"b","quantity":10001}`,
		`{"item":"b","quantity":1,"user_id":2}`} {
		if got, _ := e.do("POST", "/orders", ann.AccessToken, bad); got != 422 && got != 400 {
			t.Errorf("%s: status %d", bad, got)
		}
	}
	status, body := e.do("POST", "/orders", ann.AccessToken, `{"item":"book","quantity":2}`)
	if status != 201 {
		t.Fatalf("create: %d %s", status, body)
	}
	var o struct {
		ID     int `json:"id"`
		UserID int `json:"user_id"`
	}
	_ = json.Unmarshal([]byte(body), &o)
	if o.UserID != ann.User.ID {
		t.Fatalf("order owner = %d, want the caller %d", o.UserID, ann.User.ID)
	}
	path := fmt.Sprintf("/orders/%d", o.ID)

	// IDOR checks: Bob guessing Ann's order id learns nothing, and changes nothing.
	if got, _ := e.do("GET", path, bob.AccessToken, ""); got != 404 {
		t.Errorf("bob get: %d, want 404", got)
	}
	if got, _ := e.do("DELETE", path, bob.AccessToken, ""); got != 404 {
		t.Errorf("bob delete: %d, want 404", got)
	}
	if got, _ := e.do("GET", fmt.Sprintf("/orders?user_id=%d", ann.User.ID), bob.AccessToken, ""); got != 403 {
		t.Errorf("bob listing ann's orders: %d, want 403", got)
	}
	_, list := e.do("GET", "/orders", bob.AccessToken, "")
	if !strings.Contains(list, `"data":[]`) {
		t.Errorf("bob sees orders that are not his: %s", list)
	}
	_, list = e.do("GET", "/orders", ann.AccessToken, "")
	if !strings.Contains(list, `"item":"book"`) {
		t.Errorf("ann cannot see her order: %s", list)
	}

	// Owner and admin can read; admin can filter by user.
	if got, _ := e.do("GET", path, ann.AccessToken, ""); got != 200 {
		t.Errorf("ann get: %d", got)
	}
	if got, _ := e.do("GET", path, admin.AccessToken, ""); got != 200 {
		t.Errorf("admin get: %d", got)
	}
	if got, _ := e.do("GET", fmt.Sprintf("/orders?user_id=%d", ann.User.ID), admin.AccessToken, ""); got != 200 {
		t.Errorf("admin filter: %d", got)
	}
	if got, _ := e.do("GET", "/orders?user_id=x", admin.AccessToken, ""); got != 400 {
		t.Errorf("bad filter: %d", got)
	}

	// Deleting the account cascades to the orders.
	if got, _ := e.do("DELETE", path, ann.AccessToken, ""); got != 204 {
		t.Errorf("owner delete: %d", got)
	}
	if got, _ := e.do("DELETE", path, ann.AccessToken, ""); got != 404 {
		t.Errorf("delete again: %d", got)
	}
}
```

Notice the structure: Ann creates an order, then **Bob tries every operation on it** (get, delete, list with Ann's id) and each must fail. These tests are the executable form of the table at the top of this post. Another test walks every protected endpoint with *no* token, a garbage token and an *expired* token and demands `401` each time:

`internal/handlers/handlers_test.go (excerpt)`

```go
func TestAuthRequired(t *testing.T) {
	e := newEnv(t)
	endpoints := []struct{ method, path string }{
		{"GET", "/auth/me"}, {"GET", "/users"}, {"GET", "/users/1"}, {"PUT", "/users/1"}, {"DELETE", "/users/1"},
		{"GET", "/orders"}, {"POST", "/orders"}, {"GET", "/orders/1"}, {"DELETE", "/orders/1"},
	}
	// An expired-but-correctly-signed token (same secret, negative TTL).
	expiredTM, _ := auth.NewTokenManager(secret, "test", -time.Minute)
	expired, _ := expiredTM.Issue(1, "admin")

	for _, ep := range endpoints {
		for name, tok := range map[string]string{"none": "", "garbage": "abc.def.ghi", "expired": expired} {
			if got, _ := e.do(ep.method, ep.path, tok, `{}`); got != 401 {
				t.Errorf("%s %s with %s token: status %d, want 401", ep.method, ep.path, name, got)
			}
		}
	}
	// Public endpoints stay public.
	if got, _ := e.do("POST", "/auth/login", "", `{"email":"a@b.co","password":"xxxxxxxxxxxx"}`); got != 401 {
		t.Fatalf("login should be reachable (401 for bad creds), got %d", got)
	}
}
```

If someone adds a route and forgets `protected(...)`, adding it to that list turns the omission into a failing test.

## Complete code for this part

Everything to type, file by file. `orders.go` (both layers) is rewritten around "the owner comes from the token" and "scope the SQL by owner"; `users.go` gains the self-or-admin and admin-only rules. Then `go build ./...`.

**Change** `internal/handlers/orders.go`

```diff
--- a/internal/handlers/orders.go
+++ b/internal/handlers/orders.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 	"unicode/utf8"
 
+	"go-rest/internal/auth"
 	"go-rest/internal/storage"
 )
 
@@ -17,9 +18,9 @@ const maxQuantity = 10_000
 
 type OrderStore interface {
 	List(ctx context.Context, userID, afterID, limit int) ([]*storage.Order, bool, error) // userID 0 = all
-	Get(ctx context.Context, id int) (*storage.Order, error)
+	Get(ctx context.Context, id, ownerID int) (*storage.Order, error)                     // ownerID 0 = any
 	Create(ctx context.Context, userID int, item string, quantity int) (*storage.Order, error)
-	Delete(ctx context.Context, id int) error
+	Delete(ctx context.Context, id, ownerID int) error
 }
 
 type OrderHandler struct {
@@ -31,22 +32,40 @@ func NewOrderHandler(store OrderStore, log *slog.Logger, limits Limits) *OrderHa
 	return &OrderHandler{base: base{log: log, limits: limits}, store: store}
 }
 
+// orderInput has NO user_id. The owner is always the authenticated caller,
+// taken from the token, never from the request body. Accepting an owner id from
+// the client is how "create an order as someone else" bugs happen.
+// (DisallowUnknownFields also rejects a client that sends one anyway.)
 type orderInput struct {
-	UserID   int    `json:"user_id"`
 	Item     string `json:"item"`
 	Quantity int    `json:"quantity"`
 }
 
-// List supports an optional filter and pagination:
-// GET /orders?user_id=3&limit=20&cursor=...
+// scope returns the owner filter every query must apply: the caller's own id,
+// or 0 ("any owner") for admins.
+func scope(r *http.Request) int {
+	p, _ := auth.FromContext(r.Context())
+	if p.IsAdmin() {
+		return 0
+	}
+	return p.UserID
+}
+
+// List returns the caller's orders. Admins see everyone's and may filter with
+// ?user_id=. Regular users cannot ask for another user's orders (403).
 func (h *OrderHandler) List(w http.ResponseWriter, r *http.Request) {
-	userID := 0
+	userID := scope(r)
 	if v := r.URL.Query().Get("user_id"); v != "" {
-		var err error
-		if userID, err = strconv.Atoi(v); err != nil || userID <= 0 {
+		id, err := strconv.Atoi(v)
+		if err != nil || id <= 0 {
 			h.writeError(w, r, http.StatusBadRequest, "invalid user_id")
 			return
 		}
+		if userID != 0 && id != userID {
+			h.writeError(w, r, http.StatusForbidden, "forbidden")
+			return
+		}
+		userID = id
 	}
 	after, limit, ok := h.pageParams(w, r)
 	if !ok {
@@ -65,7 +84,7 @@ func (h *OrderHandler) Get(w http.ResponseWriter, r *http.Request) {
 	if !ok {
 		return
 	}
-	o, err := h.store.Get(r.Context(), id)
+	o, err := h.store.Get(r.Context(), id, scope(r))
 	if err != nil {
 		h.storeError(w, r, "get order", err)
 		return
@@ -80,9 +99,6 @@ func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
 	}
 	in.Item = strings.TrimSpace(in.Item)
 	switch {
-	case in.UserID <= 0:
-		h.writeError(w, r, http.StatusUnprocessableEntity, "user_id is required")
-		return
 	case in.Item == "" || utf8.RuneCountInString(in.Item) > h.limits.MaxNameLen:
 		h.writeError(w, r, http.StatusUnprocessableEntity, fmt.Sprintf("item is required and at most %d characters", h.limits.MaxNameLen))
 		return
@@ -91,11 +107,11 @@ func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	o, err := h.store.Create(r.Context(), in.UserID, in.Item, in.Quantity)
+	p, _ := auth.FromContext(r.Context())
+	o, err := h.store.Create(r.Context(), p.UserID, in.Item, in.Quantity)
 	if errors.Is(err, storage.ErrUserNotFound) {
-		// The URL itself is valid; the *body* references something that
-		// doesn't exist, so 422 is more accurate than 404.
-		h.writeError(w, r, http.StatusUnprocessableEntity, "user_id does not exist")
+		// The token is valid but its user was deleted since: treat as unauthenticated.
+		h.writeError(w, r, http.StatusUnauthorized, "account no longer exists")
 		return
 	}
 	if err != nil {
@@ -110,7 +126,7 @@ func (h *OrderHandler) Delete(w http.ResponseWriter, r *http.Request) {
 	if !ok {
 		return
 	}
-	if err := h.store.Delete(r.Context(), id); err != nil {
+	if err := h.store.Delete(r.Context(), id, scope(r)); err != nil {
 		h.storeError(w, r, "delete order", err)
 		return
 	}
```

**Change** `internal/handlers/users.go`

```diff
--- a/internal/handlers/users.go
+++ b/internal/handlers/users.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 	"unicode/utf8"
 
+	"go-rest/internal/auth"
 	"go-rest/internal/storage"
 )
 
@@ -17,7 +18,6 @@ import (
 type UserStore interface {
 	List(ctx context.Context, afterID, limit int) ([]*storage.User, bool, error)
 	Get(ctx context.Context, id int) (*storage.User, error)
-	Create(ctx context.Context, name string) (*storage.User, error)
 	Update(ctx context.Context, id int, name string) (*storage.User, error)
 	Delete(ctx context.Context, id int) error
 }
@@ -38,7 +38,24 @@ type userInput struct {
 	Name string `json:"name"`
 }
 
+// authorizeSelf allows the user themselves or an admin, otherwise answers 403.
+// 403 (not 404) is fine here: user ids are not secret, and the caller is
+// authenticated; they just may not act on someone else's account.
+func (h *UserHandler) authorizeSelf(w http.ResponseWriter, r *http.Request, id int) bool {
+	p, _ := auth.FromContext(r.Context())
+	if p.UserID == id || p.IsAdmin() {
+		return true
+	}
+	h.writeError(w, r, http.StatusForbidden, "forbidden")
+	return false
+}
+
+// List is admin-only: it exposes every user's name and email.
 func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
+	if p, _ := auth.FromContext(r.Context()); !p.IsAdmin() {
+		h.writeError(w, r, http.StatusForbidden, "forbidden")
+		return
+	}
 	after, limit, ok := h.pageParams(w, r)
 	if !ok {
 		return
@@ -53,7 +70,7 @@ func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
 
 func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
 	id, ok := h.parseID(w, r)
-	if !ok {
+	if !ok || !h.authorizeSelf(w, r, id) {
 		return
 	}
 	user, err := h.store.Get(r.Context(), id)
@@ -64,22 +81,9 @@ func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
 	h.writeJSON(w, r, http.StatusOK, user)
 }
 
-func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
-	in, ok := h.decodeUser(w, r)
-	if !ok {
-		return
-	}
-	created, err := h.store.Create(r.Context(), in.Name)
-	if err != nil {
-		h.internalError(w, r, "create user", err)
-		return
-	}
-	h.writeJSON(w, r, http.StatusCreated, created)
-}
-
 func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
 	id, ok := h.parseID(w, r)
-	if !ok {
+	if !ok || !h.authorizeSelf(w, r, id) {
 		return
 	}
 	in, ok := h.decodeUser(w, r)
@@ -96,7 +100,7 @@ func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
 
 func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
 	id, ok := h.parseID(w, r)
-	if !ok {
+	if !ok || !h.authorizeSelf(w, r, id) {
 		return
 	}
 	if err := h.store.Delete(r.Context(), id); err != nil {
```

**Replace the contents of** `internal/routes/routes.go` **with**

```go
package routes

import (
	"net/http"

	"go-rest/internal/handlers"
)

// Deps are the handlers plus the authentication middleware to guard them.
type Deps struct {
	Auth         *handlers.AuthHandler
	Users        *handlers.UserHandler
	Orders       *handlers.OrderHandler
	Authenticate func(http.Handler) http.Handler
}

// SetupRoutes registers every endpoint. Reading this file answers "which routes
// are public?": only those registered with `public`.
func SetupRoutes(mux *http.ServeMux, d Deps) {
	public := func(pattern string, h http.HandlerFunc) { mux.Handle(pattern, h) }
	protected := func(pattern string, h http.HandlerFunc) { mux.Handle(pattern, d.Authenticate(h)) }

	public("POST /auth/register", d.Auth.Register)
	public("POST /auth/login", d.Auth.Login)
	public("POST /auth/refresh", d.Auth.Refresh)
	public("POST /auth/logout", d.Auth.Logout)
	protected("GET /auth/me", d.Auth.Me)

	protected("GET /users", d.Users.List) // admin only (checked in the handler)
	protected("GET /users/{id}", d.Users.Get)
	protected("PUT /users/{id}", d.Users.Update)
	protected("DELETE /users/{id}", d.Users.Delete)

	protected("GET /orders", d.Orders.List)
	protected("POST /orders", d.Orders.Create)
	protected("GET /orders/{id}", d.Orders.Get)
	protected("DELETE /orders/{id}", d.Orders.Delete)
}
```

**Change** `internal/storage/orders.go`

```diff
--- a/internal/storage/orders.go
+++ b/internal/storage/orders.go
@@ -83,8 +83,13 @@ func (s *OrderStore) List(ctx context.Context, userID, afterID, limit int) ([]*O
 	return orders, more, nil
 }
 
-func (s *OrderStore) Get(ctx context.Context, id int) (*Order, error) {
-	o, err := scanOrder(s.pool.QueryRow(ctx, "SELECT "+orderCols+" FROM orders WHERE id = $1", id))
+// Get fetches one order. ownerID > 0 restricts the lookup to that user's
+// orders, so another user's order is simply "not found". Doing the ownership
+// check INSIDE the SQL (rather than fetch-then-compare in Go) is atomic and
+// makes it impossible to forget the check on a code path.
+func (s *OrderStore) Get(ctx context.Context, id, ownerID int) (*Order, error) {
+	o, err := scanOrder(s.pool.QueryRow(ctx,
+		"SELECT "+orderCols+" FROM orders WHERE id = $1 AND ($2::bigint = 0 OR user_id = $2)", id, ownerID))
 	if errors.Is(err, pgx.ErrNoRows) {
 		return nil, ErrNotFound
 	}
@@ -113,8 +118,10 @@ func (s *OrderStore) Create(ctx context.Context, userID int, item string, quanti
 	return o, nil
 }
 
-func (s *OrderStore) Delete(ctx context.Context, id int) error {
-	tag, err := s.pool.Exec(ctx, "DELETE FROM orders WHERE id = $1", id)
+// Delete removes an order; ownerID > 0 restricts it to that user's orders (see Get).
+func (s *OrderStore) Delete(ctx context.Context, id, ownerID int) error {
+	tag, err := s.pool.Exec(ctx,
+		"DELETE FROM orders WHERE id = $1 AND ($2::bigint = 0 OR user_id = $2)", id, ownerID)
 	if err != nil {
 		return fmt.Errorf("delete order %d: %w", id, err)
 	}
```

**Change** `internal/storage/users.go`

```diff
--- a/internal/storage/users.go
+++ b/internal/storage/users.go
@@ -110,16 +110,6 @@ func (s *UserStore) Get(ctx context.Context, id int) (*User, error) {
 	return u, nil
 }
 
-// Create adds a user without credentials. Part 12 removes it: accounts are created through registration.
-func (s *UserStore) Create(ctx context.Context, name string) (*User, error) {
-	u, err := scanUser(s.pool.QueryRow(ctx,
-		"INSERT INTO users (name) VALUES ($1) RETURNING "+userCols, name))
-	if err != nil {
-		return nil, fmt.Errorf("insert user: %w", err)
-	}
-	return u, nil
-}
-
 // CreateWithCredentials registers a normal (role=user) account. email must
 // already be normalised (trimmed, lower-cased) and passwordHash already hashed.
 func (s *UserStore) CreateWithCredentials(ctx context.Context, name, email, passwordHash string) (*User, error) {
```

## Try it

Re-run the scenario that ended Part 11 (here the admin you created with `make admin` in Part 11 is user 1, Ann is 2, Bob is 3):

```bash
# Ann creates an order. No user_id in the body any more: the owner is whoever the token says.
$ curl -XPOST localhost:7800/orders -H "Authorization: Bearer $ANN" -d '{"item":"book","quantity":2}'
{"id":1,"user_id":2,"item":"book","quantity":2,"created_at":"2026-10-24T02:09:56.095932+05:30"}

# Trying to name an owner is rejected outright (unknown field)
$ curl -w ' [HTTP %{http_code}]\n' -XPOST localhost:7800/orders -H "Authorization: Bearer $ANN" -d '{"user_id":3,"item":"book","quantity":2}'
{"error":"invalid JSON body"} [HTTP 400]

# Bob, who could read and delete this order at the end of Part 11:
$ curl -w ' [HTTP %{http_code}]\n' localhost:7800/orders/1 -H "Authorization: Bearer $BOB"
{"error":"not found"} [HTTP 404]
$ curl -o /dev/null -w 'DELETE [HTTP %{http_code}]\n' -XDELETE localhost:7800/orders/1 -H "Authorization: Bearer $BOB"
DELETE [HTTP 404]

# ...sees none of Ann's orders, and cannot ask for them by id:
$ curl localhost:7800/orders -H "Authorization: Bearer $BOB"
{"data":[]}
$ curl -o /dev/null -w 'orders?user_id=2 [HTTP %{http_code}]\n' "localhost:7800/orders?user_id=2" -H "Authorization: Bearer $BOB"
orders?user_id=2 [HTTP 403]

# ...and the user list is for admins only:
$ curl -w ' [HTTP %{http_code}]\n' localhost:7800/users -H "Authorization: Bearer $BOB"
{"error":"forbidden"} [HTTP 403]
$ curl "localhost:7800/users?limit=2" -H "Authorization: Bearer $ADMIN"
{"data":[{"id":1,"name":"admin","email":"root@example.com","role":"admin", ...},{"id":2,"name":"Ann", ...}],"next_cursor":"Mg"}
```

The same requests that returned `200` and `204` at the end of Part 11 now answer `404` and `403`.

## One loose end: `POST /users`

Accounts can only come from `/auth/register` now (they need a password), so the old `POST /users` is removed: the route, the handler, and `UserStore.Create`. Calling it answers `405 Method Not Allowed`. The "Complete code" section above deletes it from all three places.

## A checklist you can reuse on any API

1. **Every route authenticated by default**; public ones are explicit exceptions.
2. For each resource, write down *who* may read, create, update, delete. (The table at the top.)
3. **Identity from the token, not the body.**
4. **Scope queries by owner** in SQL, and make the scope a required parameter.
5. Don't accept fields the caller shouldn't set (role, owner, id) at all.
6. Choose 403 vs 404 on purpose.
7. Write tests that *attack*: wrong user, no token, expired token, tampered token.
8. Log the user id on every request (Part 11 did) so incidents can be reconstructed.

## What this model doesn't cover

- **Fine-grained permissions**: we have two roles. Real systems grow to permissions ("orders:refund"), teams and organizations (RBAC/ABAC). The *structure* (identity from the token, scope in the query) scales to those; only the policy gets richer.
- **Multi-tenancy**: with tenants, every table needs a tenant id in every query. PostgreSQL **row-level security** can enforce that in the database itself, a robust option worth studying.
- **Audit trails**: who changed what, and when, as durable records (not just logs).

## Recap

- Authentication says who; authorization says what. Test them separately.
- IDOR: ids in URLs are guessable; every object access must check ownership.
- Take identity from the token; scope SQL by owner; make the scope a required argument.
- Don't accept privileged fields at all; structure beats validation.
- 404 vs 403 is an information-disclosure decision.
- Test the attacker's path, not just the user's.

## Exercises

1. Add `GET /users/{id}/orders` (the orders of one user). Which scope rule applies? Write the attack test first.
2. A new engineer adds `PUT /orders/{id}` and writes `store.Update(ctx, id, quantity)` with no owner parameter. How would you restructure the code so this mistake fails to compile or fails a test?
3. Read about PostgreSQL row-level security (`CREATE POLICY`). Sketch how it would replace the `$2::bigint = 0 OR user_id = $2` clause, and what it would take to set `app.current_user_id` per request safely with a connection pool.
