Automated whitelists (#23985)

* Beginnings of making the breadmemes jobs easier

* stuff

* stuff pt. 2

* Stuff pt.3

* Stuff I forgot last time

* Basic whitelist

Only people that are added to the whitelist with the addwhitelist command will be able to join. I call this the "legacy" whitelist

* Remove always deny condition in favor of just breaking if playtime check fails

* Change default whitelist

Default whitelist is now the "legacy" whitelist.

* localization

* Admin check

* minor spelling change

* Fix build

* Whitelist message

* Fix vars not being datafield and spelling mistakes

* Minor spelling mistake

* Change config for salamander

* Reviews and stuff

* Add summaries

* Fix whitelists

* Forgot to add a datafield

* Fixing stuff I guess

* Reuse admin remarks to reduce load when connecting.

* Update log messages to be verbose instead of debug

* Reviews

* whoops

* Explain a bit more how whitelist checking works

* Apply CE's review

* Append Membership to Blacklist and Whitelist conditions

* Fix review comments

* Uncapitalize playerConnectionWhitelist, add to ignored client prototypes

* Make note count field work

* Fix cvar for thingy

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
Simon
2024-08-27 18:01:17 +02:00
committed by GitHub
parent e59b9c5714
commit f92ef41538
28 changed files with 4289 additions and 47 deletions

View File

@@ -108,6 +108,7 @@ namespace Content.Client.Entry
_prototypeManager.RegisterIgnore("lobbyBackground");
_prototypeManager.RegisterIgnore("gamePreset");
_prototypeManager.RegisterIgnore("noiseChannel");
_prototypeManager.RegisterIgnore("playerConnectionWhitelist");
_prototypeManager.RegisterIgnore("spaceBiome");
_prototypeManager.RegisterIgnore("worldgenConfig");
_prototypeManager.RegisterIgnore("gameRule");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class Blacklist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "blacklist",
columns: table => new
{
user_id = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_blacklist", x => x.user_id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "blacklist");
}
}
}

View File

@@ -512,6 +512,20 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist",
b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string) null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class Blacklist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "blacklist",
columns: table => new
{
user_id = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_blacklist", x => x.user_id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "blacklist");
}
}
}

View File

@@ -483,6 +483,19 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist",
b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string) null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")

View File

