---
name: forum-acl-section
description: Add or change a configurable clearance level on the GCC Forum — register a capability in aclRegistry, gate the code with aclCap, and pick a default. Use when a forum power needs its own level, or when a hardcoded level number needs to become configurable.
---

# Adding a forum ACL capability

A **capability** is one configurable power. `Base.aclRegistry()` is the single
source of truth; `gcc.forum_acl` holds per-install overrides. The model is in
[`../../docs/ACCESS-CONTROL.md`](../../docs/ACCESS-CONTROL.md); this is the
procedure. It mirrors `app/Admin/Skills/admin-acl-section` on purpose — same
shape, different application, separate table.

**Changing who can browse, create, read or reply is a different job** — read
[`forum-access-rule`](../forum-access-rule/SKILL.md) first. This skill is only
about making a level *configurable* and choosing its default.

## The three steps

### 1. Register it

```cfc
"avatars" : { "label": "Take down another player's avatar", "def": 4, "group": "Staff powers" },
```

- **Key** — dotted and grouped by prefix so related powers sort together
  (`post.news`, `post.updates`). Max 40 characters: that is the `acl_key` column.
- **Label** — reads as a capability to somebody who does not know the code.
  Plain text, no entities: the view encodes it.
- **`def`** — the level the code enforces **today**. Getting this wrong is how a
  "no functional change" commit quietly demotes every Guide.
- **`group`** — an existing group where possible: `Staff powers`, `Posting`,
  `Sections`.

### 2. Gate the code with it

```cfc
if (me().level GTE aclCap("avatars"))      // right
if (me().level GTE 4)                      // wrong: now unconfigurable
```

Keep the old constant if it documents *why* the number was chosen
(`AV_MOD_LEVEL` in `Avatar.cfc` does), but read the level through `aclCap()`.

### 3. Verify both directions

Default first — an empty `forum_acl` must behave exactly as before:

```cfc
say("default: level 3 cannot mod avatars", !asUser(1,3).canModerateAvatars(), "");
say("default: level 4 CAN mod avatars",     asUser(1,4).canModerateAvatars(), "");
```

Then with an override, in both directions, and back to default:

```cfc
asUser(302041,10).aclSet("avatars", 2, 4);  application.forumAclAt = 0;
say("override: level 2 CAN now",  asUser(1,2).canModerateAvatars(), "");
say("override: level 1 still cannot", !asUser(1,1).canModerateAvatars(), "");
```

**`asUser()` rewrites the session**, so an owner-only call must re-own
immediately before it. A component held from earlier in the script runs as
whoever is current — that is not a bug in `aclSet`, it is how `me()` works, and
it will read as a mysterious "the override did not apply".

## Sections are in the same table

A board section's browse level is `section.<he_type id>`, defaulting to
`he_type.access`. Two things to keep:

- **One row per `he_type` id, not per board.** Access is a property of the
  `he_type` row — `canSeeSection()` takes an id and nothing else — and id 1 is
  mapped by both boards (the Forum's Archive, the Help Center's Questions).
- **The default comes from `typeInfo()`, not `sectionMap()`.** `sectionMap`'s
  `access` already has the override folded in; using it as the default makes
  "back to default" mean "back to whatever it is now", and the row can never be
  reset.

## Choosing a default

| Default | Fits |
|---|---|
| 0 | Anything every player does |
| 1 | Being staff at all (`staff`) |
| 2–3 | Routine moderation (`mod`, `senior`) |
| 4–5 | Reaches past one thread (`avatars`, `post.updates`) |
| 9 | Speaks for the game (`post.news`) |
| 10 | Owner only. Reserved for the Access Control screen itself |

## Pinned, and why

`ACL_ADMIN_LEVEL` (10) gates the screen, is checked in `canAcl()` and is **not**
in the registry, so it can never be lowered from inside the screen it protects.
Do not add it — a configurable lock is not a lock.

## Don't forget

- **Adding a method to a CFC needs `docker restart gcc-local-app`** on this dev
  stack. A new capability that only reads `aclCap()` does not.
- Overrides cache for **60 seconds**. `aclSet` busts it; a row changed by hand
  in SQL takes up to a minute.
- **The legacy `hef.cfm` knows nothing about `forum_acl`.** A section made
  stricter here is still at its `he_type` level on the old board.
- Every save writes `forum_modlog` with action `acl`. Keep that — a level that
  moved needs to be attributable.
