Skip to content
Open
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
53 changes: 46 additions & 7 deletions Refresh.Core/Configuration/GameServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Refresh.Core.Configuration;
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class GameServerConfig : Config
{
public override int CurrentConfigVersion => 28;
public override int CurrentConfigVersion => 29;
public override int Version { get; set; } = 0;

protected override void Migrate(int oldVer, dynamic oldConfig)
Expand All @@ -17,14 +17,17 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
// to more cleanly split the perms between certain roles, and to make their enforcement easier.
if (oldVer < 27)
{
this.NewUserPermissions = new();
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
// Migrate filesize quota
if (oldVer >= 11)
{
try
{
this.NewUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota;
this.NormalUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota;
this.TrustedUserPermissions.UserFilesizeQuota = (int)oldConfig.UserFilesizeQuota;
}
Expand All @@ -34,8 +37,13 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
}
}

// Migrate asset flags/safety level
if (oldVer >= 18)
{
this.NewUserPermissions.BlockedAssetFlags.Dangerous = (bool)oldConfig.BlockedAssetFlags.Dangerous;
this.NewUserPermissions.BlockedAssetFlags.Media = (bool)oldConfig.BlockedAssetFlags.Media;
this.NewUserPermissions.BlockedAssetFlags.Modded = (bool)oldConfig.BlockedAssetFlags.Modded;

this.NormalUserPermissions.BlockedAssetFlags.Dangerous = (bool)oldConfig.BlockedAssetFlags.Dangerous;
this.NormalUserPermissions.BlockedAssetFlags.Media = (bool)oldConfig.BlockedAssetFlags.Media;
this.NormalUserPermissions.BlockedAssetFlags.Modded = (bool)oldConfig.BlockedAssetFlags.Modded;
Expand All @@ -50,12 +58,14 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
if (oldVer >= 2)
{
int oldSafetyLevel = (int)oldConfig.MaximumAssetSafetyLevel;
this.NormalUserPermissions.BlockedAssetFlags = new ConfigAssetFlags
ConfigAssetFlags fromSafetyLevel = new ConfigAssetFlags
{
Dangerous = oldSafetyLevel < 3,
Modded = oldSafetyLevel < 2,
Media = oldSafetyLevel < 1,
};
this.NormalUserPermissions.BlockedAssetFlags = fromSafetyLevel;
this.NewUserPermissions.BlockedAssetFlags = fromSafetyLevel;
}

// 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
Expand All @@ -80,8 +90,13 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
}

// Timed level upload limits were added in version 19.
// Migrate level limits
if (oldVer >= 19)
{
this.NewUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled;
this.NewUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours;
this.NewUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota;

this.NormalUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TimedLevelUploadLimits.Enabled;
this.NormalUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TimedLevelUploadLimits.TimeSpanHours;
this.NormalUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TimedLevelUploadLimits.LevelQuota;
Expand All @@ -94,37 +109,60 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
// Read-only mode was added for both normal and trusted users in version 20.
if (oldVer >= 20)
{
this.NewUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadOnlyMode;
this.NormalUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadOnlyMode;
this.TrustedUserPermissions.ReadOnlyMode = (bool)oldConfig.ReadonlyModeForTrustedUsers;
}
}

// In version 28, PhotoUploadRateLimit and PlaylistUploadRateLimit were added to RolePermissions, and various renamings related to level upload rate-limiting
// were done to prepare for this: the class TimedLevelUploadLimitProperties was renamed to EntityUploadRateLimitProperties, its attribute LevelQuota was renamed to UploadQuota,
// and RolePermissions' attribute TimedLevelUploadLimits was renamed to LevelUploadRateLimit
// In version 28, PhotoUploadRateLimit and PlaylistUploadRateLimit were added to RolePermissions
// and various attributes related to level rate-limiting were renamed
else if (oldVer == 27)
{
this.NormalUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.Enabled;
this.NormalUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.TimeSpanHours;
this.NormalUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.LevelQuota;

this.NormalUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.Enabled;
this.NormalUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.TimeSpanHours;
this.NormalUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.NormalUserPermissions.TimedLevelUploadLimits.LevelQuota;

this.TrustedUserPermissions.LevelUploadRateLimit.Enabled = (bool)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.Enabled;
this.TrustedUserPermissions.LevelUploadRateLimit.TimeSpanHours = (int)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.TimeSpanHours;
this.TrustedUserPermissions.LevelUploadRateLimit.UploadQuota = (int)oldConfig.TrustedUserPermissions.TimedLevelUploadLimits.LevelQuota;
}

// In version 29, the NewUser role and its related config options
// (new user role perms and HoursUntilNewAccountNoLongerNew) were added
else if (oldVer < 29)
{
this.NewUserPermissions = oldConfig.NormalUserPermissions;
}
}

