-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Search de-base64ed #74
Changes from 3 commits
bf70c27
c9ced13
76e491d
1fc4f96
1e5aa7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,8 @@ namespace ReplayBrowser.Helpers; | |
|
||
public class ReplayHelper | ||
{ | ||
static readonly string REDACTION_MESSAGE = "The account you are trying to search for is private or deleted. This might happen for various reasons as chosen by the account owner or the site administrative decision"; | ||
|
||
private readonly IMemoryCache _cache; | ||
private readonly ReplayDbContext _context; | ||
private readonly AccountService _accountService; | ||
|
@@ -63,40 +65,18 @@ public async Task<CollectedPlayerData> GetPlayerProfile(Guid playerGuid, Authent | |
{ | ||
var accountCaller = await _accountService.GetAccount(authenticationState); | ||
|
||
var isGdpr = _context.GdprRequests.Any(g => g.Guid == playerGuid); | ||
var isGdpr = await _context.GdprRequests.AnyAsync(g => g.Guid == playerGuid); | ||
if (isGdpr) | ||
{ | ||
throw new UnauthorizedAccessException("This account is protected by a GDPR request. There is no data available."); | ||
} | ||
throw new UnauthorizedAccessException(REDACTION_MESSAGE); | ||
|
||
var accountRequested = _context.Accounts | ||
.Include(a => a.Settings) | ||
.FirstOrDefault(a => a.Guid == playerGuid); | ||
|
||
if (!skipPermsCheck) | ||
{ | ||
if (accountRequested is { Settings.RedactInformation: true }) | ||
{ | ||
if (accountCaller == null || !accountCaller.IsAdmin) | ||
{ | ||
if (accountCaller?.Guid != playerGuid) | ||
{ | ||
if (accountRequested.Protected) | ||
{ | ||
throw new UnauthorizedAccessException("This account is protected and redacted. This might happens due to harassment or other reasons."); | ||
} | ||
else | ||
{ | ||
throw new UnauthorizedAccessException( | ||
"The account you are trying to view is private. Contact the account owner and ask them to make their account public."); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
CheckAccountAccess(caller: accountCaller, found: accountRequested); | ||
|
||
if (!skipPermsCheck) | ||
{ | ||
await _accountService.AddHistory(accountCaller, new HistoryEntry() | ||
{ | ||
Action = Enum.GetName(typeof(Action), Action.ProfileViewed) ?? "Unknown", | ||
|
@@ -271,37 +251,7 @@ public async Task<SearchResult> SearchReplays(List<SearchQueryItem> searchItems, | |
.Include(a => a.Settings) | ||
.FirstOrDefault(a => a.Username.ToLower().Equals(query.ToLower())); | ||
|
||
if (callerAccount != null) | ||
{ | ||
if (!callerAccount.Username.ToLower().Equals(query, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
if (foundOocAccount != null && foundOocAccount.Settings.RedactInformation) | ||
{ | ||
if (callerAccount == null || !callerAccount.IsAdmin) | ||
{ | ||
if (foundOocAccount.Protected) | ||
{ | ||
throw new UnauthorizedAccessException("This account is protected and redacted. This might happens due to harassment or other reasons."); | ||
} | ||
else | ||
{ | ||
throw new UnauthorizedAccessException("The account you are trying to search for is private. Contact the account owner and ask them to make their account public."); | ||
} | ||
} | ||
} | ||
} | ||
} else if (foundOocAccount != null && foundOocAccount.Settings.RedactInformation) | ||
{ | ||
if (foundOocAccount.Protected) | ||
{ | ||
throw new UnauthorizedAccessException("This account is protected and redacted. This might happens due to harassment or other reasons."); | ||
} | ||
else | ||
{ | ||
throw new UnauthorizedAccessException( | ||
"The account you are trying to search for is private. Contact the account owner and ask them to make their account public."); | ||
} | ||
} | ||
CheckAccountAccess(caller: callerAccount, found: foundOocAccount); | ||
} | ||
|
||
foreach (var searchQueryItem in searchItems.Where(x => x.SearchModeEnum == SearchMode.Guid)) | ||
|
@@ -310,52 +260,10 @@ public async Task<SearchResult> SearchReplays(List<SearchQueryItem> searchItems, | |
|
||
var foundGuidAccount = _context.Accounts | ||
.Include(a => a.Settings) | ||
// This .ToLower & .Contains trick allows for partially matching against a GUID | ||
.FirstOrDefault(a => a.Guid.ToString().ToLower().Contains(query.ToLower())); | ||
|
||
if (foundGuidAccount != null && foundGuidAccount.Settings.RedactInformation) | ||
{ | ||
if (callerAccount != null) | ||
{ | ||
if (callerAccount.Guid != foundGuidAccount.Guid) | ||
{ | ||
// if the requestor is not the found account and the requestor is not an admin, deny access | ||
if (callerAccount == null || !callerAccount.IsAdmin) | ||
{ | ||
if (foundGuidAccount.Protected) | ||
{ | ||
throw new UnauthorizedAccessException("This account is protected and redacted. This might happens due to harassment or other reasons."); | ||
} | ||
else | ||
{ | ||
throw new UnauthorizedAccessException( | ||
"The account you are trying to search for is private. Contact the account owner and ask them to make their account public."); | ||
} | ||
} | ||
} | ||
} else | ||
{ | ||
if (foundGuidAccount.Protected) | ||
{ | ||
throw new UnauthorizedAccessException("This account is protected and redacted. This might happens due to harassment or other reasons."); | ||
} | ||
else | ||
{ | ||
throw new UnauthorizedAccessException( | ||
"The account you are trying to search for is private. Contact the account owner and ask them to make their account public."); | ||
} | ||
} | ||
} else if (foundGuidAccount != null && foundGuidAccount.Settings.RedactInformation) | ||
{ | ||
if (foundGuidAccount.Protected) | ||
{ | ||
throw new UnauthorizedAccessException("This account is protected and redacted. This might happens due to harassment or other reasons."); | ||
} | ||
else | ||
{ | ||
throw new UnauthorizedAccessException( | ||
"The account you are trying to search for is private. Contact the account owner and ask them to make their account public."); | ||
} | ||
} | ||
CheckAccountAccess(caller: callerAccount, found: foundGuidAccount); | ||
} | ||
|
||
// "Execution of the current method continues before the call is completed" is a desired outcome here | ||
|
@@ -366,7 +274,7 @@ public async Task<SearchResult> SearchReplays(List<SearchQueryItem> searchItems, | |
{ | ||
Action = Enum.GetName(typeof(Action), Action.SearchPerformed) ?? "Unknown", | ||
Time = DateTime.UtcNow, | ||
Details = string.Join(", ", searchItems.Select(x => $"{x.SearchMode}={x.SearchValue}")) | ||
Details = string.Join(", ", searchItems.Select(x => $"{x.SearchModeEnum}={x.SearchValue}")) | ||
}); | ||
}); | ||
#pragma warning restore CS4014 | ||
|
@@ -384,6 +292,43 @@ public async Task<SearchResult> SearchReplays(List<SearchQueryItem> searchItems, | |
}; | ||
} | ||
|
||
/// <summary> | ||
/// Check whether the caller account (first arg) has access to view the found account (second arg) | ||
/// </summary> | ||
/// <remarks> | ||
/// I am really not a fan of two params of same type being used here. It can and probably will lead to confusing them around. | ||
/// TODO: Investigate what's the diff between <see cref="AccountSettings.RedactInformation"/> and <see cref="Account.Protected"/> | ||
/// </remarks> | ||
public static void CheckAccountAccess(Account? caller, Account? found) | ||
{ | ||
// There's no account to worry about yay | ||
if (found is null) | ||
return; | ||
|
||
// Is there any redaction to worry about? | ||
if (!found.Settings.RedactInformation && !found.Protected) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Protected accounts can still change their redaction settings and the only thing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW does that keep showing up even when redaction is on? Also need to eventually clarify in UI and code as to which combo deletes stuff and which just hides it from public view. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. It will keep showing up, even if your settings are changed. Only way to remove it is to edit the DB. |
||
return; | ||
// Ah shit | ||
|
||
// Not the person we're looking for | ||
if (caller is null) | ||
throw new UnauthorizedAccessException(REDACTION_MESSAGE); | ||
|
||
// Admins can see everything. Without this we could just peek into the DB. | ||
if (caller.IsAdmin) | ||
return; | ||
|
||
// Same person (or at least account), let them at it | ||
if (caller.Guid == found.Guid) | ||
return; | ||
|
||
// Catch-all | ||
// Don't give more info about why, what, just use a generic message for everything | ||
// For debugging you can always just check the logs or DB | ||
// Giving specific info like "admin" vs "self redacted" vs "GDPR request" | ||
throw new UnauthorizedAccessException(REDACTION_MESSAGE); | ||
} | ||
|
||
public async Task<PlayerData?> HasProfile(string username, AuthenticationState state) | ||
{ | ||
var accountGuid = AccountHelper.GetAccountGuid(state); | ||
|
@@ -431,7 +376,7 @@ public async Task<SearchResult> SearchReplays(List<SearchQueryItem> searchItems, | |
var stopWatch = new Stopwatch(); | ||
stopWatch.Start(); | ||
|
||
var cacheKey = $"{string.Join("-", searchItems.Select(x => $"{x.SearchMode}-{x.SearchValue}"))}"; | ||
var cacheKey = $"{string.Join("-", searchItems.Select(x => $"{x.SearchModeEnum}-{x.SearchValue}"))}"; | ||
|
||
var queryable = _context.Replays | ||
.AsNoTracking() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,25 @@ | ||
namespace ReplayBrowser.Models; | ||
using System.ComponentModel.DataAnnotations; | ||
|
||
namespace ReplayBrowser.Models; | ||
|
||
public enum SearchMode | ||
{ | ||
[Display(Name = "Map")] | ||
Map, | ||
[Display(Name = "Gamemode")] | ||
Gamemode, | ||
[Display(Name = "Server ID")] | ||
ServerId, | ||
[Display(Name = "Round End Text")] | ||
RoundEndText, | ||
[Display(Name = "Player IC Name")] | ||
PlayerIcName, | ||
[Display(Name = "Player OOC Name")] | ||
PlayerOocName, | ||
[Display(Name = "Player GUID")] | ||
Guid, | ||
[Display(Name = "Server Name")] | ||
ServerName, | ||
[Display(Name = "Round ID")] | ||
RoundId | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,85 @@ | ||
using System.Text.Json.Serialization; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace ReplayBrowser.Models; | ||
|
||
public class SearchQueryItem | ||
{ | ||
[JsonPropertyName("searchMode")] | ||
public required string SearchMode { get; set; } | ||
public string SearchMode | ||
{ | ||
set | ||
{ | ||
if (!ModeMapping.TryGetValue(value.ToLower(), out var mapped)) | ||
throw new ArgumentOutOfRangeException(); | ||
SearchModeEnum = mapped; | ||
} | ||
} | ||
[JsonPropertyName("searchValue")] | ||
public required string SearchValue { get; set; } | ||
[JsonIgnore] | ||
public SearchMode SearchModeEnum { get; set; } | ||
|
||
public SearchMode SearchModeEnum | ||
{ | ||
get | ||
public static List<SearchQueryItem> FromQuery(IQueryCollection query) { | ||
List<SearchQueryItem> result = []; | ||
// Yes this is fragile. No it won't really do anything but annoy people | ||
// Technically inefficient. In practice, meh | ||
// Too bad this collection isn't just a list of tuples | ||
var ordered = query.OrderBy(q => q.Key.Contains('[') ? int.Parse(q.Key[(q.Key.IndexOf('[') + 1)..q.Key.IndexOf(']')]) : int.MaxValue).ToList(); | ||
|
||
foreach (var item in ordered) | ||
{ | ||
return SearchMode switch | ||
{ | ||
"Map" => Models.SearchMode.Map, | ||
"Gamemode" => Models.SearchMode.Gamemode, | ||
"Server id" => Models.SearchMode.ServerId, | ||
"Round end text" => Models.SearchMode.RoundEndText, | ||
"Player ic name" => Models.SearchMode.PlayerIcName, | ||
"Player ooc name" => Models.SearchMode.PlayerOocName, | ||
"Guid" => Models.SearchMode.Guid, | ||
"Server name" => Models.SearchMode.ServerName, | ||
"Round id" => Models.SearchMode.RoundId, | ||
_ => throw new ArgumentOutOfRangeException() | ||
}; | ||
var index = item.Key.IndexOf('['); | ||
if (index != -1) | ||
result.AddRange(QueryValueParse(item.Key[..index], item.Value)); | ||
else | ||
result.AddRange(QueryValueParse(item.Key, item.Value)); | ||
} | ||
|
||
var legacyQuery = query["searches"]; | ||
if (legacyQuery.Count > 0 && legacyQuery[0]!.Length > 0) | ||
result.AddRange(FromQueryLegacy(legacyQuery[0]!)); | ||
|
||
return result; | ||
} | ||
|
||
public static List<SearchQueryItem> FromQueryLegacy(string searchesParam) { | ||
var decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(searchesParam)); | ||
return System.Text.Json.JsonSerializer.Deserialize<List<SearchQueryItem>>(decoded)!; | ||
} | ||
|
||
public static List<SearchQueryItem> QueryValueParse(string key, StringValues values) { | ||
if (!ModeMapping.TryGetValue(key, out var type)) | ||
return []; | ||
|
||
return values | ||
.Where(v => v is not null && v.Length > 0) | ||
.Select(v => new SearchQueryItem { SearchModeEnum = type, SearchValue = v! }) | ||
.ToList(); | ||
} | ||
|
||
public static string QueryName(SearchMode mode) | ||
=> ModeMapping.First(v => v.Value == mode).Key; | ||
|
||
// String values must be lowercase! | ||
// Be careful with changing any of the values here, as it can cause old searched to be invalid | ||
// For this reason, it's better to only add new entries | ||
public static readonly Dictionary<string, SearchMode> ModeMapping = new() { | ||
{ "guid", Models.SearchMode.Guid }, | ||
{ "username", Models.SearchMode.PlayerOocName }, | ||
{ "character", Models.SearchMode.PlayerIcName }, | ||
{ "server_id", Models.SearchMode.ServerId }, | ||
{ "server", Models.SearchMode.ServerName }, | ||
{ "round", Models.SearchMode.RoundId }, | ||
{ "map", Models.SearchMode.Map }, | ||
{ "gamemode", Models.SearchMode.Gamemode }, | ||
{ "endtext", Models.SearchMode.RoundEndText }, | ||
// Legacy | ||
{ "player ooc name", Models.SearchMode.PlayerOocName }, | ||
{ "player ic name", Models.SearchMode.PlayerIcName }, | ||
{ "server id", Models.SearchMode.ServerId }, | ||
{ "server name", Models.SearchMode.ServerName }, | ||
{ "round id", Models.SearchMode.RoundId }, | ||
{ "round end text", Models.SearchMode.RoundEndText }, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be made a const.