You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 07:26:18 +00:00
Merge pull request #203 from simoleo89/fix/housekeeping-core-peer-rank
fix(housekeeping): harden privileged staff actions
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
package com.eu.habbo.habbohotel.modtool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingAuditLogContractTest {
|
||||
private static String auditLogSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writerUsesActionLogSchemaReadByHousekeepingClient() throws Exception {
|
||||
String source = auditLogSource();
|
||||
|
||||
assertTrue(source.contains("actor_id"), "housekeeping_log writer must persist actor_id");
|
||||
assertTrue(source.contains("actor_name"), "housekeeping_log writer must persist actor_name");
|
||||
assertTrue(source.contains("target_type"), "housekeeping_log writer must persist target_type");
|
||||
assertTrue(source.contains("target_id"), "housekeeping_log writer must persist target_id");
|
||||
assertTrue(source.contains("target_label"), "housekeeping_log writer must persist target_label");
|
||||
assertTrue(source.contains("success"), "housekeeping_log writer must persist success");
|
||||
assertFalse(source.contains("operator_id"), "housekeeping_log writer must not use the obsolete operator_id schema");
|
||||
assertFalse(source.contains("target_user_id"), "housekeeping_log writer must not use the obsolete target_user_id schema");
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingAuditCoverageContractTest {
|
||||
private static final List<String> SENSITIVE_HANDLERS = List.of(
|
||||
"HousekeepingBanUserEvent.java",
|
||||
"HousekeepingMuteUserEvent.java",
|
||||
"HousekeepingGiveCreditsEvent.java",
|
||||
"HousekeepingGiveCurrencyEvent.java",
|
||||
"HousekeepingResetUserPasswordEvent.java",
|
||||
"HousekeepingSetUserRankEvent.java",
|
||||
"HousekeepingSetHcSubscriptionEvent.java",
|
||||
"HousekeepingTradeLockUserEvent.java",
|
||||
"HousekeepingGrantItemEvent.java",
|
||||
"HousekeepingTransferRoomOwnershipEvent.java",
|
||||
"HousekeepingSendHotelAlertEvent.java",
|
||||
"HousekeepingDeleteRoomEvent.java",
|
||||
"HousekeepingForceDisconnectUserEvent.java",
|
||||
"HousekeepingKickAllFromRoomEvent.java",
|
||||
"HousekeepingKickUserEvent.java",
|
||||
"HousekeepingMuteRoomEvent.java",
|
||||
"HousekeepingRoomStateEvent.java",
|
||||
"HousekeepingUnbanUserEvent.java"
|
||||
);
|
||||
|
||||
@Test
|
||||
void sensitiveHousekeepingActionsWriteAuditEntries() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : SENSITIVE_HANDLERS) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
assertTrue(source.contains("HousekeepingAuditLog.log"),
|
||||
handler + " must append a housekeeping audit log entry after successful privileged actions");
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingGrantMutationContractTest {
|
||||
private static final Path CREDITS_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java");
|
||||
private static final Path CURRENCY_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java");
|
||||
private static final Path GRANT_ITEM_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java");
|
||||
|
||||
@Test
|
||||
void housekeepingGrantsRejectNegativeOrOversizedAmountsServerSide() throws IOException {
|
||||
String credits = Files.readString(CREDITS_SOURCE);
|
||||
String currency = Files.readString(CURRENCY_SOURCE);
|
||||
|
||||
assertTrue(credits.contains("HousekeepingMutationGuard.isPositiveGrantAmount(amount)"),
|
||||
"credit grants must only accept positive bounded amounts");
|
||||
assertTrue(currency.contains("HousekeepingMutationGuard.isPositiveGrantAmount(amount)"),
|
||||
"currency grants must only accept positive bounded amounts");
|
||||
}
|
||||
|
||||
@Test
|
||||
void housekeepingCurrencyGrantsRejectInvalidTypesAndMissingUsers() throws IOException {
|
||||
String currency = Files.readString(CURRENCY_SOURCE);
|
||||
|
||||
assertTrue(currency.contains("HousekeepingMutationGuard.isCurrencyType(currencyType)"),
|
||||
"currency grants must reject negative currency types");
|
||||
assertTrue(currency.contains("HousekeepingMutationGuard.userExists(userId)"),
|
||||
"offline currency grants must not create orphan users_currency rows");
|
||||
}
|
||||
|
||||
@Test
|
||||
void housekeepingItemGrantsRequireRealUsersAndItemsBeforeInsert() throws IOException {
|
||||
String grantItem = Files.readString(GRANT_ITEM_SOURCE);
|
||||
|
||||
int userCheck = grantItem.indexOf("HousekeepingMutationGuard.userExists(userId)");
|
||||
int itemCheck = grantItem.indexOf("HousekeepingMutationGuard.itemExists(itemId)");
|
||||
int insert = grantItem.indexOf("INSERT INTO items");
|
||||
|
||||
assertTrue(userCheck >= 0, "item grants must check the target user exists");
|
||||
assertTrue(itemCheck >= 0, "item grants must check the item base exists");
|
||||
assertTrue(userCheck < insert, "target user must be validated before item insert");
|
||||
assertTrue(itemCheck < insert, "item base must be validated before item insert");
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingInputGuardContractTest {
|
||||
@Test
|
||||
void stringDrivenHousekeepingHandlersUseSharedLimits() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : List.of(
|
||||
"HousekeepingBanUserEvent.java",
|
||||
"HousekeepingForceDisconnectUserEvent.java",
|
||||
"HousekeepingKickUserEvent.java",
|
||||
"HousekeepingMuteUserEvent.java",
|
||||
"HousekeepingTradeLockUserEvent.java",
|
||||
"HousekeepingSendHotelAlertEvent.java",
|
||||
"HousekeepingSearchRoomsEvent.java",
|
||||
"HousekeepingFindUserByNameEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("HousekeepingInputGuard.normalize"),
|
||||
handler + " must normalize client-provided strings before use");
|
||||
assertTrue(source.contains("HousekeepingInputGuard.isWithinLimit"),
|
||||
handler + " must bound client-provided strings before expensive work or broadcast");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditedFreeTextIsSanitizedBeforePersistence() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : List.of(
|
||||
"HousekeepingBanUserEvent.java",
|
||||
"HousekeepingForceDisconnectUserEvent.java",
|
||||
"HousekeepingKickUserEvent.java",
|
||||
"HousekeepingMuteUserEvent.java",
|
||||
"HousekeepingTradeLockUserEvent.java",
|
||||
"HousekeepingSendHotelAlertEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("HousekeepingInputGuard.auditValue"),
|
||||
handler + " must collapse control whitespace before writing free text to audit detail");
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingInputGuardTest {
|
||||
@Test
|
||||
void normalizesNullableText() {
|
||||
assertEquals("", HousekeepingInputGuard.normalize(null));
|
||||
assertEquals("hello", HousekeepingInputGuard.normalize(" hello "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void enforcesInclusiveLengthLimits() {
|
||||
assertTrue(HousekeepingInputGuard.isWithinLimit("abc", 3));
|
||||
assertFalse(HousekeepingInputGuard.isWithinLimit("abcd", 3));
|
||||
assertFalse(HousekeepingInputGuard.isWithinLimit(null, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditValuesCollapseControlWhitespaceAndCapLength() {
|
||||
String value = HousekeepingInputGuard.auditValue(" one\r\ntwo\tthree ");
|
||||
|
||||
assertEquals("one two three", value);
|
||||
|
||||
String oversized = "x".repeat(HousekeepingInputGuard.MAX_REASON_LENGTH + 1);
|
||||
assertEquals(HousekeepingInputGuard.MAX_REASON_LENGTH, HousekeepingInputGuard.auditValue(oversized).length());
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingMutationGuardTest {
|
||||
@Test
|
||||
void positiveGrantAmountsMustBeStrictlyPositiveAndBounded() {
|
||||
assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(-1));
|
||||
assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(0));
|
||||
assertTrue(HousekeepingMutationGuard.isPositiveGrantAmount(1));
|
||||
assertTrue(HousekeepingMutationGuard.isPositiveGrantAmount(HousekeepingMutationGuard.MAX_GRANT));
|
||||
assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(HousekeepingMutationGuard.MAX_GRANT + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void currencyTypesCannotBeNegative() {
|
||||
assertFalse(HousekeepingMutationGuard.isCurrencyType(-1));
|
||||
assertTrue(HousekeepingMutationGuard.isCurrencyType(0));
|
||||
assertTrue(HousekeepingMutationGuard.isCurrencyType(101));
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingRoomGuardContractTest {
|
||||
@Test
|
||||
void destructiveRoomActionsRespectOwnerRankCeiling() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : List.of(
|
||||
"HousekeepingDeleteRoomEvent.java",
|
||||
"HousekeepingKickAllFromRoomEvent.java",
|
||||
"HousekeepingMuteRoomEvent.java",
|
||||
"HousekeepingRoomStateEvent.java",
|
||||
"HousekeepingTransferRoomOwnershipEvent.java"
|
||||
)) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
|
||||
assertTrue(source.contains("HousekeepingRoomGuard.canManageRoom(this.client.getHabbo(), room)"),
|
||||
handler + " must reject room mutations when the room owner is peer-or-higher ranked");
|
||||
assertTrue(source.contains("housekeeping.error.rank_too_high"),
|
||||
handler + " must surface a rank-ceiling error for protected room owners");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomGuardDelegatesToTargetRankGuard() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomGuard.java"));
|
||||
|
||||
assertTrue(source.contains("HousekeepingTargetRankGuard.canTargetUser(operator, room.getOwnerId())"),
|
||||
"room-owner checks must use the same core-rank peer override as user moderation");
|
||||
}
|
||||
|
||||
@Test
|
||||
void roomMuteRejectsNegativeDurations() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteRoomEvent.java"));
|
||||
|
||||
assertTrue(source.contains("minutes < 0"),
|
||||
"room mute should reject negative duration values instead of treating them as unmute");
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingSanctionDurationContractTest {
|
||||
private static final Path BAN_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java");
|
||||
private static final Path MUTE_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java");
|
||||
private static final Path TRADE_LOCK_SOURCE = Path.of(
|
||||
"src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java");
|
||||
|
||||
@Test
|
||||
void sanctionsUseSharedOverflowSafeDurationHelpers() throws IOException {
|
||||
String ban = Files.readString(BAN_SOURCE);
|
||||
String mute = Files.readString(MUTE_SOURCE);
|
||||
String tradeLock = Files.readString(TRADE_LOCK_SOURCE);
|
||||
|
||||
assertTrue(ban.contains("HousekeepingSanctionDuration.secondsFromHours(hours)"));
|
||||
assertTrue(mute.contains("HousekeepingSanctionDuration.secondsFromMinutes(minutes)"));
|
||||
assertTrue(tradeLock.contains("HousekeepingSanctionDuration.secondsFromHours(hours)"));
|
||||
assertTrue(tradeLock.contains("HousekeepingSanctionDuration.unixUntil("));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sanctionsDoNotUseOverflowProneIntDurationConstants() throws IOException {
|
||||
String ban = Files.readString(BAN_SOURCE);
|
||||
String tradeLock = Files.readString(TRADE_LOCK_SOURCE);
|
||||
|
||||
assertFalse(ban.contains("100 * 365 * 24 * 3600"));
|
||||
assertFalse(tradeLock.contains("100 * 365 * 24 * 3600"));
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class HousekeepingSanctionDurationTest {
|
||||
@Test
|
||||
void convertsHoursAndMinutesWithoutIntegerOverflow() {
|
||||
assertEquals(3600, HousekeepingSanctionDuration.secondsFromHours(1));
|
||||
assertEquals(60, HousekeepingSanctionDuration.secondsFromMinutes(1));
|
||||
assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.secondsFromHours(Integer.MAX_VALUE));
|
||||
assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.secondsFromMinutes(Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void capsUnixTimestampInsteadOfWrapping() {
|
||||
assertEquals(1_000_060, HousekeepingSanctionDuration.unixUntil(1_000_000, 60));
|
||||
assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.unixUntil(Integer.MAX_VALUE - 10, 60));
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HousekeepingTargetRankGuardContractTest {
|
||||
private static final List<String> RANK_GUARDED_HANDLERS = List.of(
|
||||
"HousekeepingBanUserEvent.java",
|
||||
"HousekeepingForceDisconnectUserEvent.java",
|
||||
"HousekeepingGiveCreditsEvent.java",
|
||||
"HousekeepingGiveCurrencyEvent.java",
|
||||
"HousekeepingGrantItemEvent.java",
|
||||
"HousekeepingKickUserEvent.java",
|
||||
"HousekeepingMuteUserEvent.java",
|
||||
"HousekeepingResetUserPasswordEvent.java",
|
||||
"HousekeepingSetHcSubscriptionEvent.java",
|
||||
"HousekeepingTradeLockUserEvent.java",
|
||||
"HousekeepingUnbanUserEvent.java"
|
||||
);
|
||||
|
||||
@Test
|
||||
void privilegedUserActionsRejectPeerRanksUnlessOperatorIsCoreRank() throws Exception {
|
||||
String guard = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuard.java"));
|
||||
|
||||
assertTrue(guard.contains("static boolean canTargetRank(Habbo operator, int targetRankId)"),
|
||||
"rank comparison should be reusable for online and offline housekeeping targets");
|
||||
assertTrue(guard.contains("static boolean canAssignRank(Habbo operator, int rankId)"),
|
||||
"rank assignment should use the same peer/core ceiling as target moderation");
|
||||
assertTrue(guard.contains("targetRankId < operatorRankId"),
|
||||
"non-core housekeeping operators must only target lower-ranked users");
|
||||
assertTrue(guard.contains("isCoreRank(operatorRankId) && targetRankId <= operatorRankId"),
|
||||
"the highest/core rank should be allowed to target peer ranks");
|
||||
assertTrue(guard.contains("private static boolean isCoreRank(int rankId)"),
|
||||
"core-rank detection should be centralized in the target-rank guard");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sensitiveHousekeepingUserActionsUseRankGuard() throws Exception {
|
||||
Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping");
|
||||
|
||||
for (String handler : RANK_GUARDED_HANDLERS) {
|
||||
String source = Files.readString(base.resolve(handler));
|
||||
assertTrue(source.contains("HousekeepingTargetRankGuard.canTargetUser(this.client.getHabbo(), userId)"),
|
||||
handler + " must reject equal or higher-ranked targets before applying privileged user actions");
|
||||
assertTrue(source.contains("housekeeping.error.rank_too_high"),
|
||||
handler + " must return a rank-ceiling error when the target cannot be managed");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void housekeepingRankChangesUseCentralRankCeilings() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java"));
|
||||
|
||||
assertTrue(source.contains("HousekeepingTargetRankGuard.canAssignRank(this.client.getHabbo(), rank.getId())"),
|
||||
"housekeeping rank assignment must not grant peer-or-higher ranks to non-core operators");
|
||||
assertTrue(source.contains("HousekeepingTargetRankGuard.canTargetRank(this.client.getHabbo(), targetRankId)"),
|
||||
"housekeeping rank assignment must not modify peer-or-higher ranked targets for non-core operators");
|
||||
assertTrue(source.contains("housekeeping.error.user_not_found"),
|
||||
"rank changes must reject missing offline users instead of reporting success for a zero-row update");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user