public string LicenseText { get; set; } = "Welcome to Refresh!";

/// <summary>
/// Role-specific permissions for normal users and below
/// Role-specific permissions for new users.
/// </summary>
public RolePermissions NewUserPermissions = new();
/// <summary>
/// Role-specific permissions for normal, not-new users.
/// </summary>
public RolePermissions NormalUserPermissions = new();
/// <summary>
/// Role-specific permissions for trusted users and above
/// Role-specific permissions for trusted users and above.
/// </summary>
public RolePermissions TrustedUserPermissions = new();

/// <summary>
/// How long we should wait (in hours) until we should use NewUserJob to set a new user's role from NewUser to User,
/// effectively marking them as no longer new.
/// Once their account hits this age, we will start applying NormalUserPermissions instead of NewUserPermissions
/// as their role-perms.
/// </summary>
public int HoursUntilNewAccountNoLongerNew { get; set; } = 24 * 7; // TODO should we think of a better name?

public bool AllowUsersToUseIpAuthentication { get; set; } = false;
public bool PermitPsnLogin { get; set; } = true;
public bool PermitRpcnLogin { get; set; } = true;
Expand Down Expand Up @@ -181,5 +219,6 @@ protected override void Migrate(int oldVer, dynamic oldConfig)
public string[] HmacDigestKeys = ["CustomServerDigest"];

public bool PermitShowingOnlineUsers { get; set; } = true;

public bool EnableDiveIn { get; set; } = true;
}
21 changes: 21 additions & 0 deletions Refresh.Core/Configuration/RolePermissions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,25 @@ public RolePermissions() {}
/// 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;

