kmph
2018-09-14 00:34

Kolejny wpis w temacie #mon .

Długo nie robiłem żadnych commitów. Zatrzymał mnie problem banowania. Naprawdę sądziłem, że to będzie coś koło 1 dnia roboty. Jednak wąskim gardłem okazał się mój brak wiedzy, jak działa framework.

Problem wydaje się być raczej prosty: jest chatbox w tej grze, chcę dać modom możliwość uciszania kont, które spamują chatbox głupotami. W tym celu przeczytałem odnośny tutorial z oficjalnej dokumentacji: Create an ASP.NET Core app with user data protected by authorization Otóż proponują tam, by do zadań związanych z uprawnieniami wykorzystać mechanizm ról. No to świetnie: daję role Mod oraz Muted, Mod może sobie wołać na ChatHubie metodę Mute, w której jest tego rodzaju:

var bannedUser = await userManager.FindByNameAsync(username);
if(bannedUser != null)
{
    await userManager.AddToRoleAsync(bannedUser, "Muted");
}

Natomiast metoda SendMessage jest obwarowana kodem podobnym do tego:

if(Context.User.IsInRole("Muted")
    return;

(Tak - to jest dużo prostszy kod niż ten, który proponowali w tutorialu. Po prostu kod z tutoriala, wraz z rejestrowaniem Authorization Handlers etc, strzelaniem z armaty do wróbla. Może jednak ichni kod jest bardziej "czysty", tym bardziej, że w przeciwieństwie do mojego nie używają tam magic stringów.)

Z powyższym kodem jest jeden bardzo poważny problem: nie działa. Mod sobie banuje, zbanowany dalej może pisać.

Na szczęście pisząc ten kod, miałem już za sobą napisaną obsługę "kont anonimowych". Dzięki temu wiedziałem, czemu to może nie działać. Inaczej chyba nie wiedziałbym, gdzie szukać problemu.

O co chodzi z tymi kontami anonimowymi? Ano spodobał mi się mechanizm z pokemonshowdown.com, gdzie nie trzeba się rejestrować, by móc grać: trzeba jedynie wybrać sobie nicka. Wtedy to konto jest "niezerejestrowane", każdy może przejąć tego nicka, ale bitwy są zapisywane w historii tego konta. Ale w każdej chwili można sobie nicka zarejestrować, dając mu własne hasło. Brak konieczności rejestracji mi się spodobał, znacznie obniża to barierę wejścia.

No to próbowałem sobie zaimplementować to. Również za pomocą ról. Gdy ktoś wybiera sobie nicka, tworzone jest konto o tej nazwie. Konto nie ma hasła. Gdy ktoś rejestruje to konto (zakłada mu hasło), wówczas wołamy:

var user = await _userManager.GetUserAsync(User);
var result = await _userManager.AddPasswordAsync(user, Input.Password);
if(result.Succeeded)
    await _userManager.AddToRoleAsync(user, "Registered")

I od tej pory czat przestanie tego nicka wyświetlać kursywą, zaś kto będzie chciał sobie wybrać tego nicka, ten będzie musiał podać hasło. W myśl wyżej wymienionego tutoriala, za każdym razem, gdy chcemy wiedzieć, czy konto jest zarejestrowane, czy nie (w tym na stronie logowania), wołamy User.IsInRole("Registered")

No i znowu - ten kod zdawał się nie działać. Rejestrowałem konto, a ono dalej było traktowane jako niezarejestrowane. User.IsInRole("Registered") zwracał false.

Zauważyłem jednak, że już wylogowanie się z dopiero co zarejestrowanego konta i zalogowanie się na nie ponownie naprawia problem. Wtedy konto jest traktowane jako zarejestrowane i User.IsInRole("Registered") zwraca true.

Naprawiłem więc kod rejestrujący konto, zdaje się on działać po tych zmianach:

var user = await _userManager.GetUserAsync(User);
var result = await _userManager.AddPasswordAsync(user, Input.Password);
if (result.Succeeded)
{
    var resultRole = await _userManager.AddToRoleAsync(user, "Registered");
    if(resultRole.Succeeded)
    {
        await _signInManager.SignOutAsync();
        await _signInManager.SignInAsync(user, false);
    }
}

Przypuszczałem więc, że z tym banowaniem jest podobnie. I rzeczywiście - gdy tylko wyloguję się ze zbanowanego konta i zaloguję się na nie ponownie, konto zaczyna być wreszcie traktowane jako uciszone i nie może wypowiadać się w czacie.

Ale tu już miałem poważniejszy problem. Nie widziałem w SignInManager metody, która pozwalałaby wylogować innego usera. Wołanie, tak jak w kodzie odpowiadającym za rejestrację signInManager.SignOutAsync() spowoduje wylogowanie się moda, a nie zbanowanego usera.

Pytanie na Stacku. Nawet pojawiły się 3 odpowiedzi. A więc:

  1. (Zdownvote'owana, nie przeze mnie): Na siłę wylogować zbanowanego usera. No chyba jednak nie tak to powinno działać, mimo wszystko. Ani Wikipedia, ani pokemonshowdown, nie wologowują na siłę userów, którym odebrany został dostęp do jakiejś cześci serwisu.
    2.: Link do odpowiedzi na inne pytanie. Problemy z tą odpowiedzia są 3: (a) Jest z 14 roku, a zdaje mi się, że technologie około.netowe mają zwyczaj robić breaking changes w publicznych API; (b) Jak sądzę, zakłada, że operujemy na tym samym userze, który wysyła żądanie - a więc o ile mod nie banuje siebie samego, to nic nie rozwiązuje; (c) Jeśli jednak (b) nie zachodzi, to odpowiedź ta robi coś dziwnego, znalezienie dokumentacji do tych zabaw może być niełatwe, a wolałbym nie robić naprawdę dziwnych rzeczy bez znalezienia ich w dokumentacji;
    3.: (odpowiedź pojawiła się później): Że separation of concerns, że authorization to nie authentication, że roles to ani jedno, ani drugie - no dobrze, ale w takim razie co zrobić?

Udałem się z tym do devów .NET Core. Chwała im za to, że są skłonni odpowiadać na pytania userów - więc jako ostateczność takie rozwiązanie może pozostać. A tu miałem jak "zahaczyć" - zgłosiłem issue do tego tutoriala dokumentacji, który pisał o IsInRole. Odpowiedź ich była taka, że należy użyć signinManager.RefreshSignIn. Tak też uczyniłem:

var bannedUser = await userManager.FindByNameAsync(username);
if(bannedUser != null)
{
    await userManager.AddToRoleAsync(bannedUser, "Muted");
    await signInManager.RefreshSignInAsync(bannedUser);
}

I... Znowu nie działa! Kiedy wołam w hubie SignalR, rzuca wyjątkami. Kiedy wołam na RazorPage, wydaje się nie mieć żadnego efektu. Ale moja issue, którą otworzyłem celem tego pytania, jest już zamknięta.

Zaczynam bać się nadużycia cierpliwości devów. No dobrze, muszę im zaprzeczyć, ale zrobić to tak, by nie zostać zignorowanym. Trzeba by mieć dobre argumenty. Tworzę prościutki projekt, który ma dowieść mojego punktu widzenia i otwieram kolejny issue. Tym razem wreszcie mam odpowiedź: Nie wołać User.IsInRole("Muted") (tj. ClaimsPrincipal.IsInRole), bo to będzie zwracać nieaktualne dane i devy nie mają zamiaru nic z tym robić, tylko wołać UserManager.IsInRoleAsync("Muted"). A zatem poprawiony (i wreszcie działający!) kod sprawdzania, czy user jest uciszony:

var user = await userManager.GetUserAsync(Context.User);
 if ((await userManager.GetRolesAsync(user)).Contains("Muted")) return;

UFF! Tyle czasu na coś tak (zdawałoby się) prostego! Zaczynam być rozeźlony.

A przede mną kolejne problemy, których rozwiązanie, obawiam się, może przebiegać podobną ścieżką:

  • Autozdejmowanie mute'a po dwóch godzinach? Spróbowałem zaimplementować za pomocą System.Timer ale... rzuca wyjątkami! Pewnie by się dało zaimplementować podobnie, jak timery do gry (tj. gdyby instancja klasy System.Timer była w jakimś Singletonie), no ale już nie chcę Singletonów.
  • Banowanie adresów IP. Bo przecież zmute'owanie konta nie ma sensu, jeśli gość może sobie zmienić nick (np. z Jerkass1 na Jerkass2) i dalej spamować czat.

Ja wiem - to pewnie wynika z mojej niedostatecznej znajomości frameworka. Ale zdumiewa mnie, ile czasu jest porzebne, by rozwiązać zdawałoby się najprostsze problemy.

grzesiek51114

Kiedy człowiek się uczy to często komplikuje proste rzeczy. Nie Ty pierwszy, nie ostatni. ;-)

tamtamtu

role sprawiaja sporo problemow - do skomplikowanych rzeczy lepiej ich nie uzywac - i nie, nie trzeba ich uzywac. Super nadaja sie tylko do prymitywnych przypadkow.

_Mateusz_

Co do zdejmowania banów możesz to zaimplementować jako background task przy pomocy: https://docs.microsoft.com/en[...]edservice?view=aspnetcore-2.1

WeiXiao

by rozwiązać zdawałoby się najprostsze problemy. hehe, a trzeba było ręcznie zaimplementować na ifach i by szło :P

Szalony Programista

Ify można skrócić w końcu to algebra boolowska, jedynie po za ffpga jest trudno zrównoleglić.

Szalony Programista

Czasem idzie skrócić te ify i wtedy człowiek jest zadowolony, że się do czegoś ta matematyka przydała.

WeiXiao

@Szalony Programista: ale nikt nie powiedział, że wyszłoby ich dużo :P