Authenticator + model, rozdělení zodpovědností

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Tori
Člen | 32
+
0
-

Pěkný večer.
Procházela jsem si autentikátor v sandboxu i dokumentaci (tady nebo tady) a moc nerozumím tomu, jakým způsobem se ověrují přihlašovací údaje. Vadilo mi:

  1. Entitu uživatele a editaci jeho údajů má na starosti model (např. UserRepository). Proč by měl vědět něco o tom, jestli se má při úspěšném ověření přihl.údajů vracet instance Identity nebo čeho? → Proč je autentikátor v sandboxu zařazený pod NS Model?
  2. A naopak, autentikátor patří pod NS Nette\Security, implementuje Nette\Security\IAuthenticator. Tedy nemá nic společného s modelem, tím méně s Nette\Database. → Proč v případech, kdy není v NS Model, dostává přesto připojení k databázi a používá konkrétní Nette\Database API?

Došla jsem teda k něčemu takovémuto:

  • rozhraní Security\IUserRepository určuje, že třída umí
    • najít data uživatele podle přihlašovacího jména
    • vytvořit hash hesla
  • Model\UserRepository implements Security\IUserRepository
    • kromě věcí z rozhraní je to běžný model, vrací data na profil uživatele apod.
  • Security\Authenticator
    • dostane třídu implementující Security\IUserRepository, od ní získá údaje o uživ., s její pomocí ověří heslo
    • vrací Identity nebo vyhazuje při chybě.

Příklad jsem kvůli délce dala jako Gist. Budu ráda za vaši kritiku.


edit (2013–03–14 18h): Vlastně totéž už radil Hosiplan před dávnými lety… snad někdy přečtu tu tunu užitečných rad tady.

Editoval Tori (14. 3. 2013 18:25)

Jan Tvrdík
Nette guru | 2595
+
0
-

Pěkný večer Tori,

v prvé řadě – model není třída, ale jedna ze tří částí MVC architektury. Může být jako třída implementován, ale v praxi je implementován spíše jako nějaký soubor tříd.

Proč by měl (UserRepository) vědět něco o tom, jestli se má při úspěšném ověření přihl.údajů vracet instance Identity nebo čeho?

To záleží, jakou zodpovědnost chceš, aby třída UserRepository měla. Pokud má sloužit jako repositář v takovém tom klasickém slova smyslu, tak by se o žádnou autorizaci starat neměl. Už proto, že je to porušení Single responsibility principle.

Proč je autentikátor v sandboxu zařazený pod NS Model?

Protože je to součást logiky aplikace pracující s daty. Tedy z pohledu MVC ho řadíme do modelu.

A naopak, autentikátor patří pod NS Nette\Security.

Nechápu otázku / problém.


Pominu-li použití mizerného hashovacího algoritmu (sha1), tak to vypadá rozumně, ač možná zbytečně složitě.

Tori
Člen | 32
+
0
-

A naopak, autentikátor patří pod NS Nette\Security.

Nechápu otázku / problém.

Myšleno že v těch ostatních dvou uvedených příkladech, tj. v dokumentaci (https://doc.nette.org/…thentication) a kuchařce (https://doc.nette.org/…thentication) je zařazen pod NS Security, tak proč používá mapper místo nějaké vyšší vrstvy modelu? Ten autentikátor v sandboxu je btw stejný jako v příkladu v docs, ale každý je zařazený v jiném kontextu (namespace) – vlastně i proto jsem nad tím začala uvažovat.

Složité to asi je proto, že jsem autentikátor nechtěla jako součást modelu. Z pohledu autentikátoru by měla implementace IUserRepository být spíš service než repository (podle pěti vrstev modelu). Tzn. pokud změním způsob ukládání uživatelů do DB, změním úložiště, nebo budu chtít solit, zas upravím jen repository.

Zkusím to použít a uvidím, jestli se objeví nějaký zádrhel. Každopádně díky. :)

Editoval Tori (14. 3. 2013 7:41)

pawouk
Člen | 172
+
0
-

@Tori: No já s tebou zase souhlasím, také si myslím, že do Authenticator neptaří do modelu a teké to mám mimo. Rozhodně mi přijde špatně aby authenticator měl přímí přístup do DB. Tam má sahat jen model a autenticator se nemá starat o to zda vzal model data z db nebo xml. Nicméně já to mám vyřešešné tak, že do autenticatoru injectnu model a je to. Pak se metoda autenticator celkově zjednoduší:

public function authenticate(array $credentials){
    list($username, $password) = $credentials;
    $user = $this->model->findUser($username);
    if(!$user)
		throw new Security\AuthenticationException('The username is incorrect.', self::IDENTITY_NOT_FOUND);
    if(!$user->checkPassword($password))
		throw new Security\AuthenticationException('The password is incorrect.', self::INVALID_CREDENTIAL);
    return new Security\Identity($user->id, $user->getRoles(), $user->toArray());
}

Editoval pawouk (14. 3. 2013 7:55)

Tori
Člen | 32
+
0
-

@pawouk: $user->checkPassword($password) – tohle se mi líbí, proč by měl autentikátor vědět, jakým způsobem se heslo kontroluje a jestli se hašuje nebo ne, s dovolením použiju.

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Model je business logika. Přihlášování do aplikace, pomocí uživatelského účtu, zapsaného v databázi, je jednoznačně její součástí. Není žádný důvod autentikátor někam vyčleňovat, je to prostě součást modelu. Měl by mít pouze jednu zodpovědnost (přihlašování), tudíž si nutně bude muset spoustu věcí vyžádat jako závislosti (třídu na komunikaci s databází atd.) a je mu jedno, kolik vrstev tyhle závislostí zase obalují.

Proměnná $this->model je peklo :). Jak píše Honza, pojem Model je to M z MVC. Když bych náhodou měl pracovat na tomhle tvém projektu, budu se ptát: a jaký propána model? Tohle je fakt bad pattern. Zajímalo by mě, instance jaké třídy v té proměnné je?

Šaman
Člen | 2659
+
0
-

Model mě nepřestane fascinovat tím, že 10 lidí má na něj 10 různých názorů.

Ověření, zda zadaný uživatel má na něco práva (autorizátor) je určitě věc modelu. Ověření, zda uživatel je ten, za kterého se vydává (autentizátor), tam si nejsem úplně jist, ale má to k modelu tak blízko, že je to podle mě ok.

Ale od té doby, kdy nepoužívám $this->model (který sloužil jako repositoryLocator a zároveň nejvyšší fasáda) mám stále morální problém s tím nechat presentery hrabat se v jednotlivých Repository (potažmo mapperech, protože často ta třída plní obě funkce). Prostě mi připadá, že presenter má předat třeba data z formuláře modelu a né přemýšlet, kterému repository to předat, případně mu to ještě předkousat na objekt.

duke
Člen | 650
+
0
-

Tori napsala:

@pawouk: $user->checkPassword($password) – tohle se mi líbí, proč by měl autentikátor vědět, jakým způsobem se heslo kontroluje a jestli se hašuje nebo ne, s dovolením použiju.

Ono lze položit i jinou otázku. Proč by měl user vědět, jakým způsobem probíhá autentikace (tj. že se vůbec používá nějaké heslo a to či ono hashování). To je přece věcí autentikátoru. User by měl pouze na požádání poskytnout údaje, tj. např. heslo (ve tvaru, v jakém ho autentikátor očekává, a do tohoto tvaru ho logicky může zakódovat opět jen autentikátor).

Tori
Člen | 32
+
0
-

@duke: Aha… já jsem pawouka pochopila tak, že to ověření hesla taky dává do modelu. Ale vlastně jemu model vrací instanci (čeho?), která to ověření dělá.

Já tohle právě nemám velmi ujasněné.
Způsob počítání hashe je bezpečnostní záležitost a model by to nemuselo zajímat (dokud se změnou algoritmu nezmění délka výsledného řetězce).
Na druhou stranu, při registraci uživatele nebo změně hesla taky potřebuju nějak vyrobit hash – mám někde (kde?) volat autentikátor? Nemůžu mít ten algoritmus na dvou místech.
Anebo co když umožním i přihlašování jinými způsoby (Facebook, OpenID nebo třeba uživatele kousne myš a ověří jeho DNA) – pro tyhle případy by zas autentikátor potřeboval jen to rozhraní k datové vrstvě s metodami getUserData, getUserRoles a checkCredentials a nechat případné hashování (nebo rozbor krve) na modelu.

Takže možná takhle:

  • User dostane nějaké údaje potřebné k přihlášení. Podle zvoleného způsobu ověřování (jméno+heslo nebo FB …) si požádá AuthenticatorFactory o příslušný autentikátor a tomu předhodí ty údaje.
  • pak jsou implementace IAuthenticator: každá má na starost, aby zkontaktovala třídu, která s tímhle typem údajů umí pracovat, a nechala je u ní ověřit. Případně jí nějakou předáme jako výchozí způsob ověřování.
  • a v modelu jsou různé implementace ICanAuthenticate, každá požaduje nějaký typ údajů pro přihlášení, a jakým způsobem je ověří a jestli čte z DB nebo ne, jestli hashuje, to je její věc. … protože vlastně hashování nepatří pod zabezpečení aplikace (tzn. autentikátor), ale pod zabezpečení dat samotných, takže to přeci jen patří do modelu. (a tím je vyřešená i registrace a změna hesla)

Takže při použití SimpleAuthenticator by se nastavení změnilo z tohoto:

authenticator:
	class: Nette\Security\SimpleAuthenticator
        arguments: [[
		john: IJ^%4dfh54*
		kathy: 12345
	]]

na toto:

authenticator: Nette\Security\SimpleAuthenticator

