---
name: api-access-rule
description: Gate a GCC Player API endpoint behind an in-game entitlement, or add a new access rule kind. Covers Registry "requires", Base.requireAccess and the matching Meta.canAccess mirror. Use when an endpoint needs more than a valid key.
---

# Gating a Player API endpoint

Access is declared per endpoint as `requires` in `Registry.cfc`, and enforced in
`Base.requireAccess`.

| Value | Meaning |
|---|---|
| `""` | any valid key |
| `"project:<n>"` | caller must hold game project *n* with `finishflag = 1` |

## The governing principle

**Mirror an in-game entitlement; never invent one.** `dsr.battles` requires
project 5 because Deep Space Radar is what grants that feed in-game. The API
should expose what a player can already see, through the same condition. If
there is no in-game equivalent, the endpoint probably should not exist.

## Using an existing rule

One line in the registry entry:

```cfml
"requires": "project:5",
```

Nothing else. The router calls `guard.requireAccess(spec.requires)` before
dispatch, and `meta.whoami` already understands it.

## Adding a new rule kind

Two implementations must agree, or `meta.whoami` will advertise endpoints that
then 403:

1. **`Base.requireAccess`** — enforces, aborts with 403.
2. **`Meta.canAccess`** — answers the same question without aborting, so
   `whoami` can list what the caller may reach.

Both **fail closed** on an unrecognised rule, and must keep doing so:

```cfml
// Base.requireAccess
if (listFirst(arguments.rule, ":") == "fed")
{
    // ... check, apiError(403, ...) on failure
    return;
}
// unknown kinds fall through to denial
apiError(403, "This endpoint is not available to your account.");
```

```cfml
// Meta.canAccess — same predicate, returns boolean
if (listFirst(arguments.rule, ":") == "fed") return <same check>;
return false;   // unknown kinds fail closed here too
```

Duplicating the predicate is the cost of `whoami` being honest. Keep the two
literally side by side in review.

## Verifying a gate

A gate that has only been tested from the allowed side has not been tested.

```bash
# allowed account
curl -s -o /dev/null -w "%{http_code}\n" -H "X-API-Key: $WITH" \
  "http://127.0.0.1:8888/api/?action=dsr.battles"      # 200

# account lacking the entitlement
curl -s -o /dev/null -w "%{http_code}\n" -H "X-API-Key: $WITHOUT" \
  "http://127.0.0.1:8888/api/?action=dsr.battles"      # 403
```

And confirm `meta.whoami` agrees — the gated action should be absent from
`available` for the account that cannot reach it:

```bash
curl -s -H "X-API-Key: $WITHOUT" "http://127.0.0.1:8888/api/?action=meta.whoami"
```

A mismatch between `whoami` and the actual 403 means the two implementations
have drifted.
