From bde5a43ef6bc9e660aad951ce38bebbc9e87f229 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Tue, 31 Mar 2026 21:13:22 +0200 Subject: [PATCH 1/3] Properly organize role-specific config settings, separate timed level limit for trusted users --- .../Configuration/GameServerConfig.cs | 107 +++++++++++------- Refresh.Core/Configuration/RolePermissions.cs | 17 +++ Refresh.Core/Extensions/GameUserExtensions.cs | 15 ++- .../Endpoints/InstanceApiEndpoints.cs | 4 +- .../Endpoints/Levels/PublishEndpoints.cs | 12 +- .../Endpoints/ResourceEndpoints.cs | 4 +- .../GameServer/TestGameServerConfig.cs | 27 +++++ .../Tests/Assets/AssetUploadTests.cs | 22 ++-- .../Tests/Configs/GameServerConfigTests.cs | 58 ++++++++++ .../Tests/Levels/PublishEndpointsTests.cs | 14 ++- .../Tests/Planets/ModdedPlanetsTests.cs | 4 +- .../Tests/Playlists/PlaylistUploadTests.cs | 8 +- 12 files changed, 215 insertions(+), 77 deletions(-) create mode 100644 Refresh.Core/Configuration/RolePermissions.cs create mode 100644 RefreshTests.GameServer/GameServer/TestGameServerConfig.cs create mode 100644 RefreshTests.GameServer/Tests/Configs/GameServerConfigTests.cs diff --git a/Refresh.Core/Configuration/GameServerConfig.cs b/Refresh.Core/Configuration/GameServerConfig.cs index a3a7f5ae7..411b7474b 100644 --- a/Refresh.Core/Configuration/GameServerConfig.cs +++ b/Refresh.Core/Configuration/GameServerConfig.cs @@ -1,8 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Bunkum.Core.Configuration; using Microsoft.CSharp.RuntimeBinder; -using Refresh.Database.Models.Assets; -using Refresh.Database.Models.Users; namespace Refresh.Core.Configuration; @@ -10,52 +8,95 @@ namespace Refresh.Core.Configuration; [SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")] public class GameServerConfig : Config { - public override int CurrentConfigVersion => 26; + public override int CurrentConfigVersion => 27; public override int Version { get; set; } = 0; protected override void Migrate(int oldVer, dynamic oldConfig) { - if (oldVer < 18) + // In version 27, various (mostly already role-specific) perms, like blocked assets and read-only mode, were moved to dedicated child objects, + // to better split them between certain roles. + if (oldVer < 27) { - // Asset safety level was added in config version 2, so dont try to migrate if we are coming from an older version than that - if (oldVer >= 2) + this.NormalUserPermissions = new(); + this.TrustedUserPermissions = new(); + + if (oldVer >= 18) { - int oldSafetyLevel = (int)oldConfig.MaximumAssetSafetyLevel; - this.BlockedAssetFlags = new ConfigAssetFlags - { - Dangerous = oldSafetyLevel < 3, - Modded = oldSafetyLevel < 2, - Media = oldSafetyLevel < 1, - }; - } + this.NormalUserPermissions.BlockedAssetFlags.Dangerous = (bool)oldConfig.BlockedAssetFlags.Dangerous; + this.NormalUserPermissions.BlockedAssetFlags.Media = (bool)oldConfig.BlockedAssetFlags.Media; + this.NormalUserPermissions.BlockedAssetFlags.Modded = (bool)oldConfig.BlockedAssetFlags.Modded; - // Asset safety level for trusted users was added in config version 12, so dont try to migrate if we are coming from a version older than that - if (oldVer >= 12) + this.TrustedUserPermissions.BlockedAssetFlags.Dangerous = (bool)oldConfig.BlockedAssetFlagsForTrustedUsers.Dangerous; + this.TrustedUserPermissions.BlockedAssetFlags.Media = (bool)oldConfig.BlockedAssetFlagsForTrustedUsers.Media; + this.TrustedUserPermissions.BlockedAssetFlags.Modded = (bool)oldConfig.BlockedAssetFlagsForTrustedUsers.Modded; + } + else { - // There was no version bump for trusted users being added, so we just have to catch this error :/ - try + // Asset safety level was added in config version 2, so dont try to migrate if we are coming from an older version than that + if (oldVer >= 2) { - int oldTrustedSafetyLevel = (int)oldConfig.MaximumAssetSafetyLevelForTrustedUsers; - this.BlockedAssetFlagsForTrustedUsers = new ConfigAssetFlags + int oldSafetyLevel = (int)oldConfig.MaximumAssetSafetyLevel; + this.NormalUserPermissions.BlockedAssetFlags = new ConfigAssetFlags { - Dangerous = oldTrustedSafetyLevel < 3, - Modded = oldTrustedSafetyLevel < 2, - Media = oldTrustedSafetyLevel < 1, + Dangerous = oldSafetyLevel < 3, + Modded = oldSafetyLevel < 2, + Media = oldSafetyLevel < 1, }; } - catch (RuntimeBinderException) + + // Asset safety level for trusted users was added in config version 12, so dont try to migrate if we are coming from a version older than that + if (oldVer >= 12) { - this.BlockedAssetFlagsForTrustedUsers = this.BlockedAssetFlags; + // There was no version bump for trusted users being added, so we just have to catch this error :/ + try + { + int oldTrustedSafetyLevel = (int)oldConfig.MaximumAssetSafetyLevelForTrustedUsers; + this.TrustedUserPermissions.BlockedAssetFlags = new ConfigAssetFlags + { + Dangerous = oldTrustedSafetyLevel < 3, + Modded = oldTrustedSafetyLevel < 2, + Media = oldTrustedSafetyLevel < 1, + }; + } + catch (RuntimeBinderException) + { + this.TrustedUserPermissions.BlockedAssetFlags = this.NormalUserPermissions.BlockedAssetFlags; + } } } + + // Timed level upload limits were added in version 19. + if (oldVer >= 19) + { + this.NormalUserPermissions.TimedLevelUploadLimits.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled; + this.NormalUserPermissions.TimedLevelUploadLimits.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours; + this.NormalUserPermissions.TimedLevelUploadLimits.LevelQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota; + + this.TrustedUserPermissions.TimedLevelUploadLimits.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled; + this.TrustedUserPermissions.TimedLevelUploadLimits.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours; + this.TrustedUserPermissions.TimedLevelUploadLimits.LevelQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota; + } + + // Read-only mode was added for both normal and trusted users in version 20. + if (oldVer >= 20) + { + this.NormalUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadOnlyMode; + this.TrustedUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadonlyModeForTrustedUsers; + } } } public string LicenseText { get; set; } = "Welcome to Refresh!"; - public ConfigAssetFlags BlockedAssetFlags { get; set; } = new(AssetFlags.Dangerous | AssetFlags.Modded); - /// - public ConfigAssetFlags BlockedAssetFlagsForTrustedUsers { get; set; } = new(AssetFlags.Dangerous | AssetFlags.Modded); + /// + /// Role-specific permissions for normal users and below + /// + public RolePermissions NormalUserPermissions = new(); + /// + /// Role-specific permissions for trusted users and above + /// + public RolePermissions TrustedUserPermissions = new(); + public bool AllowUsersToUseIpAuthentication { get; set; } = false; public bool PermitPsnLogin { get; set; } = true; public bool PermitRpcnLogin { get; set; } = true; @@ -97,20 +138,10 @@ protected override void Migrate(int oldVer, dynamic oldConfig) /// public string GameConfigStorageUrl { get; set; } = "https://refresh.example.com/lbp"; public bool AllowInvalidTextureGuids { get; set; } = false; - public bool ReadOnlyMode { get; set; } = false; - /// - public bool ReadonlyModeForTrustedUsers { get; set; } = false; /// /// The amount of data the user is allowed to upload before all resource uploads get blocked, defaults to 100mb. /// public int UserFilesizeQuota { get; set; } = 100 * 1_048_576; - - public TimedLevelUploadLimitProperties TimedLevelUploadLimits { get; set; } = new() - { - Enabled = false, - TimeSpanHours = 24, - LevelQuota = 10, - }; /// /// Whether to print the room state whenever a `FindBestRoom` match returns no results diff --git a/Refresh.Core/Configuration/RolePermissions.cs b/Refresh.Core/Configuration/RolePermissions.cs new file mode 100644 index 000000000..0d03bec60 --- /dev/null +++ b/Refresh.Core/Configuration/RolePermissions.cs @@ -0,0 +1,17 @@ +using Refresh.Database.Models.Assets; + +namespace Refresh.Core.Configuration; + +public class RolePermissions +{ + public RolePermissions() {} + + public bool ReadOnlyMode { get; set; } = false; + public ConfigAssetFlags BlockedAssetFlags { get; set; } = new(AssetFlags.Dangerous | AssetFlags.Modded); + public TimedLevelUploadLimitProperties TimedLevelUploadLimits = new() + { + Enabled = false, + TimeSpanHours = 24, + LevelQuota = 10, + }; +} \ No newline at end of file diff --git a/Refresh.Core/Extensions/GameUserExtensions.cs b/Refresh.Core/Extensions/GameUserExtensions.cs index 8350dfe7e..cbe7df90e 100644 --- a/Refresh.Core/Extensions/GameUserExtensions.cs +++ b/Refresh.Core/Extensions/GameUserExtensions.cs @@ -7,12 +7,7 @@ public static class GameUserExtensions { public static bool IsWriteBlocked(this GameUser user, GameServerConfig config) { - if (config.ReadOnlyMode && user.Role != GameUserRole.Admin) - { - return user.Role < GameUserRole.Trusted || config.ReadonlyModeForTrustedUsers; - } - - return false; + return GetRolePermissionsForUser(user, config).ReadOnlyMode; } public static bool MayModifyUser(this GameUser user, GameUser targetUser) @@ -27,4 +22,12 @@ public static bool MayModifyUser(this GameUser user, GameUser targetUser) return true; } + + public static RolePermissions GetRolePermissionsForUser(this GameUser user, GameServerConfig config) + { + if (user.Role >= GameUserRole.Trusted) + return config.TrustedUserPermissions; + + return config.NormalUserPermissions; + } } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/InstanceApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/InstanceApiEndpoints.cs index abe7f55f0..1639719e4 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/InstanceApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/InstanceApiEndpoints.cs @@ -62,8 +62,8 @@ public ApiResponse GetInstanceInformation(RequestContext co SoftwareSourceUrl = "https://github.com/LittleBigRefresh/Refresh", SoftwareLicenseName = "AGPL-3.0", SoftwareLicenseUrl = "https://www.gnu.org/licenses/agpl-3.0.txt", - BlockedAssetFlags = gameConfig.BlockedAssetFlags, - BlockedAssetFlagsForTrustedUsers = gameConfig.BlockedAssetFlagsForTrustedUsers, + BlockedAssetFlags = gameConfig.NormalUserPermissions.BlockedAssetFlags, + BlockedAssetFlagsForTrustedUsers = gameConfig.TrustedUserPermissions.BlockedAssetFlags, Announcements = ApiGameAnnouncementResponse.FromOldList(database.GetAnnouncements(), dataContext), MaintenanceModeEnabled = gameConfig.MaintenanceMode, RichPresenceConfiguration = ApiRichPresenceConfigurationResponse.FromOld(RichPresenceConfiguration.Create( diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs index 7118e24b0..2d12ffa50 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs @@ -119,7 +119,8 @@ public Response StartPublish(RequestContext context, GameLevelRequest body, DataContext dataContext, GameServerConfig config, - IDateTimeProvider dateTimeProvider) + IDateTimeProvider dateTimeProvider, + GameUser user) { if (dataContext.User!.IsWriteBlocked(config)) { @@ -127,7 +128,7 @@ public Response StartPublish(RequestContext context, return Unauthorized; } - if (IsTimedLevelLimitReached(dataContext, dataContext.User!, body.Title, config.TimedLevelUploadLimits, dateTimeProvider.Now)) + if (IsTimedLevelLimitReached(dataContext, dataContext.User!, body.Title, user.GetRolePermissionsForUser(config).TimedLevelUploadLimits, dateTimeProvider.Now)) return Unauthorized; //If verifying the request fails, return BadRequest @@ -182,7 +183,8 @@ public Response PublishLevel(RequestContext context, if (user.IsWriteBlocked(config)) return Unauthorized; - if (IsTimedLevelLimitReached(dataContext, user, body.Title, config.TimedLevelUploadLimits, dateTimeProvider.Now)) + TimedLevelUploadLimitProperties timedLevelLimit = user.GetRolePermissionsForUser(config).TimedLevelUploadLimits; + if (IsTimedLevelLimitReached(dataContext, user, body.Title, timedLevelLimit, dateTimeProvider.Now)) return Unauthorized; //If verifying the request fails, return BadRequest @@ -256,9 +258,9 @@ public Response PublishLevel(RequestContext context, // Only increment if the level can be uploaded (right after the previous checks + adding the level), // don't want to increment for failed uploads - if (config.TimedLevelUploadLimits.Enabled) + if (timedLevelLimit.Enabled) { - dataContext.Database.IncrementTimedLevelLimit(user, config.TimedLevelUploadLimits.TimeSpanHours); + dataContext.Database.IncrementTimedLevelLimit(user, timedLevelLimit.TimeSpanHours); } // Update the modded status of the level diff --git a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs index 7786ca037..983386912 100644 --- a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs @@ -66,9 +66,7 @@ public Response UploadAsset(RequestContext context, string hash, string type, by gameAsset.UploadDate = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(gameAsset.UploadDate.ToUnixTimeSeconds(), timeProvider.EarliestDate, timeProvider.TimestampSeconds)); - AssetFlags blockedAssetFlags = config.BlockedAssetFlags.ToAssetFlags(); - if (user.Role >= GameUserRole.Trusted) - blockedAssetFlags = config.BlockedAssetFlagsForTrustedUsers.ToAssetFlags(); + AssetFlags blockedAssetFlags = user.GetRolePermissionsForUser(config).BlockedAssetFlags.ToAssetFlags(); // Don't block any assets uploaded from PSP, else block any unwanted assets, // For example, if the "blocked asset flags" has the "Media" bit set, and so does the asset, diff --git a/RefreshTests.GameServer/GameServer/TestGameServerConfig.cs b/RefreshTests.GameServer/GameServer/TestGameServerConfig.cs new file mode 100644 index 000000000..a5225a559 --- /dev/null +++ b/RefreshTests.GameServer/GameServer/TestGameServerConfig.cs @@ -0,0 +1,27 @@ +using Refresh.Core.Configuration; +using Refresh.Database.Models.Assets; + +namespace RefreshTests.GameServer.GameServer; + +public class TestGameServerConfig : GameServerConfig +{ + public void TestMigration() + { + this.Migrate(this.Version, this); + } + + // Various attributes to migrate from + public ConfigAssetFlags BlockedAssetFlags { get; set; } = new(AssetFlags.Dangerous | AssetFlags.Modded); + public ConfigAssetFlags BlockedAssetFlagsForTrustedUsers { get; set; } = new(AssetFlags.Dangerous | AssetFlags.Modded); + public bool ReadOnlyMode { get; set; } = false; + public bool ReadonlyModeForTrustedUsers { get; set; } = false; + public TimedLevelUploadLimitProperties TimedLevelUploadLimits { get; set; } = new() + { + Enabled = false, + TimeSpanHours = 24, + LevelQuota = 10, + }; + + public int MaximumAssetSafetyLevel { get; set; } = 0; + public int MaximumAssetSafetyLevelForTrustedUsers { get; set; } = 0; +} \ No newline at end of file diff --git a/RefreshTests.GameServer/Tests/Assets/AssetUploadTests.cs b/RefreshTests.GameServer/Tests/Assets/AssetUploadTests.cs index b6c7c661e..ed97b72af 100644 --- a/RefreshTests.GameServer/Tests/Assets/AssetUploadTests.cs +++ b/RefreshTests.GameServer/Tests/Assets/AssetUploadTests.cs @@ -71,7 +71,7 @@ public void CannotUploadAssetWhenBlocked(bool psp) { using TestContext context = this.GetServer(); - context.Server.Value.GameServerConfig.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; context.Server.Value.Server.AddService(); GameUser user = context.CreateUser(); @@ -95,8 +95,8 @@ public void TrustedCanUploadAssetWhenBlocked(bool psp) { using TestContext context = this.GetServer(); context.Server.Value.Server.AddService(); - context.Server.Value.GameServerConfig.ReadOnlyMode = true; - context.Server.Value.GameServerConfig.ReadonlyModeForTrustedUsers = false; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = false; GameUser user = context.CreateUser(); context.Database.SetUserRole(user, GameUserRole.Trusted); @@ -121,8 +121,8 @@ public void AdminCanUploadAssetWhenBlocked(bool psp) { using TestContext context = this.GetServer(); context.Server.Value.Server.AddService(); - context.Server.Value.GameServerConfig.ReadOnlyMode = true; - context.Server.Value.GameServerConfig.ReadonlyModeForTrustedUsers = true; + context.Server.Value.GameServerConfig.NormalUserPermissions.ReadOnlyMode = true; + context.Server.Value.GameServerConfig.TrustedUserPermissions.ReadOnlyMode = true; GameUser user = context.CreateUser(); context.Database.SetUserRole(user, GameUserRole.Admin); @@ -146,13 +146,13 @@ public void TrustedCanUploadAssetWithSafetyLevel() { using TestContext context = this.GetServer(); context.Server.Value.Server.AddService(); - context.Server.Value.GameServerConfig.BlockedAssetFlags = new ConfigAssetFlags + context.Server.Value.GameServerConfig.NormalUserPermissions.BlockedAssetFlags = new ConfigAssetFlags { Modded = true, Media = true, Dangerous = true, }; - context.Server.Value.GameServerConfig.BlockedAssetFlagsForTrustedUsers = new ConfigAssetFlags + context.Server.Value.GameServerConfig.TrustedUserPermissions.BlockedAssetFlags = new ConfigAssetFlags { Dangerous = true, Modded = true, @@ -178,13 +178,13 @@ public void NormalUserCantUploadAssetWithSafetyLevel() { using TestContext context = this.GetServer(); context.Server.Value.Server.AddService(); - context.Server.Value.GameServerConfig.BlockedAssetFlags = new ConfigAssetFlags + context.Server.Value.GameServerConfig.NormalUserPermissions.BlockedAssetFlags = new ConfigAssetFlags { Dangerous = true, Modded = true, Media = true, }; - context.Server.Value.GameServerConfig.BlockedAssetFlagsForTrustedUsers = new ConfigAssetFlags + context.Server.Value.GameServerConfig.TrustedUserPermissions.BlockedAssetFlags = new ConfigAssetFlags { Dangerous = true, Modded = true, @@ -209,7 +209,7 @@ public void PspCantUploadMediaAssetWhileBlocked() { using TestContext context = this.GetServer(); context.Server.Value.Server.AddService(); - context.Server.Value.GameServerConfig.BlockedAssetFlags = new ConfigAssetFlags + context.Server.Value.GameServerConfig.NormalUserPermissions.BlockedAssetFlags = new ConfigAssetFlags { Dangerous = true, Modded = true, @@ -236,7 +236,7 @@ public void PspCanUploadNormalAssetWhileBlocked() { using TestContext context = this.GetServer(); context.Server.Value.Server.AddService(); - context.Server.Value.GameServerConfig.BlockedAssetFlags = new ConfigAssetFlags + context.Server.Value.GameServerConfig.NormalUserPermissions.BlockedAssetFlags = new ConfigAssetFlags { Dangerous = true, Modded = true, diff --git a/RefreshTests.GameServer/Tests/Configs/GameServerConfigTests.cs b/RefreshTests.GameServer/Tests/Configs/GameServerConfigTests.cs new file mode 100644 index 000000000..701baa1f3 --- /dev/null +++ b/RefreshTests.GameServer/Tests/Configs/GameServerConfigTests.cs @@ -0,0 +1,58 @@ +using Refresh.Database.Models.Assets; + +namespace RefreshTests.GameServer.Tests.Configs; + +public class GameServerConfigTests : GameServerTest +{ + [Test] + public void MigratesRolePermsFromVersion26() + { + TestGameServerConfig config = new() + { + Version = 26, + ReadOnlyMode = true, + ReadonlyModeForTrustedUsers = false, + TimedLevelUploadLimits = new() + { + Enabled = true, + TimeSpanHours = 67, + LevelQuota = 2, + }, + BlockedAssetFlags = new(AssetFlags.Dangerous | AssetFlags.Media), + BlockedAssetFlagsForTrustedUsers = new(AssetFlags.Modded), + }; + + config.TestMigration(); + + Assert.That(config.NormalUserPermissions.ReadOnlyMode, Is.True); + Assert.That(config.TrustedUserPermissions.ReadOnlyMode, Is.False); + + Assert.That(config.NormalUserPermissions.TimedLevelUploadLimits.Enabled, Is.True); + Assert.That(config.TrustedUserPermissions.TimedLevelUploadLimits.Enabled, Is.True); + + Assert.That(config.NormalUserPermissions.TimedLevelUploadLimits.TimeSpanHours, Is.EqualTo(67)); + Assert.That(config.TrustedUserPermissions.TimedLevelUploadLimits.TimeSpanHours, Is.EqualTo(67)); + + Assert.That(config.NormalUserPermissions.TimedLevelUploadLimits.LevelQuota, Is.EqualTo(2)); + Assert.That(config.TrustedUserPermissions.TimedLevelUploadLimits.LevelQuota, Is.EqualTo(2)); + + Assert.That(config.NormalUserPermissions.BlockedAssetFlags.ToAssetFlags(), Is.EqualTo(AssetFlags.Dangerous | AssetFlags.Media)); + Assert.That(config.TrustedUserPermissions.BlockedAssetFlags.ToAssetFlags(), Is.EqualTo(AssetFlags.Modded)); + } + + [Test] + public void MigratesRolePermsFromVersion17() + { + TestGameServerConfig config = new() + { + Version = 17, + MaximumAssetSafetyLevel = 1, + MaximumAssetSafetyLevelForTrustedUsers = 2, + }; + + config.TestMigration(); + + Assert.That(config.NormalUserPermissions.BlockedAssetFlags.ToAssetFlags(), Is.EqualTo(AssetFlags.Dangerous | AssetFlags.Modded)); + Assert.That(config.TrustedUserPermissions.BlockedAssetFlags.ToAssetFlags(), Is.EqualTo(AssetFlags.Dangerous)); + } +} \ No newline at end of file diff --git a/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs b/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs index f0b2bd8b9..85ea1741a 100644 --- a/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs +++ b/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs @@ -503,12 +503,13 @@ public void CantPublishAfterExceedingTimedLevelLimit(int levelQuota, int uploadA // Prepare config GameServerConfig config = context.Server.Value.GameServerConfig; - config.TimedLevelUploadLimits = new() + TimedLevelUploadLimitProperties timedLevelLimit = new() { Enabled = true, LevelQuota = levelQuota, TimeSpanHours = 1, }; + config.NormalUserPermissions.TimedLevelUploadLimits = timedLevelLimit; using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); @@ -517,7 +518,7 @@ public void CantPublishAfterExceedingTimedLevelLimit(int levelQuota, int uploadA Assert.That(assetUploadMessage.StatusCode, Is.EqualTo(OK)); // Fill up quota - SpamSuccessfulUploads(config.TimedLevelUploadLimits.LevelQuota, client); + SpamSuccessfulUploads(timedLevelLimit.LevelQuota, client); // Try to upload more levels after exceeding quota for (int i = 0; i < uploadAttemptsAfterExceeding; i++) @@ -532,7 +533,7 @@ public void CantPublishAfterExceedingTimedLevelLimit(int levelQuota, int uploadA // Check amount of levels DatabaseList levelsByUser = context.Database.GetLevelsByUser(user, 1000, 0, new(TokenGame.LittleBigPlanet3), user); - Assert.That(levelsByUser.TotalItems, Is.EqualTo(config.TimedLevelUploadLimits.LevelQuota)); + Assert.That(levelsByUser.TotalItems, Is.EqualTo(timedLevelLimit.LevelQuota)); // Ensure there were error notifications sent for each blocked request to both /startPublish and /publish DatabaseList newNotifications = context.Database.GetNotificationsByUser(user, 1000, 0); @@ -549,7 +550,7 @@ public void ResetTimedLevelLimitAfterExpiry(int levelQuota, int uploadAttemptsAf // Prepare config GameServerConfig config = context.Server.Value.GameServerConfig; - config.TimedLevelUploadLimits = new() + TimedLevelUploadLimitProperties timedLevelLimit = new() { Enabled = true, LevelQuota = levelQuota, @@ -557,6 +558,7 @@ public void ResetTimedLevelLimitAfterExpiry(int levelQuota, int uploadAttemptsAf // causing both /startPublish and /publish to always reset the limit after setting it in a previous /publish request, and allowing publish requests TimeSpanHours = 0, }; + config.NormalUserPermissions.TimedLevelUploadLimits = timedLevelLimit; using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user); @@ -565,14 +567,14 @@ public void ResetTimedLevelLimitAfterExpiry(int levelQuota, int uploadAttemptsAf Assert.That(message.StatusCode, Is.EqualTo(OK)); // Fill up quota - SpamSuccessfulUploads(config.TimedLevelUploadLimits.LevelQuota, client); + SpamSuccessfulUploads(timedLevelLimit.LevelQuota, client); // Try to upload more levels after exceeding quota SpamSuccessfulUploads(uploadAttemptsAfterExceeding, client); // Check amount of levels DatabaseList levelsByUser = context.Database.GetLevelsByUser(user, 1000, 0, new(TokenGame.LittleBigPlanet3), user); - Assert.That(levelsByUser.TotalItems, Is.EqualTo(config.TimedLevelUploadLimits.LevelQuota + uploadAttemptsAfterExceeding)); + Assert.That(levelsByUser.TotalItems, Is.EqualTo(timedLevelLimit.LevelQuota + uploadAttemptsAfterExceeding)); // Ensure there were no notifications sent DatabaseList newNotifications = context.Database.GetNotificationsByUser(user, 1000, 0); diff --git a/RefreshTests.GameServer/Tests/Planets/ModdedPlanetsTests.cs b/RefreshTests.GameServer/Tests/Planets/ModdedPlanetsTests.cs index 351fa696c..cd8e95e62 100644 --- a/RefreshTests.GameServer/Tests/Planets/ModdedPlanetsTests.cs +++ b/RefreshTests.GameServer/Tests/Planets/ModdedPlanetsTests.cs @@ -54,7 +54,7 @@ private void UploadHeavilyModdedPlanet(TestContext context, HttpClient client, s { // Prepare config so normal users may upload modded assets GameServerConfig config = context.Server.Value.GameServerConfig; - config.BlockedAssetFlags = new(AssetFlags.Dangerous); + config.NormalUserPermissions.BlockedAssetFlags = new(AssetFlags.Dangerous); // Upload planet dependency and make the planet level depend on the voice recording HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_MESH_HASH}", new ReadOnlyMemoryContent("MSHb"u8.ToArray())).Result; @@ -171,7 +171,7 @@ public void ModdedLbp2PlanetDoesntHideUnmoddedVitaPlanet() // Prepare config so normal users may upload modded assets GameServerConfig config = context.Server.Value.GameServerConfig; - config.BlockedAssetFlags = new(AssetFlags.Dangerous); + config.NormalUserPermissions.BlockedAssetFlags = new(AssetFlags.Dangerous); GameUser publisher = context.CreateUser(); using HttpClient client2LBP2 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, publisher); diff --git a/RefreshTests.GameServer/Tests/Playlists/PlaylistUploadTests.cs b/RefreshTests.GameServer/Tests/Playlists/PlaylistUploadTests.cs index 56987fa75..70d459b70 100644 --- a/RefreshTests.GameServer/Tests/Playlists/PlaylistUploadTests.cs +++ b/RefreshTests.GameServer/Tests/Playlists/PlaylistUploadTests.cs @@ -122,8 +122,8 @@ public void CannotCreateSubPlaylistWhileReadOnlyMode() { using TestContext context = this.GetServer(); GameServerConfig config = context.Server.Value.GameServerConfig; - config.ReadOnlyMode = true; - config.ReadonlyModeForTrustedUsers = true; + config.NormalUserPermissions.ReadOnlyMode = true; + config.TrustedUserPermissions.ReadOnlyMode = true; GameUser user = context.CreateUser(); GamePlaylist root = context.Database.CreatePlaylist(user, new SerializedLbp1Playlist() @@ -155,8 +155,8 @@ public void CanCreateRootPlaylistWhileReadOnlyMode() { using TestContext context = this.GetServer(); GameServerConfig config = context.Server.Value.GameServerConfig; - config.ReadOnlyMode = true; - config.ReadonlyModeForTrustedUsers = true; + config.NormalUserPermissions.ReadOnlyMode = true; + config.TrustedUserPermissions.ReadOnlyMode = true; GameUser user = context.CreateUser(); HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet1, TokenPlatform.PS3, user); From 9fd4d87b37a57f9941040e23ed8230178d20c044 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Tue, 31 Mar 2026 21:32:40 +0200 Subject: [PATCH 2/3] Allow admins to bypass read-only (again) --- Refresh.Core/Extensions/GameUserExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Refresh.Core/Extensions/GameUserExtensions.cs b/Refresh.Core/Extensions/GameUserExtensions.cs index cbe7df90e..e3f9c3683 100644 --- a/Refresh.Core/Extensions/GameUserExtensions.cs +++ b/Refresh.Core/Extensions/GameUserExtensions.cs @@ -7,6 +7,7 @@ public static class GameUserExtensions { public static bool IsWriteBlocked(this GameUser user, GameServerConfig config) { + if (user.Role == GameUserRole.Admin) return false; return GetRolePermissionsForUser(user, config).ReadOnlyMode; } From c05d42a82b8326e7aa7585e69a2453286eaf68cd Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 9 Apr 2026 20:04:08 +0200 Subject: [PATCH 3/3] Make UserFilesizeQuota role-specific --- .../Configuration/GameServerConfig.cs | 20 ++++++++++++++----- Refresh.Core/Configuration/RolePermissions.cs | 5 +++++ .../Endpoints/ResourceApiEndpoints.cs | 5 +++-- .../Endpoints/ResourceEndpoints.cs | 8 +++++--- .../GameServer/TestGameServerConfig.cs | 1 + .../Tests/Assets/AssetUploadTests.cs | 3 ++- .../Tests/Configs/GameServerConfigTests.cs | 4 ++++ 7 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Refresh.Core/Configuration/GameServerConfig.cs b/Refresh.Core/Configuration/GameServerConfig.cs index 411b7474b..fab3d295b 100644 --- a/Refresh.Core/Configuration/GameServerConfig.cs +++ b/Refresh.Core/Configuration/GameServerConfig.cs @@ -14,12 +14,26 @@ public class GameServerConfig : Config protected override void Migrate(int oldVer, dynamic oldConfig) { // In version 27, various (mostly already role-specific) perms, like blocked assets and read-only mode, were moved to dedicated child objects, - // to better split them between certain roles. + // to more cleanly split the perms between certain roles, and to make their enforcement easier. if (oldVer < 27) { this.NormalUserPermissions = new(); this.TrustedUserPermissions = new(); + // filesize quota limit was added during version 11, but the version wasn't bumped, so catch error to be safe + if (oldVer >= 11) + { + try + { + this.NormalUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota; + this.TrustedUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota; + } + catch (RuntimeBinderException) + { + // do nothing + } + } + if (oldVer >= 18) { this.NormalUserPermissions.BlockedAssetFlags.Dangerous = (bool)oldConfig.BlockedAssetFlags.Dangerous; @@ -138,10 +152,6 @@ protected override void Migrate(int oldVer, dynamic oldConfig) /// public string GameConfigStorageUrl { get; set; } = "https://refresh.example.com/lbp"; public bool AllowInvalidTextureGuids { get; set; } = false; - /// - /// The amount of data the user is allowed to upload before all resource uploads get blocked, defaults to 100mb. - /// - public int UserFilesizeQuota { get; set; } = 100 * 1_048_576; /// /// Whether to print the room state whenever a `FindBestRoom` match returns no results diff --git a/Refresh.Core/Configuration/RolePermissions.cs b/Refresh.Core/Configuration/RolePermissions.cs index 0d03bec60..e1cc44e59 100644 --- a/Refresh.Core/Configuration/RolePermissions.cs +++ b/Refresh.Core/Configuration/RolePermissions.cs @@ -14,4 +14,9 @@ public RolePermissions() {} TimeSpanHours = 24, LevelQuota = 10, }; + + /// + /// The amount of data the user is allowed to upload before all resource uploads get blocked, defaults to 100mb. + /// + public int UserFilesizeQuota { get; set; } = 100 * 1_048_576; } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs index 8c4d9da68..9ab5fd328 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs @@ -193,9 +193,10 @@ IntegrationConfig integration return new ApiValidationError($"The asset must be under 2MB. Your file was {body.Length:N0} bytes."); } - if (body.Length + user.FilesizeQuotaUsage > config.UserFilesizeQuota) + RolePermissions rolePerms = user.GetRolePermissionsForUser(config); + if (body.Length + user.FilesizeQuotaUsage > rolePerms.UserFilesizeQuota) { - context.Logger.LogWarning(BunkumCategory.UserContent, "User {0} has hit the filesize quota ({1} bytes), rejecting.", user.Username, config.UserFilesizeQuota); + context.Logger.LogWarning(BunkumCategory.UserContent, "User {0} has hit the filesize quota ({1} bytes), rejecting.", user.Username, rolePerms.UserFilesizeQuota); return new ApiValidationError($"You have exceeded your filesize quota."); } diff --git a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs index 983386912..1f64f050f 100644 --- a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs @@ -48,9 +48,11 @@ public Response UploadAsset(RequestContext context, string hash, string type, by if (dataStore.ExistsInStore(assetPath)) return Conflict; - if (body.Length + user.FilesizeQuotaUsage > config.UserFilesizeQuota) + RolePermissions rolePerms = user.GetRolePermissionsForUser(config); + + if (body.Length + user.FilesizeQuotaUsage > rolePerms.UserFilesizeQuota) { - context.Logger.LogWarning(BunkumCategory.UserContent, "User {0} has hit the filesize quota ({1} bytes), rejecting.", user.Username, config.UserFilesizeQuota); + context.Logger.LogWarning(BunkumCategory.UserContent, "User {0} has hit the filesize quota ({1} bytes), rejecting.", user.Username, rolePerms.UserFilesizeQuota); return RequestEntityTooLarge; } @@ -66,7 +68,7 @@ public Response UploadAsset(RequestContext context, string hash, string type, by gameAsset.UploadDate = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(gameAsset.UploadDate.ToUnixTimeSeconds(), timeProvider.EarliestDate, timeProvider.TimestampSeconds)); - AssetFlags blockedAssetFlags = user.GetRolePermissionsForUser(config).BlockedAssetFlags.ToAssetFlags(); + AssetFlags blockedAssetFlags = rolePerms.BlockedAssetFlags.ToAssetFlags(); // Don't block any assets uploaded from PSP, else block any unwanted assets, // For example, if the "blocked asset flags" has the "Media" bit set, and so does the asset, diff --git a/RefreshTests.GameServer/GameServer/TestGameServerConfig.cs b/RefreshTests.GameServer/GameServer/TestGameServerConfig.cs index a5225a559..539b61f82 100644 --- a/RefreshTests.GameServer/GameServer/TestGameServerConfig.cs +++ b/RefreshTests.GameServer/GameServer/TestGameServerConfig.cs @@ -24,4 +24,5 @@ public void TestMigration() public int MaximumAssetSafetyLevel { get; set; } = 0; public int MaximumAssetSafetyLevelForTrustedUsers { get; set; } = 0; + public int UserFilesizeQuota { get; set; } = 141; } \ No newline at end of file diff --git a/RefreshTests.GameServer/Tests/Assets/AssetUploadTests.cs b/RefreshTests.GameServer/Tests/Assets/AssetUploadTests.cs index ed97b72af..2a79fd7fd 100644 --- a/RefreshTests.GameServer/Tests/Assets/AssetUploadTests.cs +++ b/RefreshTests.GameServer/Tests/Assets/AssetUploadTests.cs @@ -39,7 +39,8 @@ public void CannotUploadAssetPastFillingFilesizeQuota(bool psp) { using TestContext context = this.GetServer(); - context.Server.Value.GameServerConfig.UserFilesizeQuota = 8; + context.Server.Value.GameServerConfig.NormalUserPermissions.UserFilesizeQuota = 8; + context.Server.Value.GameServerConfig.TrustedUserPermissions.UserFilesizeQuota = 8; context.Server.Value.Server.AddService(); GameUser user = context.CreateUser(); diff --git a/RefreshTests.GameServer/Tests/Configs/GameServerConfigTests.cs b/RefreshTests.GameServer/Tests/Configs/GameServerConfigTests.cs index 701baa1f3..a99569f92 100644 --- a/RefreshTests.GameServer/Tests/Configs/GameServerConfigTests.cs +++ b/RefreshTests.GameServer/Tests/Configs/GameServerConfigTests.cs @@ -20,6 +20,7 @@ public void MigratesRolePermsFromVersion26() }, BlockedAssetFlags = new(AssetFlags.Dangerous | AssetFlags.Media), BlockedAssetFlagsForTrustedUsers = new(AssetFlags.Modded), + UserFilesizeQuota = 141, }; config.TestMigration(); @@ -38,6 +39,9 @@ public void MigratesRolePermsFromVersion26() Assert.That(config.NormalUserPermissions.BlockedAssetFlags.ToAssetFlags(), Is.EqualTo(AssetFlags.Dangerous | AssetFlags.Media)); Assert.That(config.TrustedUserPermissions.BlockedAssetFlags.ToAssetFlags(), Is.EqualTo(AssetFlags.Modded)); + + Assert.That(config.NormalUserPermissions.UserFilesizeQuota, Is.EqualTo(141)); + Assert.That(config.TrustedUserPermissions.UserFilesizeQuota, Is.EqualTo(141)); } [Test]