defaultAuthModel:
	class: SomeClass
	arguments: [[
        	john: IJ^%4dfh54*
		kathy: 12345
	]]
class SomeClass implements ICanAuthenticate
{
	private $credentials = array();
	public function __construct($credentials) { $this->credentials = $credentials; }

	/** @return bool */
	public function checkCredentials($data)
	{
		return isset($this->credentials[$data['username']])
			? ($this->credentials[$data['username']] === $data['password'])
			: false;
	}
}

Jak se vám to zdá takhle? Vim, že to je asi trochu překombinované, ale zkoušim si představit možné problémy, abych to nemusela při každém novém požadavku předělávat.

duke
Člen | 650
+
0
-

Tori napsala:

@duke: Aha… já jsem pawouka pochopila tak, že to ověření hesla taky dává do modelu. Ale vlastně jemu model vrací instanci (čeho?), která to ověření dělá.

Doporučuji si přečíst komentář od @vojtech.dobes ohledně pojmu „model“. Model nic nevrací; není to funkce, aby něco vracel. Vše, co se týká vnitřního chování aplikace (a sem patří i logika autentikace), je součástí „vrstvy“ zvané model.

Patrně matete model s vrstvou repozitářů. Ty také samozřejmě patří do modelu, ale krom nich tam také patří např. služby pracující nad nimi (včetně autentikátorů).

Já tohle právě nemám velmi ujasněné.
Způsob počítání hashe je bezpečnostní záležitost a model by to nemuselo zajímat (dokud se změnou algoritmu nezmění délka výsledného řetězce).

Bezpečnostní záležitosti aplikace spadají do modelu, neboť se týkají vnitřního chování aplikace (model řeší, co je přístupné a co ne), a nikoli jen vnější komunikace s uživatelem (view řeší, co z toho, co je přístupné, se zobrazí a jak).
Způsob počítání hashe je vnitřní záležitostí autentikátoru. Je to tedy součást modelu. Zajímat to nemusí UserRepository.

Na druhou stranu, při registraci uživatele nebo změně hesla taky potřebuju nějak vyrobit hash – mám někde (kde?) volat autentikátor? Nemůžu mít ten algoritmus na dvou místech.

Toto by nejspíš měla rešit služba, která má přístup jak k repozitáři, kde se přístupové údaje ukládají (např. UserRepository), tak ke službě, která umí heslo zahashovat (to může být buď přímo autentikátor, nebo nějaká služba použitá uvnitř autentikátoru).

Anebo co když umožním i přihlašování jinými způsoby…

Pak jen přibude více autentikátorů a případně dalších služeb spojených s režií kolem (jako výše pro registraci či změnu hesla, atp.).

Takže možná takhle:

  • User dostane nějaké údaje potřebné k přihlášení. Podle zvoleného způsobu ověřování (jméno+heslo nebo FB …) si požádá AuthenticatorFactory o příslušný autentikátor a tomu předhodí ty údaje.

Pokud User míníte Nette\Security\User, tak v momentu, kdy obdrží přihlašovací údaje (voláním metody login), požádá svůj autentikátor (pokud nebyl nastaven, pokusí se ho nalézt pomocí service locatoru) o autentikaci. Takže buď můžete specifický autentikátor nastavit před voláním metody login, nebo si můžete vytvořit univerzální autentikátor, který podle předaných argumentů deleguje autentikaci na konkrétnější autentikátor.

  • pak jsou implementace IAuthenticator: každá má na starost, aby zkontaktovala třídu, která s tímhle typem údajů umí pracovat, a nechala je u ní ověřit. Případně jí nějakou předáme jako výchozí způsob ověřování.

Nejsem si jist, zda rozumím. Třídy implementující rozhraní IAuthenticator jsou právě těmi třídami, které mají umět s danými údaji pracovat. Zda k tomu potřebují další služby (např. repozitáře) už je jiná věc.

  • a v modelu jsou různé implementace ICanAuthenticate, každá požaduje nějaký typ údajů pro přihlášení, a jakým způsobem je ověří a jestli čte z DB nebo ne, jestli hashuje, to je její věc. … protože vlastně hashování nepatří pod zabezpečení aplikace (tzn. autentikátor), ale pod zabezpečení dat samotných, takže to přeci jen patří do modelu. (a tím je vyřešená i registrace a změna hesla)

Tady už je to IMHO totálně zmatené. Jaký je rozdíl mezi IAuthenticator a ICanAuthenticate? Vždyť významem slov jde o totéž. Do modelu patří obojí ze zmíněných zabezpečení.

Jak se vám to zdá takhle? Vim, že to je asi trochu překombinované, ale zkoušim si představit možné problémy, abych to nemusela při každém novém požadavku předělávat.

Nejprve si ujasněte pojmy jako „model“ a zkuste se na věc podívat z pohledu SRP (single responsibility principle), jak už o tom psal @Jan Tvrdík.

Editoval duke (15. 3. 2013 14:15)