Server ban exemption system (#15076)

This commit is contained in:
Pieter-Jan Briers
2023-04-03 02:24:55 +02:00
committed by GitHub
parent e037d12899
commit c8e90e561b
26 changed files with 8681 additions and 135 deletions

View File

@@ -12,12 +12,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="6.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="7.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.3" />
<!-- Necessary at design time -->
<PackageReference Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.4" Condition="'$(UseSystemSqlite)' != 'True' and '$(Configuration)' != 'Release'" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" Condition="'$(UseSystemSqlite)' != 'True' and '$(Configuration)' != 'Release'" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,9 @@
using Microsoft.EntityFrameworkCore;
#if TOOLS
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using SQLitePCL;
// ReSharper disable UnusedType.Global
namespace Content.Server.Database;
@@ -18,8 +22,14 @@ public sealed class DesignTimeContextFactorySqlite : IDesignTimeDbContextFactory
{
public SqliteServerDbContext CreateDbContext(string[] args)
{
#if !USE_SYSTEM_SQLITE
raw.SetProvider(new SQLite3Provider_e_sqlite3());
#endif
var optionsBuilder = new DbContextOptionsBuilder<SqliteServerDbContext>();
optionsBuilder.UseSqlite("Data Source=:memory:");
return new SqliteServerDbContext(optionsBuilder.Options);
}
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
public partial class ProfileTraitIndexUnique : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_trait_profile_id",
table: "trait");
migrationBuilder.CreateIndex(
name: "IX_trait_profile_id_trait_name",
table: "trait",
columns: new[] { "profile_id", "trait_name" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_trait_profile_id_trait_name",
table: "trait");
migrationBuilder.CreateIndex(
name: "IX_trait_profile_id",
table: "trait",
column: "profile_id");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
public partial class ServerBanExemption : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "exempt_flags",
table: "server_ban",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "server_ban_exemption",
columns: table => new
{
user_id = table.Column<Guid>(type: "uuid", nullable: false),
flags = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_server_ban_exemption", x => x.user_id);
table.CheckConstraint("FlagsNotZero", "flags != 0");
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "server_ban_exemption");
migrationBuilder.DropColumn(
name: "exempt_flags",
table: "server_ban");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class BanAutoDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "auto_delete",
table: "server_ban",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "auto_delete",
table: "server_ban");
}
}
}

View File

