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

@@ -227,6 +227,13 @@ public:
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override;
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override;
// IPermissionGrants
uint64_t InsertPermissionGrant(const Grant& grant) override;
std::optional<Grant> GetPermissionGrant(uint64_t id) override;
std::vector<Grant> GetPermissionGrants(const std::string& targetType, int64_t targetId) override;
std::vector<Grant> GetActivePermissionGrants(uint32_t accountId, int64_t characterId, int64_t now) override;
std::vector<Grant> GetRecentPermissionGrants(bool activeOnly, int64_t now, uint32_t limit) override;
bool RevokePermissionGrant(uint64_t id, const std::string& revokedBy, int64_t time) override;
// IApiKeys
uint64_t InsertApiKey(const ApiKey& key) override;
std::optional<ApiKey> GetApiKey(uint64_t id) override;

View File

@@ -12,6 +12,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"AccountNotes.cpp"
"AccountStrikes.cpp"
"ApiKeys.cpp"
"PermissionGrants.cpp"
"Moderation.cpp"
"ServerHealth.cpp"
"PlayerPositions.cpp"

View File

@@ -0,0 +1,64 @@
#include "MySQLDatabase.h"
namespace {
template<typename Result> IPermissionGrants::Grant ReadGrant(Result& result) {
IPermissionGrants::Grant grant;
grant.id = result->getUInt64("id");
grant.targetType = result->getString("target_type").c_str();
grant.targetId = result->getInt64("target_id");
grant.kind = result->getString("kind").c_str();
grant.name = result->getString("name").c_str();
grant.deny = result->getInt("deny") != 0;
grant.expiresAt = result->getInt64("expires_at");
grant.note = result->getString("note").c_str();
grant.grantedAt = result->getInt64("granted_at");
grant.grantedById = result->getUInt("granted_by_id");
grant.grantedBy = result->getString("granted_by").c_str();
grant.revokedAt = result->getInt64("revoked_at");
grant.revokedBy = result->getString("revoked_by").c_str();
return grant;
}
}
uint64_t MySQLDatabase::InsertPermissionGrant(const Grant& grant) {
ExecuteInsert("INSERT INTO permission_grants (target_type, target_id, kind, name, deny, expires_at, note, granted_at, granted_by_id, granted_by) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
grant.targetType, grant.targetId, grant.kind, grant.name, grant.deny, grant.expiresAt, grant.note, grant.grantedAt, grant.grantedById, grant.grantedBy);
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
return result->next() ? result->getUInt64("id") : 0;
}
std::optional<IPermissionGrants::Grant> MySQLDatabase::GetPermissionGrant(uint64_t id) {
auto result = ExecuteSelect("SELECT * FROM permission_grants WHERE id = ?;", id);
if (!result->next()) return std::nullopt;
return ReadGrant(result);
}
std::vector<IPermissionGrants::Grant> MySQLDatabase::GetPermissionGrants(const std::string& targetType, int64_t targetId) {
std::vector<Grant> grants;
auto result = ExecuteSelect("SELECT * FROM permission_grants WHERE target_type = ? AND target_id = ? ORDER BY id DESC;", targetType, targetId);
while (result->next()) grants.push_back(ReadGrant(result));
return grants;
}
std::vector<IPermissionGrants::Grant> MySQLDatabase::GetActivePermissionGrants(uint32_t accountId, int64_t characterId, int64_t now) {
std::vector<Grant> grants;
auto result = ExecuteSelect("SELECT * FROM permission_grants WHERE revoked_at = 0 AND (expires_at = 0 OR expires_at > ?) AND "
"((target_type = 'account' AND target_id = ?) OR (? <> 0 AND target_type = 'character' AND target_id = ?)) ORDER BY id DESC;",
now, static_cast<int64_t>(accountId), characterId, characterId);
while (result->next()) grants.push_back(ReadGrant(result));
return grants;
}
std::vector<IPermissionGrants::Grant> MySQLDatabase::GetRecentPermissionGrants(bool activeOnly, int64_t now, uint32_t limit) {
std::vector<Grant> grants;
auto result = activeOnly
? ExecuteSelect("SELECT * FROM permission_grants WHERE revoked_at = 0 AND (expires_at = 0 OR expires_at > ?) ORDER BY id DESC LIMIT ?;", now, limit)
: ExecuteSelect("SELECT * FROM permission_grants ORDER BY id DESC LIMIT ?;", limit);
while (result->next()) grants.push_back(ReadGrant(result));
return grants;
}
bool MySQLDatabase::RevokePermissionGrant(uint64_t id, const std::string& revokedBy, int64_t time) {
return ExecuteUpdate("UPDATE permission_grants SET revoked_at = ?, revoked_by = ? WHERE id = ? AND revoked_at = 0;", time, revokedBy, id) > 0;
}