feat(db): permission_grants, permissions and commands given to one account or character

A new table, permission_grants (MySQL migration 96, SQLite 79), and the IPermissionGrants interface with MySQL, SQLite
and TestSQL implementations. A row grants (or with deny, takes away) a dashboard permission, a slash command, a
permission category or every command up to a GM level, for one account or one character, with an optional expiry, who
granted it and when, and a note. Rows are never deleted: removing one sets revoked_at/revoked_by, so the table is also
the history. Nothing reads it yet.

Check: both migrations run on a fresh and an existing database; DatabaseParityTests PermissionGrants passes against
MariaDB (DLU_TEST_MYSQL_HOST) and SQLite gives the same results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 00:53:19 -05:00
parent dc1c5fb36b
commit c575eba4e7
12 changed files with 262 additions and 1 deletions

View File

@@ -64,6 +64,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IAccountEmails::EmailInfo, email, confirmed);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IAccountEmails::AccountToken, accountId, data);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IAccountNotes::AccountNote, id, accountId, kind, text, actor, createdAt);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IAccountStrikes::Strike, id, accountId, characterId, source, subject, reason, givenById, givenBy, createdAt, revokedAt, revokedBy, revokeReason);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IPermissionGrants::Grant, id, targetType, targetId, kind, name, deny, expiresAt, note, grantedAt, grantedById, grantedBy, revokedAt, revokedBy);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IApiKeys::ApiKey, id, accountId, name, note, keyHash, keyPrefix, permissions, readOnly, allowedIps, allowedPaths, rateLimit, dailyQuota, createdAt, createdBy, issuedAt, expiresAt, revokedAt, revokedBy, lastUsedAt, lastIp, requestCount, quotaDay, dayCount);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ICharacterSnapshots::CharacterSnapshot, id, characterId, takenAt, reason, actor, size, hash, compressed);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IChatLog::ChatMessage, id, time, channel, senderId, senderName, accountId, recipientId, recipientName, zoneId, instanceId, cloneId, message, blocked);
@@ -1261,6 +1262,33 @@ TEST_F(ParitySeeded, AccountNotesAndStrikes) {
Both("GetAppliedStrikeSteps", [](GameDatabase& db) { return json{ db.GetAppliedStrikeSteps(2, 0), db.GetAppliedStrikeSteps(2, 1700000050), db.GetAppliedStrikeSteps(1, 0) }; });
}
TEST_F(ParitySeeded, PermissionGrants) {
Both("InsertPermissionGrant", [](GameDatabase& db) {
json ids = json::array();
ids.push_back(db.InsertPermissionGrant({ 0, "account", 2, "permission", "accounts_kick", false, 0, "helps with events", 1700000000, 1, "alice" }));
ids.push_back(db.InsertPermissionGrant({ 0, "account", 2, "command", "spawn", false, 1700001000, "", 1700000100, 1, "alice" }));
ids.push_back(db.InsertPermissionGrant({ 0, "character", CHAR_BOB, "permission_group", "Accounts", true, 0, "no tools", 1700000200, 1, "alice" }));
ids.push_back(db.InsertPermissionGrant({ 0, "account", 1, "command_group", "3", false, 0, "", 1700000300, 1, "alice" }));
ids.push_back(db.InsertPermissionGrant({ 0, "character", CHAR_ALICE, "command", "fly", false, 0, "", 1700000400, 1, "alice" }));
return ids;
});
Both("GetPermissionGrant", [](GameDatabase& db) { return json{ db.GetPermissionGrant(1), db.GetPermissionGrant(99) }; });
Both("GetPermissionGrants", [](GameDatabase& db) { return json{ db.GetPermissionGrants("account", 2), db.GetPermissionGrants("character", CHAR_BOB), db.GetPermissionGrants("account", 7) }; });
Both("GetActivePermissionGrants", [](GameDatabase& db) {
return json{ db.GetActivePermissionGrants(2, 0, 1700000500), db.GetActivePermissionGrants(2, CHAR_BOB, 1700000500),
db.GetActivePermissionGrants(2, CHAR_BOB, 1700001000), db.GetActivePermissionGrants(1, CHAR_ALICE, 1700000500) };
});
Both("GetRecentPermissionGrants", [](GameDatabase& db) {
return json{ db.GetRecentPermissionGrants(true, 1700001000, 10), db.GetRecentPermissionGrants(false, 1700001000, 2) };
});
Both("RevokePermissionGrant", [](GameDatabase& db) {
const bool first = db.RevokePermissionGrant(1, "bob", 1700000600);
const bool again = db.RevokePermissionGrant(1, "late", 1700000700);
const bool missing = db.RevokePermissionGrant(99, "bob", 1700000600);
return json{ first, again, missing, db.GetPermissionGrant(1), db.GetActivePermissionGrants(2, 0, 1700000600), db.GetRecentPermissionGrants(true, 1700000600, 10) };
});
}
TEST_F(ParitySeeded, ApiKeys) {
Both("InsertApiKey", [](GameDatabase& db) {
json ids = json::array();