diff --git a/dDatabase/GameDatabase/GameDatabase.h b/dDatabase/GameDatabase/GameDatabase.h index 7dd9ccdae..c8a0904af 100644 --- a/dDatabase/GameDatabase/GameDatabase.h +++ b/dDatabase/GameDatabase/GameDatabase.h @@ -54,6 +54,7 @@ #include "IServerTraffic.h" #include "IApiKeys.h" #include "IUgcLookup.h" +#include "IPermissionGrants.h" #ifdef _DEBUG # define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0) @@ -67,7 +68,7 @@ class GameDatabase : public IPropertyContents, public IProperty, public IPetNames, public ICharXml, public IMigrationHistory, public IUgc, public IFriends, public ICharInfo, public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList, - public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup { + public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup, public IPermissionGrants { public: virtual ~GameDatabase() = default; // TODO: These should be made private. diff --git a/dDatabase/GameDatabase/ITables/IPermissionGrants.h b/dDatabase/GameDatabase/ITables/IPermissionGrants.h new file mode 100644 index 000000000..ab12b2392 --- /dev/null +++ b/dDatabase/GameDatabase/ITables/IPermissionGrants.h @@ -0,0 +1,43 @@ +#ifndef __IPERMISSIONGRANTS__H__ +#define __IPERMISSIONGRANTS__H__ + +#include +#include +#include +#include + +/** + * Permissions and slash commands granted to (or denied from) one account or one character, on top of what its GM level + * allows (PermissionGrants.h). Rows are never deleted: removing a grant sets revokedAt, so the rows are also the history. + */ +class IPermissionGrants { +public: + struct Grant { + uint64_t id{}; + std::string targetType; // "account" or "character" + int64_t targetId{}; // the account's ID, or the character's charinfo ID + std::string kind; // "permission", "command", "permission_group" or "command_group" + std::string name; // the permission key, command name, permission category or GM level + bool deny{}; + int64_t expiresAt{}; // 0: never + std::string note; + int64_t grantedAt{}; + uint32_t grantedById{}; // the account that granted it + std::string grantedBy; + int64_t revokedAt{}; // 0: not removed + std::string revokedBy; + }; + + virtual uint64_t InsertPermissionGrant(const Grant& grant) = 0; + virtual std::optional GetPermissionGrant(uint64_t id) = 0; + // Every grant of one target, removed and expired ones too, newest first + virtual std::vector GetPermissionGrants(const std::string& targetType, int64_t targetId) = 0; + // The grants in force at `now` (not removed, not expired) of an account and, when characterId isn't 0, one character + virtual std::vector GetActivePermissionGrants(uint32_t accountId, int64_t characterId, int64_t now) = 0; + // Every target's grants, newest first: only the ones in force at `now`, or all of them (the history) + virtual std::vector GetRecentPermissionGrants(bool activeOnly, int64_t now, uint32_t limit) = 0; + // Whether it was in force until now (false: no such grant, or already removed) + virtual bool RevokePermissionGrant(uint64_t id, const std::string& revokedBy, int64_t time) = 0; +}; + +#endif //!__IPERMISSIONGRANTS__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 7d848fb7f..d43c9aad7 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -227,6 +227,13 @@ public: std::vector GetUgcPlacements(const std::vector& ugcIds) override; std::vector GetUgcMail(const std::vector& subkeys, const LOT modelItemLot) override; + // IPermissionGrants + uint64_t InsertPermissionGrant(const Grant& grant) override; + std::optional GetPermissionGrant(uint64_t id) override; + std::vector GetPermissionGrants(const std::string& targetType, int64_t targetId) override; + std::vector GetActivePermissionGrants(uint32_t accountId, int64_t characterId, int64_t now) override; + std::vector 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 GetApiKey(uint64_t id) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt index dd89bf7cf..3f10bff30 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt @@ -12,6 +12,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES "AccountNotes.cpp" "AccountStrikes.cpp" "ApiKeys.cpp" + "PermissionGrants.cpp" "Moderation.cpp" "ServerHealth.cpp" "PlayerPositions.cpp" diff --git a/dDatabase/GameDatabase/MySQL/Tables/PermissionGrants.cpp b/dDatabase/GameDatabase/MySQL/Tables/PermissionGrants.cpp new file mode 100644 index 000000000..4af601efb --- /dev/null +++ b/dDatabase/GameDatabase/MySQL/Tables/PermissionGrants.cpp @@ -0,0 +1,64 @@ +#include "MySQLDatabase.h" + +namespace { + template 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 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 MySQLDatabase::GetPermissionGrants(const std::string& targetType, int64_t targetId) { + std::vector 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 MySQLDatabase::GetActivePermissionGrants(uint32_t accountId, int64_t characterId, int64_t now) { + std::vector 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(accountId), characterId, characterId); + while (result->next()) grants.push_back(ReadGrant(result)); + return grants; +} + +std::vector MySQLDatabase::GetRecentPermissionGrants(bool activeOnly, int64_t now, uint32_t limit) { + std::vector 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; +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 344fa1da9..03a3fbf85 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -211,6 +211,13 @@ public: std::vector GetUgcPlacements(const std::vector& ugcIds) override; std::vector GetUgcMail(const std::vector& subkeys, const LOT modelItemLot) override; + // IPermissionGrants + uint64_t InsertPermissionGrant(const Grant& grant) override; + std::optional GetPermissionGrant(uint64_t id) override; + std::vector GetPermissionGrants(const std::string& targetType, int64_t targetId) override; + std::vector GetActivePermissionGrants(uint32_t accountId, int64_t characterId, int64_t now) override; + std::vector 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 GetApiKey(uint64_t id) override; diff --git a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt index f0f23ab5a..bd739cf50 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt @@ -12,6 +12,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES "AccountNotes.cpp" "AccountStrikes.cpp" "ApiKeys.cpp" + "PermissionGrants.cpp" "Moderation.cpp" "ServerHealth.cpp" "PlayerPositions.cpp" diff --git a/dDatabase/GameDatabase/SQLite/Tables/PermissionGrants.cpp b/dDatabase/GameDatabase/SQLite/Tables/PermissionGrants.cpp new file mode 100644 index 000000000..2fd96068a --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/PermissionGrants.cpp @@ -0,0 +1,64 @@ +#include "SQLiteDatabase.h" + +namespace { + IPermissionGrants::Grant ReadGrant(CppSQLite3Query& result) { + IPermissionGrants::Grant grant; + grant.id = static_cast(result.getInt64Field("id")); + grant.targetType = result.getStringField("target_type"); + grant.targetId = result.getInt64Field("target_id"); + grant.kind = result.getStringField("kind"); + grant.name = result.getStringField("name"); + grant.deny = result.getIntField("deny") != 0; + grant.expiresAt = result.getInt64Field("expires_at"); + grant.note = result.getStringField("note"); + grant.grantedAt = result.getInt64Field("granted_at"); + grant.grantedById = static_cast(result.getInt64Field("granted_by_id")); + grant.grantedBy = result.getStringField("granted_by"); + grant.revokedAt = result.getInt64Field("revoked_at"); + grant.revokedBy = result.getStringField("revoked_by"); + return grant; + } +} + +uint64_t SQLiteDatabase::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_rowid() AS id;"); + return result.eof() ? 0 : static_cast(result.getInt64Field("id")); +} + +std::optional SQLiteDatabase::GetPermissionGrant(uint64_t id) { + auto [_, result] = ExecuteSelect("SELECT * FROM permission_grants WHERE id = ?;", static_cast(id)); + if (result.eof()) return std::nullopt; + return ReadGrant(result); +} + +std::vector SQLiteDatabase::GetPermissionGrants(const std::string& targetType, int64_t targetId) { + std::vector grants; + auto [_, result] = ExecuteSelect("SELECT * FROM permission_grants WHERE target_type = ? AND target_id = ? ORDER BY id DESC;", targetType, targetId); + for (; !result.eof(); result.nextRow()) grants.push_back(ReadGrant(result)); + return grants; +} + +std::vector SQLiteDatabase::GetActivePermissionGrants(uint32_t accountId, int64_t characterId, int64_t now) { + std::vector 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(accountId), characterId, characterId); + for (; !result.eof(); result.nextRow()) grants.push_back(ReadGrant(result)); + return grants; +} + +std::vector SQLiteDatabase::GetRecentPermissionGrants(bool activeOnly, int64_t now, uint32_t limit) { + std::vector 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); + for (; !result.eof(); result.nextRow()) grants.push_back(ReadGrant(result)); + return grants; +} + +bool SQLiteDatabase::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, static_cast(id)) > 0; +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index c7816ab68..a94db8c5b 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -214,6 +214,12 @@ class TestSQLDatabase : public GameDatabase { std::vector GetAccountNotes(uint32_t accountId) override { return {}; } std::optional GetAccountNote(uint64_t id) override { return {}; } void DeleteAccountNote(uint64_t id) override {} + uint64_t InsertPermissionGrant(const Grant& grant) override { return 0; } + std::optional GetPermissionGrant(uint64_t id) override { return {}; } + std::vector GetPermissionGrants(const std::string& targetType, int64_t targetId) override { return {}; } + std::vector GetActivePermissionGrants(uint32_t accountId, int64_t characterId, int64_t now) override { return {}; } + std::vector GetRecentPermissionGrants(bool activeOnly, int64_t now, uint32_t limit) override { return {}; } + bool RevokePermissionGrant(uint64_t id, const std::string& revokedBy, int64_t time) override { return false; } uint64_t InsertApiKey(const ApiKey& key) override { return 0; } std::optional GetApiKey(uint64_t id) override { return {}; } std::optional GetApiKeyByHash(const std::string& keyHash) override { return {}; } diff --git a/migrations/dlu/mysql/96_permission_grants.sql b/migrations/dlu/mysql/96_permission_grants.sql new file mode 100644 index 000000000..757c0f6fe --- /dev/null +++ b/migrations/dlu/mysql/96_permission_grants.sql @@ -0,0 +1,22 @@ +/* Permissions and slash commands granted to (deny = 0) or taken from (deny = 1) one account or character, on top of + what its GM level allows. target_type: 'account' (target_id = accounts.id) or 'character' (target_id = charinfo.id). + kind: 'permission' (name = a dashboard permission key), 'command' (name = a slash command's settings name), + 'permission_group' (name = a permission category) or 'command_group' (name = a GM level: every command up to it). + expires_at: 0 for never. Rows are never deleted: removing one sets revoked_at and revoked_by, so the table is also + the history of grant changes. granted_by_id is the granting account. */ +CREATE TABLE IF NOT EXISTS permission_grants ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + target_type VARCHAR(16) NOT NULL, + target_id BIGINT NOT NULL, + kind VARCHAR(24) NOT NULL, + name VARCHAR(64) NOT NULL, + deny TINYINT NOT NULL DEFAULT 0, + expires_at BIGINT NOT NULL DEFAULT 0, + note VARCHAR(255) NOT NULL DEFAULT '', + granted_at BIGINT NOT NULL, + granted_by_id INT UNSIGNED NOT NULL DEFAULT 0, + granted_by VARCHAR(64) NOT NULL DEFAULT '', + revoked_at BIGINT NOT NULL DEFAULT 0, + revoked_by VARCHAR(64) NOT NULL DEFAULT '', + INDEX permission_grants_target (target_type, target_id) +); diff --git a/migrations/dlu/sqlite/79_permission_grants.sql b/migrations/dlu/sqlite/79_permission_grants.sql new file mode 100644 index 000000000..9541f6530 --- /dev/null +++ b/migrations/dlu/sqlite/79_permission_grants.sql @@ -0,0 +1,17 @@ +/* Permissions and slash commands granted to or taken from one account or character. See the MySQL migration. */ +CREATE TABLE IF NOT EXISTS permission_grants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + target_type TEXT NOT NULL, + target_id BIGINT NOT NULL, + kind TEXT NOT NULL, + name TEXT NOT NULL, + deny INTEGER NOT NULL DEFAULT 0, + expires_at BIGINT NOT NULL DEFAULT 0, + note TEXT NOT NULL DEFAULT '', + granted_at BIGINT NOT NULL, + granted_by_id INTEGER NOT NULL DEFAULT 0, + granted_by TEXT NOT NULL DEFAULT '', + revoked_at BIGINT NOT NULL DEFAULT 0, + revoked_by TEXT NOT NULL DEFAULT '' +); +CREATE INDEX IF NOT EXISTS permission_grants_target ON permission_grants (target_type, target_id); diff --git a/tests/dDatabaseTests/DatabaseParityTests.cpp b/tests/dDatabaseTests/DatabaseParityTests.cpp index 754764f8c..55080d10e 100644 --- a/tests/dDatabaseTests/DatabaseParityTests.cpp +++ b/tests/dDatabaseTests/DatabaseParityTests.cpp @@ -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();