# API authorization and business logic testing

Isolate object, action and tenant boundaries. Test forged roles, foreign records and stale updates in the Harbor Desk API.

Original AI-assisted SecNet educational content. Fictional local examples; not a standards provider publication, endorsement or compliance evidence. Page exercises do not create account progress.

Sources checked: 2026-10-21

## Object and function authorization

A valid token proves only part of identity. Evaluate object authorization on each record access and function authorization on each privileged action. Ignore an admin role supplied in the body; derive roles server-side. Compare owner and foreign-actor requests, and design foreign/missing responses without leaking record existence.

## Order, replay and races

Two authorized requests updating the same old version can silently overwrite each other. Compare-and-swap checks expectedVersion against the current version within one atomic write. The single-process fixture has no await between check and write; distributed production needs UPDATE ... WHERE version=? and affected-row checks. External effects such as payments also need idempotency and transaction design.

## Resource consumption and evidence

Bound body size, pagination, request rate and expensive work; a single rate limit does not solve every abuse path. Consider cost by user, tenant and operation. API Top 10 is an awareness list rather than a complete API verification plan. In the test matrix, enumerate actor × object × action × version and invariants after denial.

## Worked example: concurrent writes

Ada reads version 1 for two independent operations. If both requests submit version=1, the first accepted update advances it to 2 and the other returns 409. Silently retrying the conflict without reviewing current state can change the user’s intent. The interface should surface the conflict and present current data for a new decision. The HTTP test starts both requests concurrently and expects one 200, one 409 and final version 2. This does not prove distributed database locking: production storage needs the comparison in an atomic query. A correctly versioned but unauthorized request remains a separate denial case.

## Working sequence

1. Build the actor-object-action matrix.
2. Record a legitimate baseline request.
3. Vary one trust boundary at a time.
4. Test race and replay cases.
5. Verify denials leave data unchanged.

## 1. A foreign record

Authentication does not grant access to every object. The model denies an actor outside north/ada access to t-1. The actual API returns 404 for other users and tenants; this is the fixture’s chosen non-disclosure policy.

This code is a local policy model; it does not replace target/device testing.

### Weak implementation
```js
s=>s.authenticated
```

### Corrected implementation
```js
s=>s.authenticated&&s.actor.tenant===s.ticket.tenant&&s.actor.id===s.ticket.owner
```

### Synthetic input and actual model output
```json
{
  "before": [
    {
      "name": "owner",
      "input": {
        "authenticated": true,
        "actor": {
          "id": "ada",
          "tenant": "north"
        },
        "ticket": {
          "owner": "ada",
          "tenant": "north"
        }
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "foreign-object",
      "input": {
        "authenticated": true,
        "actor": {
          "id": "ben",
          "tenant": "south"
        },
        "ticket": {
          "owner": "ada",
          "tenant": "north"
        }
      },
      "expected": false,
      "observed": true,
      "matched": false
    },
    {
      "name": "anonymous",
      "input": {
        "authenticated": false,
        "actor": {
          "id": "ada",
          "tenant": "north"
        },
        "ticket": {
          "owner": "ada",
          "tenant": "north"
        }
      },
      "expected": false,
      "observed": false,
      "matched": true
    }
  ],
  "after": [
    {
      "name": "owner",
      "input": {
        "authenticated": true,
        "actor": {
          "id": "ada",
          "tenant": "north"
        },
        "ticket": {
          "owner": "ada",
          "tenant": "north"
        }
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "foreign-object",
      "input": {
        "authenticated": true,
        "actor": {
          "id": "ben",
          "tenant": "south"
        },
        "ticket": {
          "owner": "ada",
          "tenant": "north"
        }
      },
      "expected": false,
      "observed": false,
      "matched": true
    },
    {
      "name": "anonymous",
      "input": {
        "authenticated": false,
        "actor": {
          "id": "ada",
          "tenant": "north"
        },
        "ticket": {
          "owner": "ada",
          "tenant": "north"
        }
      },
      "expected": false,
      "observed": false,
      "matched": true
    }
  ]
}
```

## 2. Admin supplied in the body

Checking bodyRole lets a caller select authority. Even a server-side admin role must retain action and tenant scope. The allowed admin fixture does not imply unlimited authority for every administrative operation.

This code is a local policy model; it does not replace target/device testing.

### Weak implementation
```js
s=>s.bodyRole==='admin'
```

### Corrected implementation
```js
s=>s.serverRole==='admin'&&s.action==='assign-owner'&&s.actorTenant===s.ticketTenant
```

### Synthetic input and actual model output
```json
{
  "before": [
    {
      "name": "forged-role",
      "input": {
        "bodyRole": "admin",
        "serverRole": "member",
        "action": "assign-owner",
        "actorTenant": "north",
        "ticketTenant": "north"
      },
      "expected": false,
      "observed": true,
      "matched": false
    },
    {
      "name": "authorized-admin",
      "input": {
        "bodyRole": "admin",
        "serverRole": "admin",
        "action": "assign-owner",
        "actorTenant": "north",
        "ticketTenant": "north"
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "admin-other-tenant",
      "input": {
        "bodyRole": "admin",
        "serverRole": "admin",
        "action": "assign-owner",
        "actorTenant": "south",
        "ticketTenant": "north"
      },
      "expected": false,
      "observed": true,
      "matched": false
    }
  ],
  "after": [
    {
      "name": "forged-role",
      "input": {
        "bodyRole": "admin",
        "serverRole": "member",
        "action": "assign-owner",
        "actorTenant": "north",
        "ticketTenant": "north"
      },
      "expected": false,
      "observed": false,
      "matched": true
    },
    {
      "name": "authorized-admin",
      "input": {
        "bodyRole": "admin",
        "serverRole": "admin",
        "action": "assign-owner",
        "actorTenant": "north",
        "ticketTenant": "north"
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "admin-other-tenant",
      "input": {
        "bodyRole": "admin",
        "serverRole": "admin",
        "action": "assign-owner",
        "actorTenant": "south",
        "ticketTenant": "north"
      },
      "expected": false,
      "observed": false,
      "matched": true
    }
  ]
}
```

## 3. A stale write

The first write advances the version; a second write using the same old version is denied. Authorization remains a prerequisite. The downloadable HTTP test actually verifies one 200 and one 409 from two concurrent requests.

This code is a local policy model; it does not replace target/device testing.

### Weak implementation
```js
s=>s.authorized
```

### Corrected implementation
```js
s=>s.authorized&&s.expectedVersion===s.currentVersion
```

### Synthetic input and actual model output
```json
{
  "before": [
    {
      "name": "first-update",
      "input": {
        "authorized": true,
        "expectedVersion": 4,
        "currentVersion": 4
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "stale-update",
      "input": {
        "authorized": true,
        "expectedVersion": 4,
        "currentVersion": 5
      },
      "expected": false,
      "observed": true,
      "matched": false
    },
    {
      "name": "unauthorized",
      "input": {
        "authorized": false,
        "expectedVersion": 5,
        "currentVersion": 5
      },
      "expected": false,
      "observed": false,
      "matched": true
    }
  ],
  "after": [
    {
      "name": "first-update",
      "input": {
        "authorized": true,
        "expectedVersion": 4,
        "currentVersion": 4
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "stale-update",
      "input": {
        "authorized": true,
        "expectedVersion": 4,
        "currentVersion": 5
      },
      "expected": false,
      "observed": false,
      "matched": true
    },
    {
      "name": "unauthorized",
      "input": {
        "authorized": false,
        "expectedVersion": 5,
        "currentVersion": 5
      },
      "expected": false,
      "observed": false,
      "matched": true
    }
  ]
}
```

## Exit evidence

- Actor × object × action test matrix
- Version conflict and data-invariant evidence

## References

- [OWASP API Security Top 10 2023](https://api-security.owasp.org/editions/2023/en/0x11-t10/)
- [OWASP ASVS 5.0.0](https://asvs.owasp.org/)