@@ -19,7 +19,7 @@ namespace Content.Server.Database.Migrations.Postgres
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.5")
.HasAnnotation("ProductVersion", "7.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -302,8 +302,7 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasKey("Id")
.HasName("PK_admin_rank_flag");
b.HasIndex("AdminRankId")
.HasDatabaseName("IX_admin_rank_flag_admin_rank_id");
b.HasIndex("AdminRankId");
b.HasIndex("Flag", "AdminRankId")
.IsUnique();
@@ -408,9 +407,10 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasIndex("UserId");
b.ToTable("connection_log", (string)null);
b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
b.ToTable("connection_log", null, t =>
{
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
});
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
@@ -438,8 +438,7 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasKey("Id")
.HasName("PK_job");
b.HasIndex("ProfileId")
.HasDatabaseName("IX_job_profile_id");
b.HasIndex("ProfileId");
b.HasIndex("ProfileId", "JobName")
.IsUnique();
@@ -451,6 +450,37 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("job", (string)null);
});
modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("play_time_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Guid>("PlayerId")
.HasColumnType("uuid")
.HasColumnName("player_id");
b.Property<TimeSpan>("TimeSpent")
.HasColumnType("interval")
.HasColumnName("time_spent");
b.Property<string>("Tracker")
.IsRequired()
.HasColumnType("text")
.HasColumnName("tracker");
b.HasKey("Id")
.HasName("PK_play_time");
b.HasIndex("PlayerId", "Tracker")
.IsUnique();
b.ToTable("play_time", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.Property<int>("Id")
@@ -501,40 +531,10 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasIndex("UserId")
.IsUnique();
b.ToTable("player", (string)null);
b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
});
modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("play_time_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Guid>("PlayerId")
.HasColumnType("uuid")
.HasColumnName("player_id");
b.Property<TimeSpan>("TimeSpent")
.HasColumnType("interval")
.HasColumnName("time_spent");
b.Property<string>("Tracker")
.IsRequired()
.HasColumnType("text")
.HasColumnName("tracker");
b.HasKey("Id")
.HasName("PK_play_time");
b.HasIndex("PlayerId", "Tracker")
.IsUnique();
b.ToTable("play_time", (string)null);
b.ToTable("player", null, t =>
{
t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
});
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
@@ -729,6 +729,10 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("inet")
.HasColumnName("address");
b.Property<bool>("AutoDelete")
.HasColumnType("boolean")
.HasColumnName("auto_delete");
b.Property<DateTime>("BanTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("ban_time");
@@ -737,6 +741,10 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("uuid")
.HasColumnName("banning_admin");
b.Property<int>("ExemptFlags")
.HasColumnType("integer")
.HasColumnName("exempt_flags");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
@@ -761,11 +769,32 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasIndex("UserId");
b.ToTable("server_ban", (string)null);
b.ToTable("server_ban", null, t =>
{
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
});
});
b.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("user_id");
b.Property<int>("Flags")
.HasColumnType("integer")
.HasColumnName("flags");
b.HasKey("UserId")
.HasName("PK_server_ban_exemption");
b.ToTable("server_ban_exemption", null, t =>
{
t.HasCheckConstraint("FlagsNotZero", "flags != 0");
});
});
modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
@@ -847,11 +876,12 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasIndex("UserId");
b.ToTable("server_role_ban", (string)null);
b.ToTable("server_role_ban", null, t =>
{
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
b.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
});
});
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
@@ -935,8 +965,8 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasKey("Id")
.HasName("PK_trait");
b.HasIndex("ProfileId")
.HasDatabaseName("IX_trait_profile_id");
b.HasIndex("ProfileId", "TraitName")
.IsUnique();
b.ToTable("trait", (string)null);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
public partial class ProfileTraitIndexUnique : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_trait_profile_id",
table: "trait");
migrationBuilder.CreateIndex(
name: "IX_trait_profile_id_trait_name",
table: "trait",
columns: new[] { "profile_id", "trait_name" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_trait_profile_id_trait_name",
table: "trait");
migrationBuilder.CreateIndex(
name: "IX_trait_profile_id",
table: "trait",
column: "profile_id");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
public partial class ServerBanExemption : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "exempt_flags",
table: "server_ban",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "server_ban_exemption",
columns: table => new
{
user_id = table.Column<Guid>(type: "TEXT", nullable: false),
flags = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_server_ban_exemption", x => x.user_id);
table.CheckConstraint("FlagsNotZero", "flags != 0");
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "server_ban_exemption");
migrationBuilder.DropColumn(
name: "exempt_flags",
table: "server_ban");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class BanAutoDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "auto_delete",
table: "server_ban",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "auto_delete",
table: "server_ban");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Content.Server.Database.Migrations.Sqlite
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.5");
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
@@ -278,8 +278,7 @@ namespace Content.Server.Database.Migrations.Sqlite
b.HasKey("Id")
.HasName("PK_admin_rank_flag");
b.HasIndex("AdminRankId")
.HasDatabaseName("IX_admin_rank_flag_admin_rank_id");
b.HasIndex("AdminRankId");
b.HasIndex("Flag", "AdminRankId")
.IsUnique();
@@ -404,8 +403,7 @@ namespace Content.Server.Database.Migrations.Sqlite
b.HasKey("Id")
.HasName("PK_job");
b.HasIndex("ProfileId")
.HasDatabaseName("IX_job_profile_id");
b.HasIndex("ProfileId");
b.HasIndex("ProfileId", "JobName")
.IsUnique();
@@ -417,6 +415,35 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("job", (string)null);
});
modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("play_time_id");
b.Property<Guid>("PlayerId")
.HasColumnType("TEXT")
.HasColumnName("player_id");
b.Property<TimeSpan>("TimeSpent")
.HasColumnType("TEXT")
.HasColumnName("time_spent");
b.Property<string>("Tracker")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("tracker");
b.HasKey("Id")
.HasName("PK_play_time");
b.HasIndex("PlayerId", "Tracker")
.IsUnique();
b.ToTable("play_time", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.Property<int>("Id")
@@ -468,35 +495,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("player", (string)null);
});
modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("play_time_id");
b.Property<Guid>("PlayerId")
.HasColumnType("TEXT")
.HasColumnName("player_id");
b.Property<TimeSpan>("TimeSpent")
.HasColumnType("TEXT")
.HasColumnName("time_spent");
b.Property<string>("Tracker")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("tracker");
b.HasKey("Id")
.HasName("PK_play_time");
b.HasIndex("PlayerId", "Tracker")
.IsUnique();
b.ToTable("play_time", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
{
b.Property<int>("Id")
@@ -679,6 +677,10 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("address");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER")
.HasColumnName("auto_delete");
b.Property<DateTime>("BanTime")
.HasColumnType("TEXT")
.HasColumnName("ban_time");
@@ -687,6 +689,10 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("banning_admin");
b.Property<int>("ExemptFlags")
.HasColumnType("INTEGER")
.HasColumnName("exempt_flags");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
@@ -711,9 +717,30 @@ namespace Content.Server.Database.Migrations.Sqlite
b.HasIndex("UserId");
b.ToTable("server_ban", (string)null);
b.ToTable("server_ban", null, t =>
{
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
});
});
b.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.Property<int>("Flags")
.HasColumnType("INTEGER")
.HasColumnName("flags");
b.HasKey("UserId")
.HasName("PK_server_ban_exemption");
b.ToTable("server_ban_exemption", null, t =>
{
t.HasCheckConstraint("FlagsNotZero", "flags != 0");
});
});
modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
@@ -791,9 +818,10 @@ namespace Content.Server.Database.Migrations.Sqlite
b.HasIndex("UserId");
b.ToTable("server_role_ban", (string)null);
b.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
b.ToTable("server_role_ban", null, t =>
{
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
});
});
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
@@ -871,8 +899,8 @@ namespace Content.Server.Database.Migrations.Sqlite
b.HasKey("Id")
.HasName("PK_trait");
b.HasIndex("ProfileId")
.HasDatabaseName("IX_trait_profile_id");
b.HasIndex("ProfileId", "TraitName")
.IsUnique();
b.ToTable("trait", (string)null);
});

View File

@@ -29,6 +29,7 @@ namespace Content.Server.Database
public DbSet<Whitelist> Whitelist { get; set; } = null!;
public DbSet<ServerBan> Ban { get; set; } = default!;
public DbSet<ServerUnban> Unban { get; set; } = default!;
public DbSet<ServerBanExemption> BanExemption { get; set; } = default!;
public DbSet<ConnectionLog> ConnectionLog { get; set; } = default!;
public DbSet<ServerBanHit> ServerBanHit { get; set; } = default!;
public DbSet<ServerRoleBan> RoleBan { get; set; } = default!;
@@ -125,8 +126,13 @@ namespace Content.Server.Database
.HasIndex(p => p.BanId)
.IsUnique();
modelBuilder.Entity<ServerBan>()
.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
modelBuilder.Entity<ServerBan>().ToTable(t =>
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL"));
// Ban exemption can't have flags 0 since that wouldn't exempt anything.
// The row should be removed if setting to 0.
modelBuilder.Entity<ServerBanExemption>().ToTable(t =>
t.HasCheckConstraint("FlagsNotZero", "flags != 0"));
modelBuilder.Entity<ServerRoleBan>()
.HasIndex(p => p.UserId);
@@ -141,8 +147,8 @@ namespace Content.Server.Database
.HasIndex(p => p.BanId)
.IsUnique();
modelBuilder.Entity<ServerRoleBan>()
.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
modelBuilder.Entity<ServerRoleBan>().ToTable(t =>
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL"));
modelBuilder.Entity<Player>()
.HasIndex(p => p.UserId)
@@ -440,39 +446,148 @@ namespace Content.Server.Database
DateTime UnbanTime { get; set; }
}
/// <summary>
/// Flags for use with <see cref="ServerBanExemption"/>.
/// </summary>
[Flags]
public enum ServerBanExemptFlags
{
// @formatter:off
None = 0,
/// <summary>
/// Ban is a datacenter range, connections usually imply usage of a VPN service.
/// </summary>
Datacenter = 1 << 0,
// @formatter:on
}
/// <summary>
/// A ban from playing on the server.
/// If an incoming connection matches any of UserID, IP, or HWID, they will be blocked from joining the server.
/// </summary>
/// <remarks>
/// At least one of UserID, IP, or HWID must be given (otherwise the ban would match nothing).
/// </remarks>
[Table("server_ban")]
public class ServerBan : IBanCommon<ServerUnban>
{
public int Id { get; set; }
/// <summary>
/// The user ID of the banned player.
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// CIDR IP address range of the ban. The whole range can match the ban.
/// </summary>
[Column(TypeName = "inet")] public (IPAddress, int)? Address { get; set; }
/// <summary>
/// Hardware ID of the banned player.
/// </summary>
public byte[]? HWId { get; set; }
/// <summary>
/// The time when the ban was applied by an administrator.
/// </summary>
public DateTime BanTime { get; set; }
/// <summary>
/// The time the ban will expire. If null, the ban is permanent and will not expire naturally.
/// </summary>
public DateTime? ExpirationTime { get; set; }
/// <summary>
/// The administrator-stated reason for applying the ban.
/// </summary>
public string Reason { get; set; } = null!;
/// <summary>
/// User ID of the admin that applied the ban.
/// </summary>
public Guid? BanningAdmin { get; set; }
/// <summary>
/// Optional flags that allow adding exemptions to the ban via <see cref="ServerBanExemption"/>.
/// </summary>
public ServerBanExemptFlags ExemptFlags { get; set; }
/// <summary>
/// If present, an administrator has manually repealed this ban.
/// </summary>
public ServerUnban? Unban { get; set; }
/// <summary>
/// Whether this ban should be automatically deleted from the database when it expires.
/// </summary>
/// <remarks>
/// This isn't done automatically by the game,
/// you will need to set up something like a cron job to clear this from your database,
/// using a command like this:
/// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time &lt; NOW()"
/// </remarks>
public bool AutoDelete { get; set; }
public List<ServerBanHit> BanHits { get; set; } = null!;
}
/// <summary>
/// An explicit repeal of a <see cref="ServerBan"/> by an administrator.
/// Having an entry for a ban neutralizes it.
/// </summary>
[Table("server_unban")]
public class ServerUnban : IUnbanCommon
{
[Column("unban_id")] public int Id { get; set; }
/// <summary>
/// The ID of ban that is being repealed.
/// </summary>
public int BanId { get; set; }
/// <summary>
/// The ban that is being repealed.
/// </summary>
public ServerBan Ban { get; set; } = null!;
/// <summary>
/// The admin that repealed the ban.
/// </summary>
public Guid? UnbanningAdmin { get; set; }
/// <summary>
/// The time the ban repealed.
/// </summary>
public DateTime UnbanTime { get; set; }
}
/// <summary>
/// An exemption for a specific user to a certain type of <see cref="ServerBan"/>.
/// </summary>
/// <example>
/// Certain players may need to be exempted from VPN bans due to issues with their ISP.
/// We would tag all VPN bans with <see cref="ServerBanExemptFlags.Datacenter"/>,
/// and then add an exemption for these players to this table with the same flag.
/// They will only be exempted from VPN bans, other bans (if they manage to get any) will still apply.
/// </example>
[Table("server_ban_exemption")]
public sealed class ServerBanExemption
{
/// <summary>
/// The UserID of the exempted player.
/// </summary>
[Key]
public Guid UserId { get; set; }
/// <summary>
/// The ban flags to exempt this player from.
/// If any bit overlaps <see cref="ServerBan.ExemptFlags"/>, the ban is ignored.
/// </summary>
public ServerBanExemptFlags Flags { get; set; }
}
[Table("connection_log")]
public class ConnectionLog
{

View File

@@ -46,19 +46,20 @@ namespace Content.Server.Database
// ReSharper disable StringLiteralTypo
// Enforce that an address cannot be IPv6-mapped IPv4.
// So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes.
modelBuilder.Entity<ServerBan>()
.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
modelBuilder.Entity<ServerBan>().ToTable(t =>
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"));
modelBuilder.Entity<ServerRoleBan>()
.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
modelBuilder.Entity<ServerRoleBan>().ToTable( t =>
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"));
modelBuilder.Entity<Player>()
.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4",
"NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
modelBuilder.Entity<Player>().ToTable(t =>
t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4",
"NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"));
modelBuilder.Entity<ConnectionLog>().ToTable(t =>
t.HasCheckConstraint("AddressNotIPv6MappedIPv4",
"NOT inet '::ffff:0.0.0.0/96' >>= address"));
modelBuilder.Entity<ConnectionLog>()
.HasCheckConstraint("AddressNotIPv6MappedIPv4",
"NOT inet '::ffff:0.0.0.0/96' >>= address");
// ReSharper restore StringLiteralTypo
modelBuilder.Entity<AdminLog>()

View File

@@ -17,7 +17,7 @@ namespace Content.Server.Database
TypeMappingSourceDependencies dependencies,
RelationalTypeMappingSourceDependencies relationalDependencies,
ISqlGenerationHelper sqlGenerationHelper,
INpgsqlOptions? npgsqlOptions = null)
INpgsqlSingletonOptions npgsqlOptions)
: base(dependencies, relationalDependencies, sqlGenerationHelper, npgsqlOptions)
{
StoreTypeMappings["inet"] =

View File

@@ -200,12 +200,12 @@ namespace Content.Server.Database
return;
}
if (entityType.FindPrimaryKey() is IConventionKey primaryKey)
if (entityType.FindPrimaryKey() is { } primaryKey)
{
if (entityType.FindRowInternalForeignKeys(tableIdentifier).FirstOrDefault() is null
&& (entityType.BaseType is null || entityType.GetTableName() == entityType.BaseType.GetTableName()))
{
primaryKey.Builder.HasName(RewriteName(primaryKey.GetDefaultName()));
primaryKey.Builder.HasName(RewriteName(primaryKey.GetDefaultName()!));
}
else
{
@@ -215,16 +215,16 @@ namespace Content.Server.Database
foreach (var foreignKey in entityType.GetForeignKeys())
{
foreignKey.Builder.HasConstraintName(RewriteName(foreignKey.GetDefaultName()));
foreignKey.Builder.HasConstraintName(RewriteName(foreignKey.GetDefaultName()!));
}
foreach (var index in entityType.GetIndexes())
{
index.Builder.HasDatabaseName(RewriteName(index.GetDefaultDatabaseName()));
index.Builder.HasDatabaseName(RewriteName(index.GetDefaultDatabaseName()!));
}
if (annotation?.Value is not null
&& entityType.FindOwnership() is IConventionForeignKey ownership
&& entityType.FindOwnership() is { } ownership
&& (string)annotation.Value != ownership.PrincipalEntityType.GetTableName())
{
foreach (var property in entityType.GetProperties()
@@ -234,9 +234,9 @@ namespace Content.Server.Database
RewriteColumnName(property.Builder);
}
if (entityType.FindPrimaryKey() is IConventionKey key)
if (entityType.FindPrimaryKey() is { } key)
{
key.Builder.HasName(RewriteName(key.GetDefaultName()));
key.Builder.HasName(RewriteName(key.GetDefaultName()!));
}
}
}
@@ -245,7 +245,7 @@ namespace Content.Server.Database
IConventionForeignKeyBuilder relationshipBuilder,
IConventionContext<IConventionForeignKeyBuilder> context)
{
relationshipBuilder.HasConstraintName(RewriteName(relationshipBuilder.Metadata.GetDefaultName()));
relationshipBuilder.HasConstraintName(RewriteName(relationshipBuilder.Metadata.GetDefaultName()!));
}
public void ProcessKeyAdded(IConventionKeyBuilder keyBuilder, IConventionContext<IConventionKeyBuilder> context)
@@ -257,7 +257,7 @@ namespace Content.Server.Database
if (entityType.FindOwnership() is null)
{
keyBuilder.HasName(RewriteName(keyBuilder.Metadata.GetDefaultName()));
keyBuilder.HasName(RewriteName(keyBuilder.Metadata.GetDefaultName()!));
}
}
@@ -270,7 +270,7 @@ namespace Content.Server.Database
foreach (var property in entityType.GetProperties())
{
var columnName = property.GetColumnBaseName();
var columnName = property.GetColumnName();
if (columnName.StartsWith(entityType.ShortName() + '_', StringComparison.Ordinal))
{
property.Builder.HasColumnName(
@@ -310,11 +310,11 @@ namespace Content.Server.Database
var baseColumnName = StoreObjectIdentifier.Create(property.DeclaringEntityType, StoreObjectType.Table) is { } tableIdentifier
? property.GetDefaultColumnName(tableIdentifier)
: property.GetDefaultColumnBaseName();
: property.GetDefaultColumnName();
if (baseColumnName == "Id")
baseColumnName = entityType.GetTableName() + baseColumnName;
propertyBuilder.HasColumnName(RewriteName(baseColumnName));
propertyBuilder.HasColumnName(RewriteName(baseColumnName!));
foreach (var storeObjectType in _storeObjectTypes)
{

View File

@@ -0,0 +1,122 @@
using System.Linq;
using System.Text;
using Content.Server.Database;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class BanExemptionUpdateCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
public override string Command => "ban_exemption_update";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteError(LocalizationManager.GetString("cmd-ban_exemption_update-nargs"));
return;
}
var flags = ServerBanExemptFlags.None;
for (var i = 1; i < args.Length; i++)
{
var arg = args[i];
if (!Enum.TryParse<ServerBanExemptFlags>(arg, ignoreCase: true, out var flag))
{
shell.WriteError(LocalizationManager.GetString("cmd-ban_exemption_update-invalid-flag", ("flag", arg)));
return;
}
flags |= flag;
}
var player = args[0];
var playerData = await _playerLocator.LookupIdByNameOrIdAsync(player);
if (playerData == null)
{
shell.WriteError(LocalizationManager.GetString("cmd-ban_exemption_update-locate", ("player", player)));
return;
}
await _dbManager.UpdateBanExemption(playerData.UserId, flags);
shell.WriteLine(LocalizationManager.GetString(
"cmd-ban_exemption_update-success",
("player", player),
("uid", playerData.UserId)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHint(LocalizationManager.GetString("cmd-ban_exemption_get-arg-player"));
return CompletionResult.FromHintOptions(
Enum.GetNames<ServerBanExemptFlags>(),
LocalizationManager.GetString("cmd-ban_exemption_update-arg-flag"));
}
}
[AdminCommand(AdminFlags.Admin)]
public sealed class BanExemptionGetCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
public override string Command => "ban_exemption_get";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError(LocalizationManager.GetString("cmd-ban_exemption_get-nargs"));
return;
}
var player = args[0];
var playerData = await _playerLocator.LookupIdByNameOrIdAsync(player);
if (playerData == null)
{
shell.WriteError(LocalizationManager.GetString("cmd-ban_exemption_update-locate", ("player", player)));
return;
}
var flags = await _dbManager.GetBanExemption(playerData.UserId);
if (flags == ServerBanExemptFlags.None)
{
shell.WriteLine(LocalizationManager.GetString("cmd-ban_exemption_get-none"));
return;
}
var joined = new StringBuilder();
var first = true;
for (var i = 0; i < sizeof(ServerBanExemptFlags) * 8; i++)
{
var mask = (ServerBanExemptFlags) (1 << i);
if ((mask & flags) == 0)
break;
if (!first)
joined.Append(", ");
first = false;
joined.Append(mask.ToString());
}
shell.WriteLine(LocalizationManager.GetString(
"cmd-ban_exemption_get-show",
("flags", joined.ToString())));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHint(LocalizationManager.GetString("cmd-ban_exemption_get-arg-player"));
return CompletionResult.Empty;
}
}

