Skip to content

Commit 08bbe52

Browse files
authored
Disallow certain entities case-insensitively (#1084)
Disallowed usernames/asset hashes/email addresses/email domains are now blocked case-insensitively. Needs unit tests.
2 parents c15419a + 3072344 commit 08bbe52

13 files changed

Lines changed: 477 additions & 211 deletions

Refresh.Database/GameDatabaseContext.Assets.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,20 +111,30 @@ public void SetMainlinePhotoHash(GameAsset asset, string hash) =>
111111
asset.AsMainlinePhotoHash = hash;
112112
});
113113

114+
public bool IsAssetDisallowed(string hash)
115+
{
116+
string hashLower = hash.ToLower();
117+
return this.DisallowedAssets.Any(u => u.AssetHash == hashLower);
118+
}
119+
114120
public DisallowedAsset? GetDisallowedAssetInfo(string hash)
115-
=> this.DisallowedAssets.FirstOrDefault(d => d.AssetHash == hash);
121+
{
122+
string hashLower = hash.ToLower();
123+
return this.DisallowedAssets.FirstOrDefault(d => d.AssetHash == hashLower);
124+
}
116125

117126
/// <returns>
118127
/// The asset's disallowance info + whether the asset wasn't already disallowed before
119128
/// </returns
120129
public (DisallowedAsset, bool) DisallowAsset(string hash, GameAssetType type, string reason)
121130
{
122-
DisallowedAsset? existing = this.GetDisallowedAssetInfo(hash);
131+
string hashLower = hash.ToLower();
132+
DisallowedAsset? existing = this.GetDisallowedAssetInfo(hashLower);
123133
if (existing != null) return (existing, false);
124134

125135
DisallowedAsset disallowed = new()
126136
{
127-
AssetHash = hash,
137+
AssetHash = hashLower,
128138
AssetType = type,
129139
Reason = reason,
130140
DisallowedAt = this._time.Now,
@@ -137,7 +147,8 @@ public void SetMainlinePhotoHash(GameAsset asset, string hash) =>
137147

138148
public bool ReallowAsset(string hash)
139149
{
140-
DisallowedAsset? existing = this.GetDisallowedAssetInfo(hash);
150+
string hashLower = hash.ToLower();
151+
DisallowedAsset? existing = this.GetDisallowedAssetInfo(hashLower);
141152
if (existing == null) return false;
142153

143154
this.DisallowedAssets.Remove(existing);

Refresh.Database/GameDatabaseContext.Registration.cs

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -219,22 +219,29 @@ public void RemoveEmailVerificationCode(EmailVerificationCode code)
219219
}
220220

221221
public bool IsUserDisallowed(string username)
222-
=> this.DisallowedUsers.Any(u => u.Username == username);
222+
{
223+
string lowercaseUsername = username.ToLower();
224+
return this.DisallowedUsers.Any(u => u.UsernameLower == lowercaseUsername);
225+
}
223226

224227
public DisallowedUser? GetDisallowedUserInfo(string username)
225-
=> this.DisallowedUsers.FirstOrDefault(d => d.Username == username);
228+
{
229+
string lowercaseUsername = username.ToLower();
230+
return this.DisallowedUsers.FirstOrDefault(d => d.UsernameLower == lowercaseUsername);
231+
}
226232

227233
public DatabaseList<DisallowedUser> GetDisallowedUsers(int skip, int count)
228234
=> new(this.DisallowedUsers.OrderByDescending(d => d.DisallowedAt), skip, count);
229235

230236
public (DisallowedUser, bool) DisallowUser(string username, string reason)
231237
{
232-
DisallowedUser? existing = this.GetDisallowedUserInfo(username);
238+
string lowercaseUsername = username.ToLower();
239+
DisallowedUser? existing = this.GetDisallowedUserInfo(lowercaseUsername);
233240
if (existing != null) return (existing, false);
234241

235242
DisallowedUser disallowed = new()
236243
{
237-
Username = username,
244+
UsernameLower = lowercaseUsername,
238245
Reason = reason,
239246
DisallowedAt = this._time.Now,
240247
};
@@ -246,7 +253,8 @@ public DatabaseList<DisallowedUser> GetDisallowedUsers(int skip, int count)
246253

247254
public bool ReallowUser(string username)
248255
{
249-
DisallowedUser? disallowedUser = this.GetDisallowedUserInfo(username);
256+
string lowercaseUsername = username.ToLower();
257+
DisallowedUser? disallowedUser = this.GetDisallowedUserInfo(lowercaseUsername);
250258
if (disallowedUser == null)
251259
return false;
252260

@@ -257,22 +265,29 @@ public bool ReallowUser(string username)
257265
}
258266

259267
public bool IsEmailAddressDisallowed(string emailAddress)
260-
=> this.DisallowedEmailAddresses.Any(u => u.Address == emailAddress);
268+
{
269+
string emailAddressLower = emailAddress.ToLowerInvariant();
270+
return this.DisallowedEmailAddresses.Any(u => u.AddressLower == emailAddressLower);
271+
}
261272

262273
public DisallowedEmailAddress? GetDisallowedEmailAddressInfo(string emailAddress)
263-
=> this.DisallowedEmailAddresses.FirstOrDefault(d => d.Address == emailAddress);
274+
{
275+
string emailAddressLower = emailAddress.ToLowerInvariant();
276+
return this.DisallowedEmailAddresses.FirstOrDefault(d => d.AddressLower == emailAddressLower);
277+
}
264278

265279
public DatabaseList<DisallowedEmailAddress> GetDisallowedEmailAddresses(int skip, int count)
266280
=> new(this.DisallowedEmailAddresses.OrderByDescending(d => d.DisallowedAt), skip, count);
267281

268282
public (DisallowedEmailAddress, bool) DisallowEmailAddress(string emailAddress, string reason)
269283
{
270-
DisallowedEmailAddress? existing = this.GetDisallowedEmailAddressInfo(emailAddress);
284+
string emailAddressLower = emailAddress.ToLowerInvariant();
285+
DisallowedEmailAddress? existing = this.GetDisallowedEmailAddressInfo(emailAddressLower);
271286
if (existing != null) return (existing, false);
272287

273288
DisallowedEmailAddress disallowed = new()
274289
{
275-
Address = emailAddress,
290+
AddressLower = emailAddressLower,
276291
Reason = reason,
277292
DisallowedAt = this._time.Now,
278293
};
@@ -284,7 +299,8 @@ public DatabaseList<DisallowedEmailAddress> GetDisallowedEmailAddresses(int skip
284299

285300
public bool ReallowEmailAddress(string emailAddress)
286301
{
287-
DisallowedEmailAddress? disallowed = this.GetDisallowedEmailAddressInfo(emailAddress);
302+
string emailAddressLower = emailAddress.ToLowerInvariant();
303+
DisallowedEmailAddress? disallowed = this.GetDisallowedEmailAddressInfo(emailAddressLower);
288304
if (disallowed == null)
289305
return false;
290306

@@ -294,33 +310,33 @@ public bool ReallowEmailAddress(string emailAddress)
294310
return true;
295311
}
296312

297-
private string GetEmailDomainFromAddress(string emailAddress)
298-
=> emailAddress.Split('@').Last();
313+
private string GetLowercaseEmailDomainFromAddress(string emailAddress)
314+
=> emailAddress.Split('@').Last().ToLowerInvariant();
299315

300316
public bool IsEmailDomainDisallowed(string emailAddress)
301317
{
302-
string emailDomain = this.GetEmailDomainFromAddress(emailAddress);
303-
return this.DisallowedEmailDomains.Any(u => u.Domain == emailDomain);
318+
string emailDomainLower = this.GetLowercaseEmailDomainFromAddress(emailAddress);
319+
return this.DisallowedEmailDomains.Any(u => u.DomainLower == emailDomainLower);
304320
}
305321

306322
public DisallowedEmailDomain? GetDisallowedEmailDomainInfo(string emailAddress)
307323
{
308-
string emailDomain = this.GetEmailDomainFromAddress(emailAddress);
309-
return this.DisallowedEmailDomains.FirstOrDefault(d => d.Domain == emailDomain);
324+
string emailDomainLower = this.GetLowercaseEmailDomainFromAddress(emailAddress);
325+
return this.DisallowedEmailDomains.FirstOrDefault(d => d.DomainLower == emailDomainLower);
310326
}
311327

312328
public DatabaseList<DisallowedEmailDomain> GetDisallowedEmailDomains(int skip, int count)
313329
=> new(this.DisallowedEmailDomains.OrderByDescending(d => d.DisallowedAt), skip, count);
314330

315331
public (DisallowedEmailDomain, bool) DisallowEmailDomain(string emailAddress, string reason)
316332
{
317-
string emailDomain = this.GetEmailDomainFromAddress(emailAddress);
318-
DisallowedEmailDomain? existing = this.GetDisallowedEmailDomainInfo(emailDomain);
333+
string emailDomainLower = this.GetLowercaseEmailDomainFromAddress(emailAddress);
334+
DisallowedEmailDomain? existing = this.GetDisallowedEmailDomainInfo(emailDomainLower);
319335
if (existing != null) return (existing, false);
320336

321337
DisallowedEmailDomain disallowed = new()
322338
{
323-
Domain = emailDomain,
339+
DomainLower = emailDomainLower,
324340
Reason = reason,
325341
DisallowedAt = this._time.Now,
326342
};
@@ -332,8 +348,8 @@ public DatabaseList<DisallowedEmailDomain> GetDisallowedEmailDomains(int skip, i
332348

333349
public bool ReallowEmailDomain(string emailAddress)
334350
{
335-
string emailDomain = this.GetEmailDomainFromAddress(emailAddress);
336-
DisallowedEmailDomain? disallowedDomain = this.GetDisallowedEmailDomainInfo(emailDomain);
351+
string emailDomainLower = this.GetLowercaseEmailDomainFromAddress(emailAddress);
352+
DisallowedEmailDomain? disallowedDomain = this.GetDisallowedEmailDomainInfo(emailDomainLower);
337353
if (disallowedDomain == null)
338354
return false;
339355

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
4+
#nullable disable
5+
6+
namespace Refresh.Database.Migrations
7+
{
8+
/// <inheritdoc />
9+
[DbContext(typeof(GameDatabaseContext))]
10+
[Migration("20260527172459_DisallowEntitiesCaseInsensitively")]
11+
public partial class DisallowEntitiesCaseInsensitively : Migration
12+
{
13+
/// <inheritdoc />
14+
protected override void Up(MigrationBuilder migrationBuilder)
15+
{
16+
// Remove case-insensitively duplicate entries before lowercasing primary keys to not cause duplicate keys
17+
// Email Addresses
18+
migrationBuilder.Sql
19+
("""
20+
DELETE FROM "DisallowedEmailAddresses"
21+
WHERE "Address" NOT IN (
22+
SELECT min("Address")
23+
FROM "DisallowedEmailAddresses"
24+
GROUP BY lower("Address")
25+
)
26+
""");
27+
// for some reason, Postgres won't actually execute these separately if we use semicolons, so we have to do separate method calls
28+
migrationBuilder.Sql
29+
("""
30+
UPDATE "DisallowedEmailAddresses" SET "Address" = lower("Address");
31+
""");
32+
33+
// Email Domains
34+
migrationBuilder.Sql
35+
("""
36+
DELETE FROM "DisallowedEmailDomains"
37+
WHERE "Domain" NOT IN (
38+
SELECT min("Domain")
39+
FROM "DisallowedEmailDomains"
40+
GROUP BY lower("Domain")
41+
)
42+
""");
43+
migrationBuilder.Sql
44+
("""
45+
UPDATE "DisallowedEmailDomains" SET "Domain" = lower("Domain");
46+
""");
47+
48+
// Usernames
49+
migrationBuilder.Sql
50+
("""
51+
DELETE FROM "DisallowedUsers"
52+
WHERE "Username" NOT IN (
53+
SELECT min("Username")
54+
FROM "DisallowedUsers"
55+
GROUP BY lower("Username")
56+
)
57+
""");
58+
migrationBuilder.Sql
59+
("""
60+
UPDATE "DisallowedUsers" SET "Username" = lower("Username");
61+
""");
62+
63+
// Assets
64+
migrationBuilder.Sql
65+
("""
66+
DELETE FROM "DisallowedAssets"
67+
WHERE "AssetHash" NOT IN (
68+
SELECT min("AssetHash")
69+
FROM "DisallowedAssets"
70+
GROUP BY lower("AssetHash")
71+
)
72+
""");
73+
migrationBuilder.Sql
74+
("""
75+
UPDATE "DisallowedAssets" SET "AssetHash" = lower("AssetHash");
76+
""");
77+
}
78+
79+
/// <inheritdoc />
80+
protected override void Down(MigrationBuilder migrationBuilder)
81+
{
82+
83+
}
84+
}
85+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
4+
#nullable disable
5+
6+
namespace Refresh.Database.Migrations
7+
{
8+
/// <inheritdoc />
9+
[DbContext(typeof(GameDatabaseContext))]
10+
[Migration("20260606101615_RenameDisallowancePKsForClarity")]
11+
public partial class RenameDisallowancePKsForClarity : Migration
12+
{
13+
/// <inheritdoc />
14+
protected override void Up(MigrationBuilder migrationBuilder)
15+
{
16+
migrationBuilder.RenameColumn(
17+
name: "Username",
18+
table: "DisallowedUsers",
19+
newName: "UsernameLower");
20+
21+
migrationBuilder.RenameColumn(
22+
name: "Domain",
23+
table: "DisallowedEmailDomains",
24+
newName: "DomainLower");
25+
26+
migrationBuilder.RenameColumn(
27+
name: "Address",
28+
table: "DisallowedEmailAddresses",
29+
newName: "AddressLower");
30+
}
31+
32+
/// <inheritdoc />
33+
protected override void Down(MigrationBuilder migrationBuilder)
34+
{
35+
migrationBuilder.RenameColumn(
36+
name: "UsernameLower",
37+
table: "DisallowedUsers",
38+
newName: "Username");
39+
40+
migrationBuilder.RenameColumn(
41+
name: "DomainLower",
42+
table: "DisallowedEmailDomains",
43+
newName: "Domain");
44+
45+
migrationBuilder.RenameColumn(
46+
name: "AddressLower",
47+
table: "DisallowedEmailAddresses",
48+
newName: "Address");
49+
}
50+
}
51+
}

Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,7 +1532,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
15321532

15331533
modelBuilder.Entity("Refresh.Database.Models.Users.DisallowedEmailAddress", b =>
15341534
{
1535-
b.Property<string>("Address")
1535+
b.Property<string>("AddressLower")
15361536
.HasColumnType("text");
15371537

15381538
b.Property<DateTimeOffset>("DisallowedAt")
@@ -1541,14 +1541,14 @@ protected override void BuildModel(ModelBuilder modelBuilder)
15411541
b.Property<string>("Reason")
15421542
.HasColumnType("text");
15431543

1544-
b.HasKey("Address");
1544+
b.HasKey("AddressLower");
15451545

15461546
b.ToTable("DisallowedEmailAddresses");
15471547
});
15481548

15491549
modelBuilder.Entity("Refresh.Database.Models.Users.DisallowedEmailDomain", b =>
15501550
{
1551-
b.Property<string>("Domain")
1551+
b.Property<string>("DomainLower")
15521552
.HasColumnType("text");
15531553

15541554
b.Property<DateTimeOffset>("DisallowedAt")
@@ -1557,14 +1557,14 @@ protected override void BuildModel(ModelBuilder modelBuilder)
15571557
b.Property<string>("Reason")
15581558
.HasColumnType("text");
15591559

1560-
b.HasKey("Domain");
1560+
b.HasKey("DomainLower");
15611561

15621562
b.ToTable("DisallowedEmailDomains");
15631563
});
15641564

15651565
modelBuilder.Entity("Refresh.Database.Models.Users.DisallowedUser", b =>
15661566
{
1567-
b.Property<string>("Username")
1567+
b.Property<string>("UsernameLower")
15681568
.HasColumnType("text");
15691569

15701570
b.Property<DateTimeOffset>("DisallowedAt")
@@ -1573,7 +1573,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
15731573
b.Property<string>("Reason")
15741574
.HasColumnType("text");
15751575

1576-
b.HasKey("Username");
1576+
b.HasKey("UsernameLower");
15771577

15781578
b.ToTable("DisallowedUsers");
15791579
});
@@ -1602,12 +1602,12 @@ protected override void BuildModel(ModelBuilder modelBuilder)
16021602
b.Property<byte>("Entity")
16031603
.HasColumnType("smallint");
16041604

1605-
b.Property<int>("UploadCount")
1606-
.HasColumnType("integer");
1607-
16081605
b.Property<DateTimeOffset>("ExpiryDate")
16091606
.HasColumnType("timestamp with time zone");
16101607

1608+
b.Property<int>("UploadCount")
1609+
.HasColumnType("integer");
1610+
16111611
b.HasKey("UserId", "Entity");
16121612

16131613
b.ToTable("EntityUploadRateLimits");

0 commit comments

Comments
 (0)