Skip to content

Commit 367aa93

Browse files
ryanbarlow97claude
andauthored
LuckPerms bridge for the website staff panel (#32)
* Add LuckPerms bridge config and site client Adds the luckperms-bridge config section (publish, apply, poll-seconds, snapshot-seconds; off by default) and a client for the staff panel's LuckPerms plugin routes: snapshot upload, unchanged-snapshot check, change polling and result posting. Results and checks carry the revision the site uses to order them. Snapshots go through the gateway's longer upload timeout because they can be about a megabyte. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Bridge LuckPerms and the staff panel LuckPermsBridge runs one daemon worker thread (TFMCWeb-LuckPerms) that publishes LuckPerms snapshots and, on the one server set to apply, polls staff-queued changes, so a snapshot never predates an applied change. Unchanged snapshots are only confirmed by hash; a full upload follows when the site asks for one or the data changed. Results the site did not take are kept and posted again on later ticks. Snapshots and results carry a strictly increasing revision. Failures are logged once per outage with fixed messages. LuckPermsBridgeStore, loaded by class name so TFMCWeb still runs without LuckPerms, builds sorted snapshots (groups, tracks, every user with stored nodes, cached player names) and applies changes: every precondition is checked in op order against a staged copy of the holder first, then the live holder is changed and saved once. A failed mutation or save reloads loaded holders from storage. Each applied change pushes a messaging update and is written to the LuckPerms action log. The plugin starts, reloads and stops the bridge, and /web status reports it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Document the LuckPerms bridge Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Refuse admin changes to staff players and staff groups A change may carry a guard listing the groups a website admin (not root) may manage. When it does, the bridge checks live LuckPerms data before mutating anything: the player must hold no positive inheritance node, in any context or expiry, for a group outside that list (target_is_staff), and every group the change adds must, with all the groups it inherits transitively, stay inside the list (root_only). Names compare case-insensitively, inheritance cycles are safe, and a dangling parent counts as outside the list. A guard that is present but malformed allows no groups, and a guarded group or track change is refused as root_only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Serialise LuckPerms writes between the bridge and Patreon writer The staff-panel bridge and the Patreon rank writer run on different threads and both load, change, save and clean up the same users, so one could reload a user from storage mid-way and drop the other's change. LuckPermsWriteLock holds one global lock; the Patreon store holds it around each user write and the bridge around every user, group and track change. Writes are rare, so one lock is enough. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep an outlived bridge pass from running beside a new worker A pass blocked on storage or the site can outlive stop(), and the old active flag let it resume under a refreshed worker, racing the new one on the remembered/unposted results, revision and snapshot state. Each pass is now bound to the worker generation that scheduled it; stop() advances the generation. Passes run under one lock, so two generations never interleave, and per-generation state is reset by the first pass of a new generation. A superseded pass fetches and publishes nothing more; a LuckPerms save already in progress completes, and the rest of its fetched batch is answered save_failed (the site hands out each change once, so every fetched change still gets a result). stop() no longer interrupts the worker and waits at most a second, so a reload never stalls the main thread for long. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Remove a group or track created by a change that then fails createAndLoadGroup and createAndLoadTrack store the new holder at once, so a create change whose node, track-group or save step then failed left an empty group or track behind and answered save_failed; retrying it returned group_exists or track_exists. The created holder is now deleted before the failure is returned; an existing holder is still reloaded from storage. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Treat a player as staff when any inherited group is staff The admin guard compared only the player's direct groups with the admin groups. If an allowed group had just gained a staff parent in LuckPerms before the site's mirror caught up, an admin could still edit that player. Every positive inheritance node the player holds, in any context or expiry, now has its live inheritance walked (case-insensitive, cycle-safe, a missing group counts as outside), and any group outside the admin groups refuses the change with target_is_staff. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent c1d7a2e commit 367aa93

20 files changed

Lines changed: 3395 additions & 5 deletions

‎README.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,30 @@ The same bridge connects selected moderation and roleplay events to the wider co
1414
- **Moderation connections** — sends player warnings, and bans, unbans and expired timed bans from the server ban list, to the linked services so the Discord Banned role follows the in-game ban.
1515
- **Player benefit synchronization** — shares resolved player metadata and entitlements with ProvinceSystem.
1616
- **Bird-mail notifications** — provides the connection for BirdMessenger arrival notifications to linked Discord accounts.
17+
- **Staff-panel LuckPerms bridge** — publishes LuckPerms groups, tracks and player permissions to the website's staff panel, and applies the rank and permission changes staff queue there through LuckPerms.
1718

1819
These features rely on the corresponding TF-Minecraft services and gameplay integrations being available.
1920

21+
## LuckPerms bridge
22+
23+
The bridge is off by default and needs LuckPerms on the server:
24+
25+
```yaml
26+
luckperms-bridge:
27+
publish: false # send LuckPerms snapshots to this server's site
28+
apply: false # also apply staff-queued changes (implies publish)
29+
poll-seconds: 3 # how often to fetch queued changes when applying
30+
snapshot-seconds: 30 # how often to publish a snapshot
31+
```
32+
33+
LuckPerms storage is shared between servers, so set `apply: true` on exactly one
34+
of them. A server that only publishes makes its site's panel read-only. Changes
35+
are checked against live LuckPerms data, saved together or not at all, logged in
36+
`/lp log`, and followed by a fresh snapshot. A change made by a website admin
37+
(rather than root) is refused if the player already holds a staff group, or if
38+
the group it adds, or any group that group inherits, is outside the admin groups. `/web status` shows the bridge state
39+
and the age of the last snapshot.
40+
2041
## Documentation
2142

2243
[Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/TFMCWeb/README.md)

‎src/main/java/net/tfminecraft/tfmcweb/Cache.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ public final class Cache {
7676
public static boolean banMirrorEnabled = true;
7777
public static int banMirrorPollSeconds = 30;
7878

79+
/** Publish LuckPerms snapshots to this server's site. {@link #luckPermsBridgeApply} implies it. */
80+
public static boolean luckPermsBridgePublish = false;
81+
/** Apply staff-queued LuckPerms changes. LuckPerms storage is shared: one server only. */
82+
public static boolean luckPermsBridgeApply = false;
83+
public static int luckPermsBridgePollSeconds = 3;
84+
public static int luckPermsBridgeSnapshotSeconds = 30;
85+
7986
private Cache() {}
8087

8188
/** Mutable noble/gilded/ascended → same-name group map. */

‎src/main/java/net/tfminecraft/tfmcweb/TFMCWeb.java‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import net.tfminecraft.tfmcweb.gate.DiscordGateService;
1111
import net.tfminecraft.tfmcweb.listeners.PlayerJoinListener;
1212
import net.tfminecraft.tfmcweb.loaders.ConfigLoader;
13+
import net.tfminecraft.tfmcweb.luckperms.LuckPermsBridge;
1314
import net.tfminecraft.tfmcweb.entitlements.PlayerMetaSyncService;
1415
import net.tfminecraft.tfmcweb.managers.BanMirrorPoller;
1516
import net.tfminecraft.tfmcweb.managers.LinkDiscordCommand;
@@ -30,6 +31,7 @@ public class TFMCWeb extends JavaPlugin {
3031
private DiscordGateService gateService;
3132
private PluginNoticePoller noticePoller;
3233
private PatreonRankWriter rankWriter;
34+
private LuckPermsBridge luckPermsBridge;
3335
private BanMirrorPoller banMirror;
3436

3537
@Override
@@ -52,6 +54,8 @@ public void onEnable() {
5254
noticePoller.start();
5355
rankWriter = new PatreonRankWriter(this);
5456
rankWriter.refresh();
57+
luckPermsBridge = new LuckPermsBridge(this);
58+
luckPermsBridge.refresh();
5559

5660
if (getCommand("linkdiscord") != null) {
5761
getCommand("linkdiscord").setExecutor(new LinkDiscordCommand(this, linkCache, gateService));
@@ -66,7 +70,13 @@ public void onEnable() {
6670
getLogger().severe("Command unlinkdiscord missing from plugin.yml");
6771
}
6872
if (getCommand("web") != null) {
69-
WebCommand web = new WebCommand(this, linkCache, gateService, rankWriter);
73+
WebCommand web = new WebCommand(
74+
this,
75+
linkCache,
76+
gateService,
77+
rankWriter,
78+
luckPermsBridge
79+
);
7080
getCommand("web").setExecutor(web);
7181
getCommand("web").setTabCompleter(web);
7282
} else {
@@ -116,6 +126,9 @@ public void onDisable() {
116126
if (rankWriter != null) {
117127
rankWriter.stop();
118128
}
129+
if (luckPermsBridge != null) {
130+
luckPermsBridge.stop();
131+
}
119132
if (banMirror != null) {
120133
banMirror.stop();
121134
}
@@ -134,6 +147,9 @@ public void reloadLocalConfig() {
134147
if (rankWriter != null) {
135148
rankWriter.refresh();
136149
}
150+
if (luckPermsBridge != null) {
151+
luckPermsBridge.refresh();
152+
}
137153
PlayerMetaSyncService.pushAllOnlineAsync();
138154
}
139155

0 commit comments

Comments
 (0)