View File

@@ -332,6 +332,52 @@ namespace Content.Server.Database
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban);
protected static async Task<ServerBanExemptFlags?> GetBanExemptionCore(DbGuard db, NetUserId? userId)
{
if (userId == null)
return null;
var exemption = await db.DbContext.BanExemption
.SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId);
return exemption?.Flags;
}
public async Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
{
await using var db = await GetDb();
if (flags == 0)
{
// Delete whatever is there.
await db.DbContext.BanExemption.Where(u => u.UserId == userId.UserId).ExecuteDeleteAsync();
return;
}
var exemption = await db.DbContext.BanExemption.SingleOrDefaultAsync(u => u.UserId == userId.UserId);
if (exemption == null)
{
exemption = new ServerBanExemption
{
UserId = userId
};
db.DbContext.BanExemption.Add(exemption);
}
exemption.Flags = flags;
await db.DbContext.SaveChangesAsync();
}
public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId)
{
await using var db = await GetDb();
var flags = await GetBanExemptionCore(db, userId);
return flags ?? ServerBanExemptFlags.None;
}
#endregion
#region Role Bans
@@ -985,6 +1031,5 @@ namespace Content.Server.Database
public abstract ValueTask DisposeAsync();
}
}
}

View File

@@ -84,6 +84,23 @@ namespace Content.Server.Database
Task AddServerBanAsync(ServerBanDef serverBan);
Task AddServerUnbanAsync(ServerUnbanDef serverBan);
/// <summary>
/// Update ban exemption information for a player.
/// </summary>
/// <remarks>
/// Database rows are automatically created and removed when appropriate.
/// </remarks>
/// <param name="userId">The user to update</param>
/// <param name="flags">The new ban exemption flags.</param>
Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags);
/// <summary>
/// Get current ban exemption flags for a user
/// </summary>
/// <returns><see cref="ServerBanExemptFlags.None"/> if the user is not exempt from any bans.</returns>
Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId);
#endregion
#region Role Bans
@@ -353,6 +370,18 @@ namespace Content.Server.Database
return _db.AddServerUnbanAsync(serverUnban);
}
public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
{
DbWriteOpsMetric.Inc();
return _db.UpdateBanExemption(userId, flags);
}
public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId)
{
DbReadOpsMetric.Inc();
return _db.GetBanExemption(userId);
}
#region Role Ban
public Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
@@ -742,10 +771,10 @@ namespace Content.Server.Database
return true;
}
public IDisposable BeginScope<TState>(TState state)
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
// TODO: this
return null!;
return null;
}
}
}

View File

@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Content.Server.Database
{
@@ -58,7 +59,8 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false)
var exempt = await GetBanExemptionCore(db, userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt)
.OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync();
@@ -77,7 +79,8 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned);
var exempt = await GetBanExemptionCore(db, userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt);
var queryBans = await query.ToArrayAsync();
var bans = new List<ServerBanDef>(queryBans.Length);
@@ -100,8 +103,11 @@ namespace Content.Server.Database
NetUserId? userId,
ImmutableArray<byte>? hwId,
DbGuardImpl db,
bool includeUnbanned)
bool includeUnbanned,
ServerBanExemptFlags? exemptFlags)
{
DebugTools.Assert(!(address == null && userId == null && hwId == null));
IQueryable<ServerBan>? query = null;
if (userId is { } uid)
@@ -131,14 +137,22 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
DebugTools.Assert(
query != null,
"At least one filter item (IP/UserID/HWID) must have been given to make query not null.");
if (!includeUnbanned)
{
query = query?.Where(p =>
query = query.Where(p =>
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now));
}
query = query!.Distinct();
return query;
if (exemptFlags is { } exempt)
{
query = query.Where(b => (b.ExemptFlags & exempt) == 0);
}
return query.Distinct();
}
private static ServerBanDef? ConvertBan(ServerBan? ban)

