Skip to content

Add multi-server-per-admin support via sa_admins_servers table#287

Open
Ravid-A wants to merge 2 commits into
daffyyyy:mainfrom
Ravid-A:feature/multi-server-per-admin
Open

Add multi-server-per-admin support via sa_admins_servers table#287
Ravid-A wants to merge 2 commits into
daffyyyy:mainfrom
Ravid-A:feature/multi-server-per-admin

Conversation

@Ravid-A
Copy link
Copy Markdown

@Ravid-A Ravid-A commented May 31, 2026

Summary

Admin records are bound to a single server through the sa_admins.server_id column (NULL = global). Granting one admin to several specific servers means duplicating the whole admin row (plus its flag rows) per server, and "all servers" is modelled as a NULL foreign-key value, which is awkward to query and easy to get wrong.

This PR introduces a sa_admins_servers link table — mirroring the existing sa_groups / sa_groups_servers pattern — so a single admin record can be attached to many servers. It also adds an explicit global flag on sa_admins for all-servers admins, replacing the NULL-server_id convention. Existing data is migrated automatically and behaviour is preserved.

Changes

All changes are additive/mechanical against eea700b.

  • Migration 017_CreateAdminsServersTable.sql (MySQL + SQLite) — creates sa_admins_servers (id, admin_id, server_id NOT NULL, FK admin_id → sa_admins(id) ON DELETE CASCADE); adds global column to sa_admins (default 0); backfills (server-specific admins → one link row each, old NULL-server admins → global = 1); then drops sa_admins.server_id. Both new files are registered in the .csproj for CopyToOutputDirectory, matching every other migration.
  • IDatabaseProvider — adds GetAddAdminServerQuery() and GetDeleteOrphanedAdminsQuery().
  • MysqlDatabaseProvider / SqliteDatabaseProvider (symmetric):
    • GetAdminsQuery — now selects admins where global = 1 OR a sa_admins_servers row exists for the current server (via EXISTS, so the flag join doesn't multiply rows). Previously filtered on sa_admins.server_id.
    • GetAddAdminQuery — inserts the global column instead of server_id.
    • GetAddAdminServerQuery — inserts the per-server link row.
    • GetDeleteAdminQuery(globalDelete: false) — deletes only the current server's link row, leaving the admin and its other server assignments intact. Global delete still removes the sa_admins row (cascading the links).
    • GetDeleteOrphanedAdminsQuery — deletes non-global admins that have no remaining sa_admins_servers rows (see Notes).
  • PermissionManagerAddAdminBySteamId: -g sets global = 1 and writes no link row (applies everywhere); otherwise global = 0 and a link row is written for the current server. New DeleteOrphanedAdmins() runs the orphan-cleanup query.
  • PlayerManagerDeleteOrphanedAdmins() is added to the existing expire-timer task batch (alongside DeleteOldAdmins()), so orphans are swept on the same cadence as expired bans/mutes/warns.
  • The global identifier is a MySQL reserved word, so it is backticked in every statement.

Test plan

  • SQLite migration backfill — synthetic sa_admins (one NULL-server, two server-specific) run through 017: NULL admin → global = 1 with no link row; server-specific admins → one link row each with correct server_id; sa_admins.server_id dropped; sa_admins_servers.server_id enforced NOT NULL. (SQLite 3.50.4 via System.Data.SQLite.Core 1.0.119.)
  • SQLite load queryGetAdminsQuery returns, per server: server 5 → global + server-5 admin; server 7 → global + server-7 admin; unregistered server 9 → global only.
  • SQLite orphan sweep — after a non-global delete strips an admin's last link, GetDeleteOrphanedAdminsQuery removes exactly that admin (global = 0, no links) while leaving the global admin and a still-linked admin untouched.
  • dotnet build (Rebuild, net8.0) — clean, 0 warnings / 0 errors.
  • MySQL — not retested by submitter (no MySQL test env handy). The SQL mirrors the existing sa_groups_servers patterns and existing admin queries; EXISTS, the global backfill, DROP COLUMN, and the orphan DELETE … NOT IN (subquery on a different table) are all standard MySQL. A second pair of eyes on a MySQL deployment before merge would be welcome.
  • Live in-game smoke test — recommend a quick css_addadmin (with and without -g) and css_deladmin on a running server at merge time to confirm permission load/unload end-to-end.

Notes

  • Backward compatible. Existing global and server-specific admins keep working unchanged after the migration; no manual data steps required.
  • Orphan handling. A non-global delete only removes the current server's link row, so an admin can be left with global = 0 and zero links — invisible to every server (never loaded) but still occupying rows. DeleteOrphanedAdmins() sweeps these on the expire timer so they don't accumulate.
  • SQLite FK cascade caveat (pre-existing, unchanged). SQLite connections in SqliteDatabaseProvider don't issue PRAGMA foreign_keys = ON, so ON DELETE CASCADE doesn't fire on SQLite — exactly as with the existing sa_admins_flags table. This means an orphaned admin's sa_admins_flags rows can linger on SQLite after the admin row is swept; MySQL cascades them. Enabling the pragma globally would fix this class of issue but is a separate cleanup, out of scope here.
  • The globalAdmin/-g plumbing in basecommands.cs is left as-is; it now drives the global column instead of a NULL server_id.

Tested SA version: eea700b. Submitted from Ravid-A:feature/multi-server-per-admin.

Ravid-A added a commit to Ravid-A/CS2-SimpleAdmin that referenced this pull request May 31, 2026
Add multi-server-per-admin support via sa_admins_servers table daffyyyy#287
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant