Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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,61 @@ 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
// (NewUserPermissions 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>
/// The minimum age (time after registration) an account must have before it automatically gets promoted from
/// NewUser to User.
/// We do this by using NewUserJob to compare the account's age, and if it is old enough, we update their role.
/// Currently, the only difference between new users and regular users is that we apply NewUserPermissions
/// instead of NormalUserPermissions, which you can freely configure in this config.
/// </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
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 @@ -185,12 +185,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 @@ -192,7 +192,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 job that handles promoting new users to 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 readonly 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 (configured required 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; }
}
6 changes: 5 additions & 1 deletion Refresh.Workers/WorkerManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Bunkum.Core.Storage;
using NotEnoughLogs;
using Refresh.Common;
using Refresh.Common.Time;
using Refresh.Database;
using Refresh.Database.Models.Workers;
using Refresh.Workers.State;
Expand All @@ -12,6 +13,7 @@ public class WorkerManager
private readonly Logger _logger;
private readonly IDataStore _dataStore;
private readonly GameDatabaseProvider _databaseProvider;
private readonly IDateTimeProvider _timeProvider;

private readonly int _workerId;

Expand All @@ -22,11 +24,12 @@ public class WorkerManager

private readonly List<WorkerJob> _jobs = [];

public WorkerManager(Logger logger, IDataStore dataStore, GameDatabaseProvider databaseProvider)
public WorkerManager(Logger logger, IDataStore dataStore, GameDatabaseProvider databaseProvider, IDateTimeProvider timeProvider)
{
this._dataStore = dataStore;
this._databaseProvider = databaseProvider;
this._logger = logger;
this._timeProvider = timeProvider;

using GameDatabaseContext context = this._databaseProvider.GetContext();
this._workerId = context.CreateWorker();
Expand All @@ -49,6 +52,7 @@ public void RunWorkCycle()
Database = this._databaseProvider.GetContext(),
Logger = this._logger,
DataStore = this._dataStore,
TimeProvider = this._timeProvider,
};

foreach (WorkerJob job in this._jobs)
Expand Down
Loading
Loading