View File

@@ -68,9 +68,11 @@ namespace Content.Server.Database
{
await using var db = await GetDbImpl();
var exempt = await GetBanExemptionCore(db, userId);
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var bans = await GetAllBans(db.SqliteDbContext, includeUnbanned: false);
var bans = await GetAllBans(db.SqliteDbContext, includeUnbanned: false, exempt);
return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId)) is { } foundBan
? ConvertBan(foundBan)
@@ -83,9 +85,11 @@ namespace Content.Server.Database
{
await using var db = await GetDbImpl();
var exempt = await GetBanExemptionCore(db, userId);
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned);
var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt);
return queryBans
.Where(b => BanMatches(b, address, userId, hwId))
@@ -95,7 +99,8 @@ namespace Content.Server.Database
private static async Task<List<ServerBan>> GetAllBans(
SqliteServerDbContext db,
bool includeUnbanned)
bool includeUnbanned,
ServerBanExemptFlags? exemptFlags)
{
IQueryable<ServerBan> query = db.Ban.Include(p => p.Unban);
if (!includeUnbanned)
@@ -104,6 +109,11 @@ namespace Content.Server.Database
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow));
}
if (exemptFlags is { } exempt)
{
query = query.Where(b => (b.ExemptFlags & exempt) == 0);
}
return await query.ToListAsync();
}