@@ -28,6 +28,7 @@ namespace Content.Server.Database
public DbSet<AdminLog> AdminLog { get; set; } = null!;
public DbSet<AdminLogPlayer> AdminLogPlayer { get; set; } = null!;
public DbSet<Whitelist> Whitelist { get; set; } = null!;
public DbSet<Blacklist> Blacklist { get; set; } = null!;
public DbSet<ServerBan> Ban { get; set; } = default!;
public DbSet<ServerUnban> Unban { get; set; } = default!;
public DbSet<ServerBanExemption> BanExemption { get; set; } = default!;
@@ -551,6 +552,15 @@ namespace Content.Server.Database
[Required, Key] public Guid UserId { get; set; }
}
/// <summary>
/// List of users who are on the "blacklist". This is a list that may be used by Whitelist implementations to deny access to certain users.
/// </summary>
[Table("blacklist")]
public class Blacklist
{
[Required, Key] public Guid UserId { get; set; }
}
public class Admin
{
[Key] public Guid UserId { get; set; }

View File

@@ -0,0 +1,221 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Connection.Whitelist;
using Content.Server.Connection.Whitelist.Conditions;
using Content.Server.Database;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Players.PlayTimeTracking;
using Robust.Shared.Network;
namespace Content.Server.Connection;
/// <summary>
/// Handles whitelist conditions for incoming connections.
/// </summary>
public sealed partial class ConnectionManager
{
private PlayerConnectionWhitelistPrototype[]? _whitelists;
public void PostInit()
{
_cfg.OnValueChanged(CCVars.WhitelistPrototypeList, UpdateWhitelists, true);
}
private void UpdateWhitelists(string s)
{
var list = new List<PlayerConnectionWhitelistPrototype>();
foreach (var id in s.Split(','))
{
if (_prototypeManager.TryIndex(id, out PlayerConnectionWhitelistPrototype? prototype))
{
list.Add(prototype);
}
else
{
_sawmill.Fatal($"Whitelist prototype {id} does not exist. Denying all connections.");
_whitelists = null; // Invalidate the list, causes deny on all connections.
return;
}
}
_whitelists = list.ToArray();
}
private bool IsValid(PlayerConnectionWhitelistPrototype whitelist, int playerCount)
{
return playerCount >= whitelist.MinimumPlayers && playerCount <= whitelist.MaximumPlayers;
}
public async Task<(bool isWhitelisted, string? denyMessage)> IsWhitelisted(PlayerConnectionWhitelistPrototype whitelist, NetUserData data, ISawmill sawmill)
{
var cacheRemarks = await _db.GetAllAdminRemarks(data.UserId);
var cachePlaytime = await _db.GetPlayTimes(data.UserId);
foreach (var condition in whitelist.Conditions)
{
bool matched;
string denyMessage;
switch (condition)
{
case ConditionAlwaysMatch:
matched = true;
denyMessage = Loc.GetString("whitelist-always-deny");
break;
case ConditionManualWhitelistMembership:
matched = await CheckConditionManualWhitelist(data);
denyMessage = Loc.GetString("whitelist-manual");
break;
case ConditionManualBlacklistMembership:
matched = await CheckConditionManualBlacklist(data);
denyMessage = Loc.GetString("whitelist-blacklisted");
break;
case ConditionNotesDateRange conditionNotes:
matched = CheckConditionNotesDateRange(conditionNotes, cacheRemarks);
denyMessage = Loc.GetString("whitelist-notes");
break;
case ConditionPlayerCount conditionPlayerCount:
matched = CheckConditionPlayerCount(conditionPlayerCount);
denyMessage = Loc.GetString("whitelist-player-count");
break;
case ConditionPlaytime conditionPlaytime:
matched = CheckConditionPlaytime(conditionPlaytime, cachePlaytime);
denyMessage = Loc.GetString("whitelist-playtime", ("minutes", conditionPlaytime.MinimumPlaytime));
break;
case ConditionNotesPlaytimeRange conditionNotesPlaytimeRange:
matched = CheckConditionNotesPlaytimeRange(conditionNotesPlaytimeRange, cacheRemarks, cachePlaytime);
denyMessage = Loc.GetString("whitelist-notes");
break;
default:
throw new NotImplementedException($"Whitelist condition {condition.GetType().Name} not implemented");
}
sawmill.Verbose($"User {data.UserName} whitelist condition {condition.GetType().Name} result: {matched}");
sawmill.Verbose($"Action: {condition.Action.ToString()}");
switch (condition.Action)
{
case ConditionAction.Allow:
if (matched)
{
sawmill.Verbose($"User {data.UserName} passed whitelist condition {condition.GetType().Name} and it's a breaking condition");
return (true, denyMessage);
}
break;
case ConditionAction.Deny:
if (matched)
{
sawmill.Verbose($"User {data.UserName} failed whitelist condition {condition.GetType().Name}");
return (false, denyMessage);
}
break;
default:
sawmill.Verbose($"User {data.UserName} failed whitelist condition {condition.GetType().Name} but it's not a breaking condition");
break;
}
}
sawmill.Verbose($"User {data.UserName} passed all whitelist conditions");
return (true, null);
}
#region Condition Checking
private async Task<bool> CheckConditionManualWhitelist(NetUserData data)
{
return !(await _db.GetWhitelistStatusAsync(data.UserId));
}
private async Task<bool> CheckConditionManualBlacklist(NetUserData data)
{
return await _db.GetBlacklistStatusAsync(data.UserId);
}
private bool CheckConditionNotesDateRange(ConditionNotesDateRange conditionNotes, List<IAdminRemarksRecord> remarks)
{
var range = DateTime.UtcNow.AddDays(-conditionNotes.Range);
return CheckRemarks(remarks,
conditionNotes.IncludeExpired,
conditionNotes.IncludeSecret,
conditionNotes.MinimumSeverity,
conditionNotes.MinimumNotes,
adminRemarksRecord => adminRemarksRecord.CreatedAt > range);
}
private bool CheckConditionPlayerCount(ConditionPlayerCount conditionPlayerCount)
{
var count = _plyMgr.PlayerCount;
return count >= conditionPlayerCount.MinimumPlayers && count <= conditionPlayerCount.MaximumPlayers;
}
private bool CheckConditionPlaytime(ConditionPlaytime conditionPlaytime, List<PlayTime> playtime)
{
var tracker = playtime.Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
if (tracker is null)
{
return false;
}
return tracker.TimeSpent.TotalMinutes >= conditionPlaytime.MinimumPlaytime;
}
private bool CheckConditionNotesPlaytimeRange(
ConditionNotesPlaytimeRange conditionNotesPlaytimeRange,
List<IAdminRemarksRecord> remarks,
List<PlayTime> playtime)
{
var overallTracker = playtime.Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
if (overallTracker is null)
{
return false;
}
return CheckRemarks(remarks,
conditionNotesPlaytimeRange.IncludeExpired,
conditionNotesPlaytimeRange.IncludeSecret,
conditionNotesPlaytimeRange.MinimumSeverity,
conditionNotesPlaytimeRange.MinimumNotes,
adminRemarksRecord => adminRemarksRecord.PlaytimeAtNote >= overallTracker.TimeSpent - TimeSpan.FromMinutes(conditionNotesPlaytimeRange.Range));
}
private bool CheckRemarks(List<IAdminRemarksRecord> remarks, bool includeExpired, bool includeSecret, NoteSeverity minimumSeverity, int MinimumNotes, Func<IAdminRemarksRecord, bool> additionalCheck)
{
var utcNow = DateTime.UtcNow;
var notes = remarks.Count(r => r is AdminNoteRecord note && note.Severity >= minimumSeverity && (includeSecret || !note.Secret) && (includeExpired || note.ExpirationTime == null || note.ExpirationTime > utcNow));
if (notes < MinimumNotes)
{
return false;
}
foreach (var adminRemarksRecord in remarks)
{
// If we're not including expired notes, skip them
if (!includeExpired && (adminRemarksRecord.ExpirationTime == null || adminRemarksRecord.ExpirationTime <= utcNow))
continue;
// In order to get the severity of the remark, we need to see if it's an AdminNoteRecord.
if (adminRemarksRecord is not AdminNoteRecord adminNoteRecord)
continue;
// We want to filter out secret notes if we're not including them.
if (!includeSecret && adminNoteRecord.Secret)
continue;
// At this point, we need to remove the note if it's not within the severity range.
if (adminNoteRecord.Severity < minimumSeverity)
continue;
// Perform the additional check specific to each method
if (!additionalCheck(adminRemarksRecord))
continue;
// If we've made it this far, we have a match
return true;
}
// No matches
return false;
}
#endregion
}

View File

@@ -1,5 +1,9 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Content.Server.Connection.Whitelist;
using Content.Server.Connection.Whitelist.Conditions;
using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
@@ -14,6 +18,7 @@ using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -26,6 +31,7 @@ namespace Content.Server.Connection
public interface IConnectionManager
{
void Initialize();
void PostInit();
/// <summary>
/// Temporarily allow a user to bypass regular connection requirements.
@@ -43,7 +49,7 @@ namespace Content.Server.Connection
/// <summary>
/// Handles various duties like guest username assignment, bans, connection logs, etc...
/// </summary>
public sealed class ConnectionManager : IConnectionManager
public sealed partial class ConnectionManager : IConnectionManager
{
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IPlayerManager _plyMgr = default!;
@@ -52,12 +58,14 @@ namespace Content.Server.Connection
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
private ISawmill _sawmill = default!;
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
public void Initialize()
{
@@ -268,20 +276,33 @@ namespace Content.Server.Connection
return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null);
}
if (_cfg.GetCVar(CCVars.WhitelistEnabled))
// Checks for whitelist IF it's enabled AND the user isn't an admin. Admins are always allowed.
if (_cfg.GetCVar(CCVars.WhitelistEnabled) && adminData is null)
{
var min = _cfg.GetCVar(CCVars.WhitelistMinPlayers);
var max = _cfg.GetCVar(CCVars.WhitelistMaxPlayers);
var playerCountValid = _plyMgr.PlayerCount >= min && _plyMgr.PlayerCount < max;
if (playerCountValid && await _db.GetWhitelistStatusAsync(userId) == false
&& adminData is null)
if (_whitelists is null)
{
var msg = Loc.GetString(_cfg.GetCVar(CCVars.WhitelistReason));
// was the whitelist playercount changed?
if (min > 0 || max < int.MaxValue)
msg += "\n" + Loc.GetString("whitelist-playercount-invalid", ("min", min), ("max", max));
return (ConnectionDenyReason.Whitelist, msg, null);
_sawmill.Error("Whitelist enabled but no whitelists loaded.");
// Misconfigured, deny everyone.
return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-misconfigured"), null);
}
foreach (var whitelist in _whitelists)
{
if (!IsValid(whitelist, _plyMgr.PlayerCount))
{
// Not valid for current player count.
continue;
}
var whitelistStatus = await IsWhitelisted(whitelist, e.UserData, _sawmill);
if (!whitelistStatus.isWhitelisted)
{
// Not whitelisted.
return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-fail-prefix", ("msg", whitelistStatus.denyMessage!)), null);
}
// Whitelisted, don't check any more.
break;
}
}

View File

@@ -0,0 +1,117 @@
using Content.Server.Administration;
using Content.Server.Database;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Connection.Whitelist;
[AdminCommand(AdminFlags.Ban)]
public sealed class AddBlacklistCommand : LocalizedCommands
{
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IServerDbManager _db = default!;
public override string Command => "blacklistadd";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
shell.WriteLine(Help);
return;
}
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("shell-need-exactly-one-argument"));
shell.WriteLine(Help);
return;
}
var name = args[0];
var data = await _playerLocator.LookupIdByNameAsync(name);
if (data == null)
{
shell.WriteError(Loc.GetString("cmd-blacklistadd-not-found", ("username", args[0])));
return;
}
var guid = data.UserId;
var isBlacklisted = await _db.GetBlacklistStatusAsync(guid);
if (isBlacklisted)
{
shell.WriteLine(Loc.GetString("cmd-blacklistadd-existing", ("username", data.Username)));
return;
}
await _db.AddToBlacklistAsync(guid);
shell.WriteLine(Loc.GetString("cmd-blacklistadd-added", ("username", data.Username)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHint(Loc.GetString("cmd-blacklistadd-arg-player"));
}
return CompletionResult.Empty;
}
}
[AdminCommand(AdminFlags.Ban)]
public sealed class RemoveBlacklistCommand : LocalizedCommands
{
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IServerDbManager _db = default!;
public override string Command => "blacklistremove";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
shell.WriteLine(Help);
return;
}
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("shell-need-exactly-one-argument"));
shell.WriteLine(Help);
return;
}
var name = args[0];
var data = await _playerLocator.LookupIdByNameAsync(name);
if (data == null)
{
shell.WriteError(Loc.GetString("cmd-blacklistremove-not-found", ("username", args[0])));
return;
}
var guid = data.UserId;
var isBlacklisted = await _db.GetBlacklistStatusAsync(guid);
if (!isBlacklisted)
{
shell.WriteLine(Loc.GetString("cmd-blacklistremove-existing", ("username", data.Username)));
return;
}
await _db.RemoveFromBlacklistAsync(guid);
shell.WriteLine(Loc.GetString("cmd-blacklistremove-removed", ("username", data.Username)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHint(Loc.GetString("cmd-blacklistremove-arg-player"));
}
return CompletionResult.Empty;
}
}

