Spec: Wi-Fi access becomes an explicit allow-list, configured in settings

Two changes from the operator.

Session length and devices-per-role are set in the settings UI, on the existing
Portal settings page rather than a new one. Two presentation decisions that are
really correctness decisions:

  · the role is picked from a DROPDOWN of seeded roles, not typed. match_value is
    compared exactly, so "Student" or "studnet" matches nobody and the failure
    surfaces as a pupil with no Wi-Fi and nothing logged anywhere. A dropdown
    makes an unmatchable value impossible to enter.
  · session length is entered in DAYS with presets. session_minutes stays the
    stored unit, but a term is 172,800 minutes and nobody should type that — the
    same reasoning that presents cron schedules as presets instead of raw strings.

And access is now an explicit allow-list: no role added means no Wi-Fi. That makes
`default` an anti-feature, because a default row matches everybody and turns the
allow-list into a suggestion, so it is removed from the available match types.
resolveMapping() already returns null when nothing matches and null already
produces the "No WiFi access configured for your account." denial, so
deny-by-default is the behaviour as soon as the catch-all cannot be created.

Safe to do now specifically because unifi_vlan_mappings has ZERO rows in every
environment — nothing is revoked from anybody. The same change made after a school
had leaned on a catch-all would cut off every unmapped person at once.

VLAN stays required per row. It was not in the request, so it is called out in the
spec rather than quietly dropped: the network still has to know where to put the
device.
This commit is contained in:
2026-08-21 12:48:47 -04:00
parent 79a8360714
commit c745aa8b2c

View File

@@ -53,6 +53,8 @@ The portal is not the obstacle. **MAC randomization is** — see decision 2.
| 4 | Add `role` as a `match_type` | Quotas were asked for per role; roles are the platform's own authority and need no external sync | | 4 | Add `role` as a `match_type` | Quotas were asked for per role; roles are the platform's own authority and need no external sync |
| 5 | One Wi-Fi Devices page grouped by person; no profile card | Matches the requirement exactly; keeps `/admin/users` as the one directory | | 5 | One Wi-Fi Devices page grouped by person; no profile card | Matches the requirement exactly; keeps `/admin/users` as the one directory |
| 6 | Device type is **reported, not enforced**, for now | Whether `dev_cat` is populated in this environment is unverified; enforcing on unvalidated data can lock out the wrong people | | 6 | Device type is **reported, not enforced**, for now | Whether `dev_cat` is populated in this environment is unverified; enforcing on unvalidated data can lock out the wrong people |
| 7 | **Explicit allow-list only** — no `default` mapping | No Wi-Fi unless an admin has added that role and set an amount. A `default` row silently grants everyone, which is the opposite of the requirement |
| 8 | Roles are chosen from a dropdown; session length entered in days | A free-text role slug that matches nothing fails silently — it presents as "this student has no Wi-Fi" with no error anywhere to explain it |
--- ---
@@ -173,6 +175,41 @@ so "one term" means one term.
--- ---
## C2. Settings: who gets Wi-Fi, and how much
Access is an **explicit allow-list**. A person gets Wi-Fi only if one of their
roles has been added here with an amount. No role added means no Wi-Fi, and that
is the intended answer rather than a gap to be filled by a catch-all.
Consequences, stated so nobody re-adds the hole later:
- **`default` is removed from the available match types.** A `default` row matches
everybody, so its existence makes the allow-list advisory. `resolveMapping()`
already returns null when nothing matches, and null already produces the
"No WiFi access configured for your account." denial — so deny-by-default is
the behaviour once the catch-all cannot be created.
- Safe to remove now: **there are zero rows in `unifi_vlan_mappings` in any
environment**, so nothing is being revoked from anyone. Doing this later, after
a school had leaned on a catch-all, would cut off every unmapped person at once.
- `email_domain`, `group` and `ou` stay available — they are narrow matches, not
catch-alls, and removing working options is not part of this.
### The settings screen
Extends the existing Portal settings page rather than adding another one. Per row:
| Field | Control | Why |
|---|---|---|
| Role | **dropdown of real seeded roles** | Free text is the defect. `match_value` is a string compared exactly, so `Student` or `studnet` matches nobody and the failure surfaces as a pupil with no Wi-Fi and nothing logged. The dropdown makes an unmatchable value impossible to enter. |
| Devices | number, 1..50 | The quota. Column is already bounded. |
| Session length | **days, with presets** (a week / a month / a term / a year) | `session_minutes` stays the stored unit, but a term is 172,800 minutes and nobody should type that. Same reasoning as presenting cron schedules as presets rather than raw strings. |
| VLAN | existing select | **Still required** — the network needs to know where to put the device, and this was not in the original request, so it is called out rather than quietly dropped. |
| Order | existing `sort_order` | Precedence for multi-role people: staff above parent gives a staff-parent 4. |
Roles already in the system and available to add: staff, student, parent,
oncall-staff, coach, alumni, applicant, board-member, contact, homeschool,
homeschool-parent, homeschool-student, open-house, tuition-payee, forms-payer.
## D. The Wi-Fi Devices page ## D. The Wi-Fi Devices page
New page under Unifi, gated on the existing `unifi.auth`, grouped by person. New page under Unifi, gated on the existing `unifi.auth`, grouped by person.