View File

@@ -19,3 +19,23 @@ cmd-banlist-desc = Lists a user's active bans.
cmd-banlist-help = Usage: banlist <name or user ID>
cmd-banlist-empty = No active bans found for {$user}
cmd-banlistF-hint = <name/user ID>
cmd-ban_exemption_update-desc = Set an exemption to a type of ban on a player.
cmd-ban_exemption_update-help = Usage: ban_exemption_update <player> <flag> [<flag> [...]]
Specify multiple flags to give a player multiple ban exemption flags.
To remove all exemptions, run this command and give "None" as only flag.
cmd-ban_exemption_update-nargs = Expected at least 2 arguments
cmd-ban_exemption_update-locate = Unable to locate player '{$player}'.
cmd-ban_exemption_update-invalid-flag = Invalid flag '{$flag}'.
cmd-ban_exemption_update-success = Updated ban exemption flags for '{$player}' ({$uid}).
cmd-ban_exemption_update-arg-player = <player>
cmd-ban_exemption_update-arg-flag = <flag>
cmd-ban_exemption_get-desc = Show ban exemptions for a certain player.
cmd-ban_exemption_get-help = Usage: ban_exemption_get <player>
cmd-ban_exemption_get-nargs = Expected exactly 1 argument
cmd-ban_exemption_get-none = User is not exempt from any bans.
cmd-ban_exemption_get-show = User is exempt from the following ban flags: {$flags}.
cmd-ban_exemption_get-arg-player = <player>