ASP.NET Core MVC autoryzacja użytkowników w grupach

0

Cześć, robię apliację w ASP.NET CORE MVC. Aplikacja ta pozwala dobierać się użytkownikom w grupy i rozmawiać na forach w poszczególnych grupach. Do logowania i rejestracji wykorzystałem wbudowaną w Visual Studio opcję Authentication - Indiwidual User Account, którą wybiera się przy tworzeniu projektu. Do utworzenia grup stworzyłem 2 modele w EF Core i zmigrowałem je do bazy danych. Pierwszy model przedstawia dane grupy. Drugi model jest modelem pośredniczącym - łączy relację wiele do wielu pomiędzy grupami a użytkownikami.

Mam problem z autoryzacją użytkowników znajdujących się w danych grupach. Póki co nie mam żdnej autoryzacji użytkowników w grupach. Będąc użytkownikiem, który nie należy do pewnej grupy mogę dostać się do innej grupy wpisując adres Url : https://localhost:xxx/Groups/ChosenGroup/'Id grupy' (muszę tylko znac albo zgadnąć ID obcej grupy). Szukam łatwego i skutecznego sposobu na autoryzacje. Znalazłem 2 rozwiązania:

  1. wykorzystanie IDataProtector. Dzięki temu mój adres url będzie miał zaszyfrowanie ID, dzięki czemu włamanie będzie bardzo utrudnione, prawię niemożliwe. Mimo tego rozwiązanie to nie podoba mi się, ponieważ znając zahashowane ID, będzie można dalej włamać się do nieswojej grupy. Link dla tych, którzy chcą poczytać o IDataProtector: https://code-maze.com/data-protection-aspnet-core/.
  2. znalazłem następujący artykuł: https://www.thereformedprogrammer.net/part-2-handling-data-authorization-asp-net-core-and-entity-framework-core/ mowa jest tutaj o "Handling data authorization in ASP.NET Core and Entity Framework Core". Artykuł wygląda dość ciekawie, jednak jestem początkujący i po prostu średnio to rozumiem.

Proszę o wskazówki od bardziej doświadczonych programistów.
Pozdrawiam.

2

Security nie powinno polegać na tym, że ktoś nie zgadnie np. id w linku lub nazwy pliku (chociaż czasem może to być stosowane w połączeniu z innymi metodami)

Nie rozumiem też sensu ukrywania Id (nie znam tego IDataProtector) bo równie dobrze Id twojej grupy mógłby być Guid i również byłoby nieprawdopodobnie ciężko odgadnąć taki Guid, ale nadal ktoś może go komuś podać.

Według mnie powinno być tak, że jeżeli ktoś wchodzi na https://localhost:xxx/Groups/ChosenGroup/5, to sprawdzasz czy on jest w tej grupie lub czy ma jakieś uprawnienia z nią związane.

Nie wiem jaka jest logika biznesowa w twojej aplikacji, ale jeżeli 1 użytkownik może być w jednej grupie, a jedna grupa może mieć wielu userów - (1 group many users + 1 user 1 group (tak się to zapisywało? :D)

public class Group
{
	public int Id { get; protected set; }
	
	public List<User> Members { get; set; } = new List<User>();
}

a pod spodem klasa User ma klucz obcy GroupId

to przy próbie wejścia do Grupy ``https://localhost:xxx/Groups/ChosenGroup/5`

Naiwna wersja: ładujesz z bazy tę grupę i jej członków (members) przy pomocy Joina (Include w EF Core) i sprawdzasz czy aktualnie zalogowany user jest na liście w tej grupie (da się to zrobić wydajniej)

chociaż w sumie jeżeli się nie mylę, to wystarczyłoby po prostu sprawdzić czy ten foregin key w userze jest równy id grupy :D

I jeżeli jest w tej grupie to zwracasz mu widok/dane, a jak nie to jakiś forbidden*/czy stronę z komunikatem

no ale łapiesz koncept

0

@WeiXiao dobrze prawi. Kwestia tego czy obecnie zalogowany użytkownik ma dostęp do danej grupy to już bardziej logika biznesowa. Jednocześnie brzmi to jak rdzeń bezpieczeństwa Twojej aplikacji, więc możesz użyć do tego middleware (zakładając że używasz ASP Core) w którym możesz sprawdzić czy użytkownik należy do grupy, i jeśli nie to zwrócić 403.

0

Grupy są tworzone dynamicznie. Każdy użytkownik może być w jednej lub wielu grupach lub może nie być w żadnej. Każda grupa może mieć tylko jednego użytkownika - administratora (użytkownik, który utworzył grupę) lub administratora i wielu użytkowników. W ramach grupy użytkownicy mogą rozmawiać ze sobą na forach, które są widoczne tylko na tych grupach. Mój program ma następującą strukturę:
1)Modele - modele, które tworzą tabele w bazie danych, data seeder
2)Kontrolery
3)Widoki - pliki .cshtml i modele widoków - klasy w c#, które definiują dane przesyłane do widoków przez kontroler
4)Serwisy - klasy w c# odpowiedzialne za przetwarzanie danych z bazy. Są używane przez kontrolery. Zazwyczaj jest jeden serwis dla jednej tabeli w bazie. Przykładowe medody w serwisach to: GetById(...), Delete(...), Add(...).

Na tę chwilę moja aplikacja nie ma żadnego systemy uwierzytelniania dostępu do grup, ponieważ robiłem ten system wzorując się na projekcie forum internetowego, w którym nie było żadnych mechanizmów uwierzytelniania oprócz logowania się.