View File

@@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Robust.Shared.Network;
namespace Content.Server.Connection.Whitelist.Conditions;
/// <summary>
/// Condition that always matches
/// </summary>
public sealed partial class ConditionAlwaysMatch : WhitelistCondition
{
}

View File

@@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Content.Server.Database;
using Robust.Shared.Network;
namespace Content.Server.Connection.Whitelist.Conditions;
/// <summary>
/// Condition that matches if the player is in the manual blacklist.
/// </summary>
public sealed partial class ConditionManualBlacklistMembership : WhitelistCondition
{
}

View File

@@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Content.Server.Database;
using Robust.Shared.Network;
namespace Content.Server.Connection.Whitelist.Conditions;
/// <summary>
/// Condition that matches if the player is in the manual whitelist.
/// </summary>
public sealed partial class ConditionManualWhitelistMembership : WhitelistCondition
{
}

View File

@@ -0,0 +1,34 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Connection.Whitelist.Conditions;
/// <summary>
/// Condition that matches if the player has notes within a certain date range.
/// </summary>
public sealed partial class ConditionNotesDateRange : WhitelistCondition
{
[DataField]
public bool IncludeExpired = false;
[DataField]
public NoteSeverity MinimumSeverity = NoteSeverity.Minor;
/// <summary>
/// The minimum number of notes required.
/// </summary>
[DataField]
public int MinimumNotes = 1;
/// <summary>
/// Range in days to check for notes.
/// </summary>
[DataField]
public int Range = int.MaxValue;
[DataField]
public bool IncludeSecret = false;
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Database;
namespace Content.Server.Connection.Whitelist.Conditions;
/// <summary>
/// Condition that matches if the player has notes within a certain playtime range.
/// </summary>
public sealed partial class ConditionNotesPlaytimeRange : WhitelistCondition
{
[DataField]
public bool IncludeExpired = false;
[DataField]
public NoteSeverity MinimumSeverity = NoteSeverity.Minor;
/// <summary>
/// The minimum number of notes required.
/// </summary>
[DataField]
public int MinimumNotes = 1;
/// <summary>
/// The range in minutes to check for notes.
/// </summary>
[DataField]
public int Range = int.MaxValue;
[DataField]
public bool IncludeSecret = false;
}

View File

@@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Robust.Server.Player;
using Robust.Shared.Network;
namespace Content.Server.Connection.Whitelist.Conditions;
/// <summary>
/// Condition that matches if the player count is within a certain range.
/// </summary>
public sealed partial class ConditionPlayerCount : WhitelistCondition
{
[DataField]
public int MinimumPlayers = 0;
[DataField]
public int MaximumPlayers = int.MaxValue;
}

View File

@@ -0,0 +1,15 @@
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared.Players.PlayTimeTracking;
using Robust.Shared.Network;
namespace Content.Server.Connection.Whitelist.Conditions;
/// <summary>
/// Condition that matches if the player has played for a certain amount of time.
/// </summary>
public sealed partial class ConditionPlaytime : WhitelistCondition
{
[DataField]
public int MinimumPlaytime = 0; // In minutes
}

View File

@@ -0,0 +1,41 @@
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Shared.Network;
namespace Content.Server.Connection.Whitelist;
/// <summary>
/// This class is used to determine if a player should be allowed to join the server.
/// It is used in <see cref="PlayerConnectionWhitelistPrototype"/>
/// </summary>
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract partial class WhitelistCondition
{
/// <summary>
/// What action should be taken if this condition is met?
/// Defaults to <see cref="ConditionAction.Next"/>.
/// </summary>
[DataField]
public ConditionAction Action { get; set; } = ConditionAction.Next;
}
/// <summary>
/// Determines what action should be taken if a condition is met.
/// </summary>
public enum ConditionAction
{
/// <summary>
/// The player is allowed to join, and the next conditions will be skipped.
/// </summary>
Allow,
/// <summary>
/// The player is denied to join, and the next conditions will be skipped.
/// </summary>
Deny,
/// <summary>
/// The next condition should be checked.
/// </summary>
Next
}

View File

@@ -0,0 +1,42 @@
using System.Threading.Tasks;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Server.Connection.Whitelist;
/// <summary>
/// Used by the <see cref="ConnectionManager"/> to determine if a player should be allowed to join the server.
/// Used in the whitelist.prototype_list CVar.
///
/// Whitelists are used to determine if a player is allowed to connect.
/// You define a PlayerConnectionWhitelist with a list of conditions.
/// Every condition has a type and a <see cref="ConditionAction"/> along with other parameters depending on the type.
/// Action must either be Allow, Deny or Next.
/// Allow means the player is instantly allowed to connect if the condition is met.
/// Deny means the player is instantly denied to connect if the condition is met.
/// Next means the next condition in the list is checked.
/// If the condition doesn't match, the next condition is checked.
/// </summary>
[Prototype("playerConnectionWhitelist")]
public sealed class PlayerConnectionWhitelistPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = default!;
/// <summary>
/// Minimum number of players required for this whitelist to be active.
/// If there are less players than this, the whitelist will be ignored and the next one in the list will be used.
/// </summary>
[DataField]
public int MinimumPlayers { get; } = 0;
/// <summary>
/// Maximum number of players allowed for this whitelist to be active.
/// If there are more players than this, the whitelist will be ignored and the next one in the list will be used.
/// </summary>
[DataField]
public int MaximumPlayers { get; } = int.MaxValue;
[DataField]
public WhitelistCondition[] Conditions { get; } = default!;
}

