Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 81 additions & 40 deletions Refresh.Core/Configuration/GameServerConfig.cs
Original file line number Diff line number Diff line change
@@ -1,61 +1,116 @@
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;

[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
[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 more cleanly split the perms between certain roles, and to make their enforcement easier.
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();

// filesize quota limit was added during version 11, but the version wasn't bumped, so catch error to be safe
if (oldVer >= 11)
{
int oldSafetyLevel = (int)oldConfig.MaximumAssetSafetyLevel;
this.BlockedAssetFlags = new ConfigAssetFlags
try
{
this.NormalUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota;
this.TrustedUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota;
}
catch (RuntimeBinderException)
{
Dangerous = oldSafetyLevel < 3,
Modded = oldSafetyLevel < 2,
Media = oldSafetyLevel < 1,
};
// do nothing
}
}

// 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)
if (oldVer >= 18)
{
// There was no version bump for trusted users being added, so we just have to catch this error :/
try
this.NormalUserPermissions.BlockedAssetFlags.Dangerous = (bool)oldConfig.BlockedAssetFlags.Dangerous;
this.NormalUserPermissions.BlockedAssetFlags.Media = (bool)oldConfig.BlockedAssetFlags.Media;
this.NormalUserPermissions.BlockedAssetFlags.Modded = (bool)oldConfig.BlockedAssetFlags.Modded;

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
{
// 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);
/// <seealso cref="GameUserRole.Trusted"/>
public ConfigAssetFlags BlockedAssetFlagsForTrustedUsers { get; set; } = new(AssetFlags.Dangerous | AssetFlags.Modded);
/// <summary>
/// Role-specific permissions for normal users and below
/// </summary>
public RolePermissions NormalUserPermissions = new();
/// <summary>
/// Role-specific permissions for trusted users and above
/// </summary>
public RolePermissions TrustedUserPermissions = new();

public bool AllowUsersToUseIpAuthentication { get; set; } = false;
public bool PermitPsnLogin { get; set; } = true;
public bool PermitRpcnLogin { get; set; } = true;
Expand Down Expand Up @@ -97,20 +152,6 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
/// </summary>
public string GameConfigStorageUrl { get; set; } = "https://refresh.example.com/lbp";
public bool AllowInvalidTextureGuids { get; set; } = false;
public bool ReadOnlyMode { get; set; } = false;
/// <seealso cref="GameUserRole.Trusted"/>
public bool ReadonlyModeForTrustedUsers { get; set; } = false;
/// <summary>
/// The amount of data the user is allowed to upload before all resource uploads get blocked, defaults to 100mb.
/// </summary>
public int UserFilesizeQuota { get; set; } = 100 * 1_048_576;

public TimedLevelUploadLimitProperties TimedLevelUploadLimits { get; set; } = new()
{
Enabled = false,
TimeSpanHours = 24,
LevelQuota = 10,
};

/// <summary>
/// Whether to print the room state whenever a `FindBestRoom` match returns no results
Expand Down
22 changes: 22 additions & 0 deletions Refresh.Core/Configuration/RolePermissions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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,
};

/// <summary>
/// The amount of data the user is allowed to upload before all resource uploads get blocked, defaults to 100mb.
/// </summary>
public int UserFilesizeQuota { get; set; } = 100 * 1_048_576;
}
16 changes: 10 additions & 6 deletions Refresh.Core/Extensions/GameUserExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ 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;
if (user.Role == GameUserRole.Admin) return false;
return GetRolePermissionsForUser(user, config).ReadOnlyMode;
}

public static bool MayModifyUser(this GameUser user, GameUser targetUser)
Expand All @@ -27,4 +23,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;
}
}
4 changes: 2 additions & 2 deletions Refresh.Interfaces.APIv3/Endpoints/InstanceApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public ApiResponse<ApiInstanceResponse> 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(
Expand Down
5 changes: 3 additions & 2 deletions Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}

Expand Down
12 changes: 7 additions & 5 deletions Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,16 @@ public Response StartPublish(RequestContext context,
GameLevelRequest body,
DataContext dataContext,
GameServerConfig config,
IDateTimeProvider dateTimeProvider)
IDateTimeProvider dateTimeProvider,
GameUser user)
{
if (dataContext.User!.IsWriteBlocked(config))
{
dataContext.Database.AddPublishFailNotification("The server is in read-only mode.", body.Title, dataContext.User!);
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -66,9 +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 = config.BlockedAssetFlags.ToAssetFlags();
if (user.Role >= GameUserRole.Trusted)
blockedAssetFlags = config.BlockedAssetFlagsForTrustedUsers.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,
Expand Down
28 changes: 28 additions & 0 deletions RefreshTests.GameServer/GameServer/TestGameServerConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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;
public int UserFilesizeQuota { get; set; } = 141;
}
Loading
Loading