Skip to content

Whitelist

The Whitelist module wraps Paper's native whitelist (enable/disable/list/ add/remove) with UniversalAdmin's own metadata - who added a player, when, why, any notes, and an optional expiration - built entirely on the existing Module/GUI/Action/Audit/Permission infrastructure. Every mutation is an Action run through ActionExecutor, so it is permission-checked and audited the same way regardless of frontend.

Open via /admin → Whitelist (needs universaladmin.whitelist.view).

The native whitelist (whitelist.json) is always the source of truth for who is actually whitelisted. UniversalAdmin's own ua_whitelist table only ever adds metadata on top of that - see "Ownership & the expiry sweep" below for why this matters.

Network whitelist

In network mode there is a second whitelist for the whole network, kept in the shared database instead of a server's whitelist.json: /admin whitelist network on|off|status|add|remove|list (same arguments as for this server's whitelist, add with an optional duration and reason) and a second row on the Whitelist page.

  • While it is on, a player gets onto any server with UniversalAdmin - and through the proxy with UniversalAdmin Velocity or BungeeCord - only if they are on it and their entry has not expired, or hold universaladmin.bypass.network-whitelist (op by default). It applies on top of each server's own whitelist, which keeps working as before.
  • Switching it on does not kick anyone who is already online - like the vanilla whitelist.
  • Changes reach every server within a second; expired entries let nobody in and are cleaned up by the hourly sweep.
  • Managing it needs universaladmin.whitelist.network in addition to .toggle/.add/.remove.

Each server's own metadata is kept per server (server:<name> scope) - until 1.3.0 every server of a network shared one table, so one server's expiry sweep could un-whitelist a player on another. On the first start of 1.3.0 a network server takes over the entries for the players on its own native whitelist.

GUI

  • Status - whether the whitelist is currently on, with an Enable/Disable toggle (universaladmin.whitelist.toggle).
  • Members - every native whitelist member, each marked either "Managed by UniversalAdmin" (has a row - click through to see who added them, when, why, and any expiration) or "Externally managed" (no row - added outside UniversalAdmin; still a real whitelist member, just not one this module annotated).
  • Search Offline Player - part of the Add wizard: type a name substring, pick from the matches (Bukkit.getOfflinePlayers(), capped at whitelist.gui.search-max-results, default 50 - a plain scan is proportionate here since this is an occasional, admin-initiated search over a small result set, unlike the Players module's full browser).
  • Add - search → select → one form with an optional reason, optional notes (multi-line) and an optional expiration (only with universaladmin.whitelist.temporary) → confirm.
  • Remove - from a member's detail page; removes the native entry and any UniversalAdmin metadata for them, regardless of who originally added them (an explicit, permission-gated removal is an intentional admin action, not something the "don't touch entries we don't own" rule below applies to).

Temporary whitelist entries

An Add can carry an optional expiration (needs universaladmin.whitelist.temporary in addition to .add - an admin can hold one without the other). Expiry is enforced two ways:

  1. On join (WhitelistJoinListener, AsyncPlayerPreLoginEvent) - if a player is still natively whitelisted but their UniversalAdmin-tracked entry has already expired, the join is denied and the native entry is cleaned up immediately. This is the case vanilla's own whitelist check can't catch on its own: the player is still on the native list at that instant.
  2. Periodic sweep (hourly, same pattern as the Moderation module's punishment-expiry sweep) - catches entries whose owner never tries to join again, so whitelist.json stays accurate over time even without a login attempt.

Both paths remove an entry by running the exact same whitelist.remove Action through ActionExecutor (with Actor.system("whitelist-expiry") in place of an admin) that the GUI's Remove button uses - so an automatic expiry produces the same kind of audit entry a manual removal would, distinguishable by actor.

Ownership & the expiry sweep

UniversalAdmin only ever tracks metadata for whitelist entries it created itself. A player who is natively whitelisted by some other means - vanilla /whitelist add, a hand-edited whitelist.json, another plugin - simply has no row in ua_whitelist. No row means UniversalAdmin has no opinion about that entry at all.

This matters because the automatic paths (the join-time check and the periodic sweep) never iterate "everyone on the native whitelist" - they only ever iterate UniversalAdmin's own table (WhitelistEntryRepository#findAll()). A native-only entry is therefore never visible to, let alone touched by, anything automatic - it's not filtered out by a check, it's structurally never seen in the first place. This is what "Nicht fremde manuell gesetzte Whitelist-Einträge unbeabsichtigt löschen" means in practice: automatic code paths have zero code that could reach a foreign entry.

WhitelistSource exists to make this an explicit assertion rather than an implicit "well there's a row so I guess we own it" - every row's source is UNIVERSAL_ADMIN, and both the sweep and the join check verify this before treating an entry as theirs to expire, even though it's the only value that exists today. If Add is used on a player who happened to already be natively whitelisted by someone else, that's a legitimate, intentional ownership transfer (an authorized admin explicitly added them through UniversalAdmin) - not the "unintentional" case the rule is about.

An explicit Remove click is different from the automatic paths: it's an authorized, intentional admin action and may remove any native entry regardless of who (or what) added it - it just never invents a fake ownership history for what it deletes.

Permissions

Node Default Meaning
universaladmin.whitelist.view op View the whitelist status and members
universaladmin.whitelist.toggle op Enable/disable the whitelist
universaladmin.whitelist.add op Add a player to the whitelist
universaladmin.whitelist.remove op Remove a player from the whitelist
universaladmin.whitelist.temporary op Give a whitelist entry an expiration
universaladmin.whitelist.network op Manage the network whitelist (with toggle/add/remove)
universaladmin.bypass.network-whitelist op Join while the network whitelist is on without being on it

See docs/user/permissions.md for how these fit into the platform-wide list, and docs/architecture/actions.md for how a node on ActionDefinition.Builder#permission(...) gets enforced.

Settings

Setting Default Meaning
whitelist.gui.search-max-results 50 Maximum offline players returned by one "Search Offline Player" query

Registered under the module's own whitelist settings namespace (see docs/development/settings.md).

Database

ua_whitelist (WhitelistScopeMigration, version 1014) - one row per UniversalAdmin-managed whitelist entry and scope: scope (local without network mode, server:<name> per network server, network for the network whitelist) and player_id (together the primary key), player_name, source, added_by_id/added_by_name, added_at, reason (nullable), notes (nullable), expires_at (nullable - NULL means permanent). For a server's own whitelist never the source of truth for membership - see "Ownership & the expiry sweep" above; for the network whitelist it is the list itself. ua_whitelist_state holds the network whitelist's on/off switch. The table used before 1.3.0, whitelist_entries (version 1007), was copied to the local scope and is no longer written.

Commands

/admin whitelist [network] on|off|add <player> [duration] [reason]|remove <player>|list (alias and shortcut /wl; network for the network whitelist, which also has status). add also works for players who never joined (online-mode servers). See commands.md.