Podoba mi się pomysł WeiXiao.
Chciałbym zaimplementować naiwną wersję : "ładujesz z bazy tę grupę i jej członków (members) przy pomocy Joina (Include w EF Core) i sprawdzasz czy aktualnie zalogowany user jest na liście w tej grupie (da się to zrobić wydajniej)" wystarczyłoby mi to, żeby zaspokoić potrzeby mojej aplikacji.

Zastanawiam się gdzie umieścić funkcje odpowiedzialne za sprawdzanie czy zalogowany user ma dostęp do różnych zasobów. Może utworzyć nowe serwisy(funkcje), które będą sprawdzać te uprawnienia? Serwisy te byłyby oczywiście wywoływane przez odpowiednie metody kontrolera. Jakie są dobre praktyki jeśli chodzi o lokalizacje tej logiki w cełej solucji?

Ogólnie problem polega na tym, że w ramach grup mam fora, później posty i postReplies - sporo tego jest w bazie danych. Trochę żmudne byłby teraz dla każdej metody w każdym kontrolerze tworzyć funkcje, która będzie sprawdziała w bazie czy zalogowany użytkownik ma dostęp do danych zasobów. Może da się to jakoś łatwiej zrobić?

Znalazłem też w dokumantacji microsoftu coś co może mi się przydać: https://docs.microsoft.com/pl-pl/aspnet/core/security/authorization/resourcebased?view=aspnetcore-3.1
Uważacie, że warto z tego korzystać czy w moim przypadku lepiej stworzyć zwykłe klasy, które po prostu wezmą dane z bazy i sprawdzą czy dany użytkownik może być na danej stronie i zwrócą 403 albo widok?

Odnośnie middleware to musiałbym jeszcze troche doczytać jak to dokładnie działa bo jestem początkujący jeśli chodzi o asp.net core.

1

Zastanawiam się gdzie umieścić funkcje odpowiedzialne za sprawdzanie czy zalogowany user ma dostęp do różnych zasobów. Może utworzyć nowe serwisy(funkcje), które będą sprawdzać te uprawnienia? Serwisy te byłyby oczywiście wywoływane przez odpowiednie metody kontrolera. Jakie są dobre praktyki jeśli chodzi o lokalizacje tej logiki w cełej solucji?

Zależy którego archyteka™ pytasz xD


Jedno z podejść jakie mogę zaproponować, to że masz jakąś klasę o nic nie mówiącej nazwie typu manager która Ci ogarnia ten dostęp do grup

private readonly GroupsManager _manager;

public HomeController(GroupsManager manager)
{
	_manager = manager;
}

i później w akcjach kontrolera używasz go do próby pobrania grupy

implementacja tego managera mogłaby jakoś tak wyglądać

public class GroupsManager
{
	private readonly Context _context; // bazka 

	public GroupsManager(Context context)
	{
		_context = context;
	}

	public async Task<ResultOf<Group>> TryLoadGroupForUser(int id, User user)
	{
		// jakimś cudem user jest nullem pomimo authorize - sytuacja niespotykana, można rzucać wyjątek :D
		if (user == null)
			return new ResultOf<Group>(ResultEnum.wtf, "trolololololololololololo", null);

		var group = await _context
						  .Groups
						  .Include(x => x.Members)
						  .FirstOrDefaultAsync(x => x.Id == id);
						  
		if (group == null)
			return new ResultOf<Group>(ResultEnum.NotFound, "ops", null);

		bool userCanAccessThatParticularGroup = group.Members.Any(x => x.Id == user.Id);

		if (!userCanAccessThatParticularGroup)
			return new ResultOf<Group>(ResultEnum.Forbidden, "cya", null);

		return new ResultOf<Group>(ResultEnum.Ok, "", group);
	}
}

że dostając id grupy i aktualnego usera pobiera tę grupę i następuje walidacja
oraz w jakiś wymyślny sposób zwraca wynik wyżej do kontrolera, aby następnie na podstawie tego zwrócić może jakiś widok, redirect, status code, komunikat, w/e.

w jaki sposób będziesz zwracał wynik to już twój pomysł, jest wiele różnych mniej lub bardziej ciekawych czy też praktycznych podejść, tutaj jest to po prostu enum.

[Authorize]
[HttpGet]
public async Task<IActionResult> Group(int id) // a może nullable ten id? 
{
	// tu sobie jakoś ładujesz current_user
	var user = "https://stackoverflow.com/questions/36641338/how-to-get-current-user-in-asp-net-core";

	var result = await _manager.TryLoadGroupForUser(id, user);

	var then = result.Result switch // nazywanie jest trudne
	{
		ResultEnum.Ok => View("GroupView", result.Data),
		ResultEnum.NotFound => View("404View", result.Message),
		ResultEnum.Forbidden => View("ForbiddenView", result.Message),
		ResultEnum.wtf => (IActionResult)Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
		ResultEnum.Unauthorized => Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
		_ => Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
	};

	return then;
}

dodatkowo wypadałoby abyś machnął sobie jakieś inne modele do zwracania, bo teraz właściwie istnieje szansa, że w raz z grupą zwrócisz informacje o memberach User, czyli maile, password hashe itd.

pierwszy z boku temat
Czym jest ViewModel?
What is ViewModel in MVC?

1 użytkowników online, w tym zalogowanych: 0, gości: 1