// Not configurable because "Restricted" and "Banned" already imply a user has no uploading perms.
public static RolePermissions FromRestrictedUser => new()
{
ReadOnlyMode = true,
BlockedAssetFlags = new(AssetFlags.Dangerous | AssetFlags.Modded | AssetFlags.Media),
// Not enabled because restricted may not upload UGC anyway, also we already enable read-only mode for them
LevelUploadRateLimit = new()
{
Enabled = false,
},
PhotoUploadRateLimit = new()
{
Enabled = false,
},
PlaylistUploadRateLimit = new()
{
Enabled = false,
},
UserFilesizeQuota = 0,
};
}
19 changes: 14 additions & 5 deletions Refresh.Core/Extensions/GameUserExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ public static class GameUserExtensions
{
public static bool IsWriteBlocked(this GameUser user, GameServerConfig config)
{
// Admins may always bypass this
if (user.Role == GameUserRole.Admin) return false;
return GetRolePermissionsForUser(user, config).ReadOnlyMode;

// Restricted and Banned may not upload/edit any UGC, they also have no role perms because unnecessary
else if (user.Role <= GameUserRole.Restricted) return true;

// Determine based on role perms
else return GetRolePermissionsForUser(user, config).ReadOnlyMode;
}

public static bool MayModifyUser(this GameUser user, GameUser targetUser)
Expand All @@ -26,9 +32,12 @@ public static bool MayModifyUser(this GameUser user, GameUser targetUser)

public static RolePermissions GetRolePermissionsForUser(this GameUser user, GameServerConfig config)
{
if (user.Role >= GameUserRole.Trusted)
return config.TrustedUserPermissions;

return config.NormalUserPermissions;
return user.Role switch
{
>= GameUserRole.Trusted => config.TrustedUserPermissions,
GameUserRole.User => config.NormalUserPermissions,
GameUserRole.NewUser => config.NewUserPermissions,
_ => RolePermissions.FromRestrictedUser,
};
}
}
3 changes: 2 additions & 1 deletion Refresh.Core/Services/RoleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ internal RoleService(GameAuthenticationService authService, GameServerConfig con
if (!(authAttrib?.Required ?? true)) return null;

MinimumRoleAttribute? roleAttrib = method.GetCustomAttribute<MinimumRoleAttribute>();
GameUserRole minimumRole = roleAttrib?.MinimumRole ?? GameUserRole.User;
GameUserRole minimumRole = roleAttrib?.MinimumRole ?? GameUserRole.NewUser;

GameUser? user = (GameUser?)this._authService.AuthenticateToken(context, database)?.User;
if (user == null) return null; // Let AuthenticationProvider handle 401

// if the user's role is lower than the minimum role for this endpoint, then return unauthorized
// by default, Restricted and Banned will be rejected by this since the default role is NewUser
if (user.Role < minimumRole)
{
return Unauthorized;
Expand Down
4 changes: 2 additions & 2 deletions Refresh.Database/Models/Users/GameUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ public partial class GameUser : IRateLimitUser
/// If `true`, unescape XML tags sent to /filter
/// </summary>
public bool UnescapeXmlSequences { get; set; }
public GameUserRole Role { get; set; }

public GameUserRole Role { get; set; } = GameUserRole.NewUser;

/// <summary>
/// Whether planets containing mods or VoiceRecordings should be shown in-game
Expand Down
6 changes: 6 additions & 0 deletions Refresh.Database/Models/Users/GameUserRole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public enum GameUserRole : sbyte
/// </summary>
User = 0,
/// <summary>
/// A newly registered user. Can have different, usually more restrictive configurable perms than regular users.
/// Useful to make spam more difficult, for example. The duration in which an account's age makes it "new" is defined by config.
/// Automatically promoted to User by NewUserJob.
/// </summary>
NewUser = -32,
/// <summary>
/// A user with read-only permissions. May log in and play, but cannot do things such as publish levels or post comments.
/// </summary>
Restricted = -126,
Expand Down
4 changes: 3 additions & 1 deletion Refresh.GameServer/RefreshGameServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,14 @@ protected override void SetupServices()

protected virtual void SetupWorkers()
{
this.WorkerManager = RefreshWorkerManager.Create(this.Logger, this._dataStore, this._databaseProvider);
this.WorkerManager = RefreshWorkerManager.Create(this.Logger, this._dataStore, this._databaseProvider, this.GetTimeProvider());

if (this._configStore.Integration.DiscordWebhookEnabled && this._configStore.GameServer.PermitShowingOnlineUsers)
{
this.WorkerManager.AddJob(new DiscordIntegrationJob(this._configStore.Integration, this._configStore.GameServer));
}

this.WorkerManager.AddJob(new NewUserJob(this._configStore.GameServer.HoursUntilNewAccountNoLongerNew));
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public Response StartPublish(RequestContext context,
{
if (dataContext.User!.IsWriteBlocked(config))
{
dataContext.Database.AddPublishFailNotification("The server is in read-only mode.", body.Title, dataContext.User!);
dataContext.Database.AddPublishFailNotification($"The server is in read-only mode.", body.Title, dataContext.User!);
return Unauthorized;
}

Expand Down
5 changes: 3 additions & 2 deletions Refresh.Interfaces.Workers/RefreshWorkerManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bunkum.Core.Storage;
using NotEnoughLogs;
using Refresh.Common.Time;
using Refresh.Database;
using Refresh.Interfaces.Workers.Migrations;
using Refresh.Interfaces.Workers.Repeating;
Expand All @@ -9,9 +10,9 @@ namespace Refresh.Interfaces.Workers;

public static class RefreshWorkerManager
{
public static WorkerManager Create(Logger logger, IDataStore dataStore, GameDatabaseProvider databaseProvider)
public static WorkerManager Create(Logger logger, IDataStore dataStore, GameDatabaseProvider databaseProvider, IDateTimeProvider timeProvider)
{
WorkerManager manager = new(logger, dataStore, databaseProvider);
WorkerManager manager = new(logger, dataStore, databaseProvider, timeProvider);

manager.AddJob<PunishmentExpiryJob>();
manager.AddJob<CleanupExpiredObjectsJob>();
Expand Down
40 changes: 40 additions & 0 deletions Refresh.Interfaces.Workers/Repeating/NewUserJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Refresh.Common;
using Refresh.Database;
using Refresh.Database.Models.Users;
using Refresh.Workers;

namespace Refresh.Interfaces.Workers.Repeating;

/// <summary>
/// A worker that handles setting new users as regular users, depending on their account age and the server config.
/// </summary>
// TODO also set users back as "new" if duration in config is updated to result in user being "new" again
public class NewUserJob : RepeatingJob
{
private int _requiredAccountAge;
protected override int Interval => 60_000 * 5; // 5 minutes, no need to execute too often

public NewUserJob(int requiredAccountAge)
{
this._requiredAccountAge = requiredAccountAge;
}

public override void ExecuteJob(WorkContext context)
{
DateTimeOffset now = context.TimeProvider.Now;
DatabaseList<GameUser> newUsers = context.Database.GetAllUsersWithRole(GameUserRole.NewUser);

foreach (GameUser user in newUsers.Items.ToList())
{
// If an account is, e.g., 2 hours and 40 minutes old, and max age for new users is 3 hours, we wouldn't
// consider max to be reached yet, so floor the difference.
long accountAge = (long)Math.Floor(now.Subtract(user.JoinDate).TotalHours);

context.Logger.LogDebug(RefreshContext.Worker, $"{nameof(NewUserJob)} - new user: {user}, join date: {user.JoinDate}, current time: {now}, account age: {accountAge}h, configured required age: {this._requiredAccountAge}h.");
if (accountAge < this._requiredAccountAge) continue; // Don't promote user if they haven't reached max age yet

context.Logger.LogInfo(RefreshContext.Worker, $"Promoting {user} to regular user since their account is {accountAge} hours old now (required configured age: {this._requiredAccountAge}h).");
context.Database.SetUserRole(user, GameUserRole.User);
}
}
}
3 changes: 2 additions & 1 deletion Refresh.WorkerManager/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Bunkum.Core.Storage;
using NotEnoughLogs;
using NotEnoughLogs.Behaviour;
using Refresh.Common.Time;
using Refresh.Core.Configuration;
using Refresh.Database;
using Refresh.Database.Configuration;
Expand Down Expand Up @@ -33,7 +34,7 @@
database.Warmup();

logger.LogInfo(BunkumCategory.Startup, "Starting worker manager!");
WorkerManager manager = RefreshWorkerManager.Create(logger, new FileSystemDataStore(), database);
WorkerManager manager = RefreshWorkerManager.Create(logger, new FileSystemDataStore(), database, new SystemDateTimeProvider());

manager.Start();
manager.WaitForExit();
3 changes: 3 additions & 0 deletions Refresh.Workers/WorkContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bunkum.Core.Storage;
using NotEnoughLogs;
using Refresh.Common.Time;
using Refresh.Database;

namespace Refresh.Workers;
Expand All @@ -9,4 +10,6 @@ public class WorkContext : IDataContext
public required GameDatabaseContext Database { get; init; }
public required Logger Logger { get; init; }
public required IDataStore DataStore { get; init; }
// TODO also use this in jobs outside of NewUserJob which also rely on current time
public required IDateTimeProvider TimeProvider { get; init; }
}
Loading
Loading