View File

@@ -1066,6 +1066,29 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync();
}
public async Task<bool> GetBlacklistStatusAsync(NetUserId player)
{
await using var db = await GetDb();
return await db.DbContext.Blacklist.AnyAsync(w => w.UserId == player);
}
public async Task AddToBlacklistAsync(NetUserId player)
{
await using var db = await GetDb();
db.DbContext.Blacklist.Add(new Blacklist() { UserId = player });
await db.DbContext.SaveChangesAsync();
}
public async Task RemoveFromBlacklistAsync(NetUserId player)
{
await using var db = await GetDb();
var entry = await db.DbContext.Blacklist.SingleAsync(w => w.UserId == player);
db.DbContext.Blacklist.Remove(entry);
await db.DbContext.SaveChangesAsync();
}
#endregion
#region Uploaded Resources Logs

View File

@@ -244,6 +244,16 @@ namespace Content.Server.Database
#endregion
#region Blacklist
Task<bool> GetBlacklistStatusAsync(NetUserId player);
Task AddToBlacklistAsync(NetUserId player);
Task RemoveFromBlacklistAsync(NetUserId player);
#endregion
#region Uploaded Resources Logs
Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data);
@@ -740,6 +750,24 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.RemoveFromWhitelistAsync(player));
}
public Task<bool> GetBlacklistStatusAsync(NetUserId player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetBlacklistStatusAsync(player));
}
public Task AddToBlacklistAsync(NetUserId player)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddToBlacklistAsync(player));
}
public Task RemoveFromBlacklistAsync(NetUserId player)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.RemoveFromBlacklistAsync(player));
}
public Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data)
{
DbWriteOpsMetric.Inc();

View File

@@ -145,6 +145,7 @@ namespace Content.Server.Entry
IoCManager.Resolve<IGameMapManager>().Initialize();
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
IoCManager.Resolve<IBanManager>().Initialize();
IoCManager.Resolve<IConnectionManager>().PostInit();
}
}

View File

@@ -1326,24 +1326,12 @@ namespace Content.Shared.CCVar
/// </summary>
public static readonly CVarDef<bool> WhitelistEnabled =
CVarDef.Create("whitelist.enabled", false, CVar.SERVERONLY);
/// <summary>
/// The loc string to display as a disconnect reason when someone is not whitelisted.
/// Specifies the whitelist prototypes to be used by the server. This should be a comma-separated list of prototypes.
/// If a whitelists conditions to be active fail (for example player count), the next whitelist will be used instead. If no whitelist is valid, the player will be allowed to connect.
/// </summary>
public static readonly CVarDef<string> WhitelistReason =
CVarDef.Create("whitelist.reason", "whitelist-not-whitelisted", CVar.SERVERONLY);
/// <summary>
/// If the playercount is below this number, the whitelist will not apply.
/// </summary>
public static readonly CVarDef<int> WhitelistMinPlayers =
CVarDef.Create("whitelist.min_players", 0, CVar.SERVERONLY);
/// <summary>
/// If the playercount is above this number, the whitelist will not apply.
/// </summary>
public static readonly CVarDef<int> WhitelistMaxPlayers =
CVarDef.Create("whitelist.max_players", int.MaxValue, CVar.SERVERONLY);
public static readonly CVarDef<string> WhitelistPrototypeList =
CVarDef.Create("whitelist.prototype_list", "basicWhitelist", CVar.SERVERONLY);
/*
* VOTE

View File

@@ -1,7 +1,7 @@
# Configuration preset used on Wizard's Den Salamander
[game]
desc = "Official English Space Station 14 servers. Medium roleplay ruleset. You must be whitelisted through Discord to play if there are more than 15 online players."
desc = "Official English Space Station 14 servers. Medium roleplay ruleset. you must be whitelisted by playing on other Wizard's Den servers if there are more than 15 online players."
hostname = "[EN] Wizard's Den Salamander [US West RP]"
soft_max_players = 130
@@ -10,8 +10,7 @@ rules_file = "MRPRuleset"
[whitelist]
enabled = true
reason = "whitelist-not-whitelisted-rp"
min_players = 15
prototype_list = "salamanderMrpWhitelist"
[shuttle]
emergency_early_launch_allowed = true

View File

@@ -1,16 +1,4 @@
whitelist-not-whitelisted = You are not whitelisted.
# proper handling for having a min/max or not
whitelist-playercount-invalid = {$min ->
[0] The whitelist for this server only applies below {$max} players.
*[other] The whitelist for this server only applies above {$min} {$max ->
[2147483647] -> players, so you may be able to join later.
*[other] -> players and below {$max} players, so you may be able to join later.
}
}
whitelist-not-whitelisted-rp = You are not whitelisted. To become whitelisted, visit our Discord (which can be found at https://spacestation14.io) and check the #rp-whitelist channel.
cmd-whitelistadd-desc = Adds the player with the given username to the server whitelist.
cmd-whitelistadd-desc = Adds the player with the given username to the server whitelist.
cmd-whitelistadd-help = Usage: whitelistadd <username or User ID>
cmd-whitelistadd-existing = {$username} is already on the whitelist!
cmd-whitelistadd-added = {$username} added to the whitelist
@@ -40,8 +28,30 @@ panic-bunker-account-denied-reason = This server is in panic bunker mode, often
panic-bunker-account-reason-account = Your Space Station 14 account is too new. It must be older than {$minutes} minutes
panic-bunker-account-reason-overall = Your overall playtime on the server must be greater than {$minutes} $minutes
whitelist-playtime = You do not have enough playtime to join this server. You need at least {$minutes} minutes of playtime to join this server.
whitelist-player-count = This server is currently not accepting players. Please try again later.
whitelist-notes = You currently have too many admin notes to join this server. You can check your notes by typing /adminremarks in chat.
whitelist-manual = You are not whitelisted on this server.
whitelist-blacklisted = You are blacklisted from this server.
whitelist-always-deny = You are not allowed to join this server.
whitelist-fail-prefix = Not whitelisted: {$msg}
whitelist-misconfigured = The server is misconfigured and is not accepting players. Please contact the server owner and try again later.
cmd-blacklistadd-desc = Adds the player with the given username to the server blacklist.
cmd-blacklistadd-help = Usage: blacklistadd <username>
cmd-blacklistadd-existing = {$username} is already on the blacklist!
cmd-blacklistadd-added = {$username} added to the blacklist
cmd-blacklistadd-not-found = Unable to find '{$username}'
cmd-blacklistadd-arg-player = [player]
cmd-blacklistremove-desc = Removes the player with the given username from the server blacklist.
cmd-blacklistremove-help = Usage: blacklistremove <username>
cmd-blacklistremove-existing = {$username} is not on the blacklist!
cmd-blacklistremove-removed = {$username} removed from the blacklist
cmd-blacklistremove-not-found = Unable to find '{$username}'
cmd-blacklistremove-arg-player = [player]
baby-jail-account-denied = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun!
baby-jail-account-denied-reason = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun! Reason: "{$reason}"
baby-jail-account-reason-account = Your Space Station 14 account is too old. It must be younger than {$minutes} minutes
baby-jail-account-reason-overall = Your overall playtime on the server must be younger than {$minutes} $minutes

View File

@@ -0,0 +1,7 @@
- type: playerConnectionWhitelist
id: basicWhitelist # Basic whitelist using only the ManualWhitelist condition
conditions:
- !type:ConditionManualWhitelistMembership
action: Allow # Allow connection if matched
- !type:ConditionAlwaysMatch # Condition that always matches
action: Deny # Deny connection if matched

View File

@@ -0,0 +1,39 @@
# This is the whitelist used for Wizard's Den Salamander
- type: playerConnectionWhitelist
id: salamanderMrpWhitelist
conditions:
- !type:ConditionManualBlacklistMembership # Deny blacklisted (MRP ban)
action: Deny
- !type:ConditionNotesPlaytimeRange # Deny for high severity notes in the last 30 days
includeExpired: false
minimumSeverity: 3 # High
minimumNotes: 1
range: 30 # 30 days
action: Deny
includeSecret: false
- !type:ConditionNotesPlaytimeRange # Deny for >=2 medium severity notes in the last 14 days
includeExpired: false
minimumSeverity: 2 # Medium
minimumNotes: 1
range: 14 # 14 Days
action: Deny
includeSecret: false
- !type:ConditionNotesPlaytimeRange # Deny for >=3 low severity notes in the last 14 days
includeExpired: false
minimumSeverity: 1 # Low
minimumNotes: 3
range: 14 # 14 Days
action: Deny
includeSecret: false
- !type:ConditionManualWhitelistMembership # Allow whitelisted players
action: Allow
- !type:ConditionPlayerCount # Allow when <= 15 players are online
minimumPlayers: 0
maximumPlayers: 15
action: Allow
#- !type:ConditionPlaytime
# minimumPlaytime: 1200 # 20 hours to be whitelisted
# action: Deny
- !type:ConditionAlwaysMatch
action: Deny