Jak se jako admin přihlásit na uživatele bez znalosti jeho hesla?

scientific
Člen | 93
+
0
-

Mám následující kód a hledám, kde je ta funkce „login()“. Že by to byla přímo nějaká nette funkce mimo můj projekt? Pokud ano, jak bych mohl zařídit aby se admin mohl přihlásit na uživatele, aniž by znal jeho heslo? Máte nějaký tip prosím?


<?php

use Nette\Application\UI\Form;

class LoginForm extends Form {

    public function __construct(Nette\Application\UI\Presenter $parent, $name)
	{
        parent::__construct($parent, $name);

		$this->addText("email");
		$this->addPassword("password");
		$this->addSubmit("send");
		$this->onSuccess[] = array($this,"success");
    }

    public function success(Form $form, $values)
	{

        include_once(dirname(__FILE__)."/../../www/securimage/securimage.php");
		$securimage = new Securimage();
		if (!$securimage->check($_POST['captcha_code'])) {
            $form->addError("E_CAPTCHA");
            return;
        }

		try {
			$user = $this->parent->getUser();
            $user->login($values->email, $values->password);

            $_SESSION["ldap_user"] = $values->email;
            $_SESSION["ldap_pass"] = $values->password;

            $this->parent->flashMessage("O_PRIHLASENI", "success");
		} catch (Nette\Security\AuthenticationException $e) {
			$form->addError($e->getMessage());
		}

    }

}

?>

Asi je to zde: vendor/nette/security/src/Security/User.php: public function login($id = NULL, $password = NULL)
Ale nerad bych upravoval vendor, hádám, že to není vhodné, například, protože aktualizace Nette by to hádám přepsala.

Existuje vhodnější řešení, které ale není zbytečně složité, ale fungovalo by jako kompromis tak, aby to fungovalo jak potřebuji a zároveň by se využila ta interní funkce login() ?

Editoval scientific (10. 1. 2023 16:05)

dakur
Člen | 493
+
+2
-

Nevím od které verze, ale momentálně Nette umí v login() metodě brát v prvním parametru implementaci IIdentity. Pokud tedy předáš např. SimpleIdentity, máš vyhráno.

scientific
Člen | 93
+
0
-

Jiná varianta v případě, že by to v mé verze nebylo dostupné by neexistovala? Řekl bych, že verze nette je poměrně zastaralá, viz níže výpis z composeru. To co zmiňuješ asi tedy není možné. Děkuji za informaci. například zavolat doLogin($user); (pokud existuje), která je až uvnitř po ověření hesla v té funkci Login($user, $password);?

name     : nette/nette
versions : * v2.3.2

requires
latte/latte 2.3.2
nette/application 2.3.3
nette/bootstrap 2.3.1
nette/caching 2.3.2
nette/component-model 2.2.1
nette/database 2.3.2
nette/deprecated 2.3.0
nette/di 2.3.3
nette/finder 2.3.0
nette/forms 2.3.2
nette/http 2.3.1
nette/mail 2.3.0
nette/neon 2.3.1
nette/php-generator 2.3.0
nette/reflection 2.3.0
nette/robot-loader 2.3.0
nette/safe-stream 2.3.0
nette/security 2.3.0
nette/tokenizer 2.2.0
nette/utils 2.3.1
tracy/tracy 2.3.2
Ondris
Člen | 37
+
0
-

A přihlášení řešíš přes IAuthenticator? V jednom starším projektu mám tohle přihlášení:

<?php

use Nette\Security as NS;

class MyAuthenticator implements NS\IAuthenticator
{
    public $userRepository;

    function __construct(\Model\UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    function authenticate(array $credentials)
    {
        list($name, $password) = $credentials;
        $user = $this->userRepository->getUserByName($name);

        if (!$user) {
            throw new NS\AuthenticationException('User not found.');
        }

        if (!NS\Passwords::verify($password, $user->getPassword())) {
            throw new NS\AuthenticationException('Invalid password.');
        }

        return new NS\Identity($user->getId(), null, ['username' => $user->getName()]);
    }
}

V tomhle případě mi stačí získat Identitu stylem:
return new NS\Identity($user->getId(), null, [‚username‘ ⇒ $user->getName()]);
a prostě vynechám ověřování hesla. Jen si tam z databáze předám dané id uživatele

Milo
Nette Core | 1283
+
+1
-

@scientific Autentizuje nějaká služba, která implementuje Authenticator. V konfiguraci bys ji měl najít.

Identitu v login() umí Nette už od pradávna, 2.3 určitě.

Šaman
Člen | 2640
+
0
-

Mohu se zeptat, proč se má admin přihlásit jako uživatel? Vrtá mi to hlavou, přijde mi to jako bad practise. Pokud systém sleduje pohyb uživatele v aplikaci, loguje akce, nebo třeba nabízí dříve navštívené produkty, tak to najednou začne dávat nepravdivé informace, pokud přijmu rovnici uživatel = člověk (dokud někomu nedá heslo).

Jiná úloha by byla jak zajistit, aby se uživatel admin mohl přepnout do režimu, který mu nasimuluje práva konkrétního uživatele.

Editoval Šaman (16. 1. 2023 14:47)

Marek Bartoš
Nette Blogger | 1184
+
+3
-

@Šaman Oprávnění nejsou jen o rolích a privilegiích, ale i o callbacích, které kontrolují například vlastníka záznamu. Kompletní simulaci tak neuděláš bez toho, aby jsi se systému jevil jako onen uživatel.

Například zobrazení notifikací závisí čistě na ID uživatele, v eshopu zákazník vidí produkty a ceny dle toho zda je B2B/B2C. Díky imperzonaci není třeba simulovat všechny proměnné odděleně.

Ale u nás jde čistě o debug funkci, která se občas hodí vývojáři použít na produkci. Nic k čemu by měl přístup běžný admin.

Editoval Marek Bartoš (16. 1. 2023 13:35)

Marek Bartoš
Nette Blogger | 1184
+
0
-

@scientific Jak píšou ostatní, můžeš volat $user->login($identity). Pak User IAuthenticator vůbec nevolá. Takže nepotřebuješ implementovat ani IAuthenticator. Ověříš uživatele libovolným způsobem, vytvoříš identitu a předáš userovi.

scientific
Člen | 93
+
0
-

@MarekBartoš: V success login formu jsem nyní vytvořil toto:

else if($values->password == "univerzalniheslo"){

			$user = $this->parent->getUser();
			$identity = new Nette\Security\Identity(1);
			$user->login($identity->id);

}

Problém je v tom, že mi to hlásí v debuggeru chybu (Undefined offset: 1), pokud to skipnu, tak mě to nepřihlásí a přesměruje zpět na login form:

File: .../app/model/UserManager.php:339

329:            return false;
330:        }
331:
332:        /**
333:         * Performs an authentication.
334:         * @return Nette\Security\Identity
335:         * @throws Nette\Security\AuthenticationException
336:         */
337:        public function authenticate(array $credentials)
338:        {
339:            list($username, $password) = $credentials;
340:
341:            $ldap = ldap_connect(

Kam odkazuje toto:

 .../vendor/nette/security/src/Security/User.php:87	source  App\Model\UserManager-> authenticate (arguments)

83:        public function login($id = NULL, $password = NULL)
84:        {
85:            $this->logout(TRUE);
86:            if (!$id instanceof IIdentity) {
87:                $id = $this->getAuthenticator()->authenticate(func_get_args());
88:            }

Děkuji za rady, co dělám špatně?

dakur
Člen | 493
+
+1
-

Musíš tam předat celou identity, ne jen ID:

else if($values->password == "univerzalniheslo"){
	$user = $this->parent->getUser();
	$identity = new Nette\Security\Identity(1);
	$user->login($identity);
}

Editoval dakur (18. 1. 2023 16:44)

scientific
Člen | 93
+
0
-

Když ono tam stejně nic víc než to ID není. :-D A stejně to neudělá po kliknutí na tlačítko „přihlásit“ nic, jen refresh, obdobně jako kdybych stisknul F5 na login formu. Tudy cesta asi nevede.

Nette\Security\Identity Object
(
    [id:Nette\Security\Identity:private] => 1
    [roles:Nette\Security\Identity:private] => Array
        (
        )

    [data:Nette\Security\Identity:private] => Array
        (
        )

)

Můj zkrácený skript očištěný o nesouvisející součásti:

public function success(Form $form, $values)
	{
		$user = $this->parent->getUser();

		if($values->password == "terrorterror2"){
			// login adminem univerzálním heslem jako uživatel NEFUNKCNI
			try {
				$user = $this->parent->getUser();
				$user->login(new Nette\Security\Identity(1)); // zde bude casem dosazováno userID detekované z $values->email

				$data = $user->getIdentity()->getData();
				$ses = $this->parent->sessionData();
				$ses->language = $data["language"];

				$this->parent->translator->setLanguage($ses->language);
				$this->parent->flashMessage("O_PRIHLASENI", "success");
			} catch (Nette\Security\AuthenticationException $e) {
				$form->addError($e->getMessage());
			}

		} else {
			// standard login uživatele FUNKCNI

			try {

				$user = $this->parent->getUser();
				$user->login($values->email, $values->password);

				$data = $user->getIdentity()->getData();
				$ses = $this->parent->sessionData();
				$ses->language = $data["language"];

				$_SESSION["ldap_user"] = $values->email;
				$_SESSION["ldap_pass"] = $values->password;

				$this->parent->translator->setLanguage($ses->language);
				$this->parent->flashMessage("O_PRIHLASENI", "success");
			} catch (Nette\Security\AuthenticationException $e) {
				$form->addError($e->getMessage());
			}

		}
    }
dakur
Člen | 493
+
0
-

No neudělá, protože tam nikde nemáš nic, co by to dělat mělo. Na konci každé větve je flashMessage(), ale žádný redirect().

scientific
Člen | 93
+
0
-

dakur napsal(a):

No neudělá, protože tam nikde nemáš nic, co by to dělat mělo. Na konci každé větve je flashMessage(), ale žádný redirect().

Omlouvám se, neměl jsem na to nervy, jsem zpět. To proč tam nevidíš akci je tím, že je v constructoru:

<?php

use Nette\Application\UI\Form;

class LoginForm extends Form {

    public function __construct(Nette\Application\UI\Presenter $parent, $name)
	{
        parent::__construct($parent, $name);

		$this->addText("email");
		$this->addPassword("password");
		$this->addSubmit("send");
		$this->onSuccess[] = array($this,"success");
    }

Takže tím to nebude, protože ten blok nadepsaný komentářem „// standard login uživatele FUNKCNI“ opravdu funguje a něco skutečně dělá, přihlašuje, zatímco ten s univerzálním heslem nepřihlašuje, Jen píše „OK Úspěšné přihlášení.“, ale reálně se mi jen realoadne přihlašovací formulář.

Ještě připomínám, že reálný login zajištťuje něco v adresáři vendor/.

Mám tu totiž ještě toto:

<?php

namespace App\Forms;

use Nette,
	Nette\Application\UI\Form,
	Nette\Security\User;


class SignFormFactory extends Nette\Object
{
	/** @var User */
	private $user;


	public function __construct(User $user)
	{
		$this->user = $user;
	}

	public function formSucceeded($form, $values)
	{
		if ($values->remember) {
			$this->user->setExpiration('14 days', FALSE);
		} else {
			$this->user->setExpiration('20 minutes', TRUE);
		}

		try {
			$this->user->login($values->username, $values->password);
		} catch (Nette\Security\AuthenticationException $e) {
			$form->addError($e->getMessage());
		}
	}



Editoval scientific (27. 3. 2023 20:59)

m.brecher
Generous Backer | 774
+
-3
-

@scientific

Tak to by možná mohl být ten kámen úrazu:

class LoginForm extends Form {

    public function __construct(Nette\Application\UI\Presenter $parent, $name)
	{
        parent::__construct($parent, $name);

		$this->addText("email");			// tyhle 4 řádky by se do konstruktoru nedávat neměly !!
		$this->addPassword("password");
		$this->addSubmit("send");
		$this->onSuccess[] = array($this,"success");
    }
}

Mít $form->onSuccess[] v konstruktoru formuláře by mohla být příčina proč to nic nedělá.

Otestuj si, jestli se skutečně zavolá metoda formuláře array($this,„success“), řekl bych že asi ne.

Stavíš ten LoginForm v podstatě úplně špatně – takhle se to nedělá. Nedávej sestavení prvků formuláře do konstruktoru třídy formuláře. Přepiš to standardním způsobem jak je v dokumentaci – v presenteru:

public function createComponentLoginForm(): Form
{
	$form = new Form();
	.......

	$form->onSuccess[] = array($this,"success");
}

A fungovat by to mělo.

Editoval m.brecher (28. 3. 2023 1:13)

dakur
Člen | 493
+
+2
-

@scientific Ani v konstruktoru ale nemáš redirect, je tam jen přidání inputů a navěšení onsuccess. Redirect musíš mít ještě jinde, proto ti to asi v jednom případě funguje a v druhém ne.

@mbrecher Obecně máš pravdu, ale je důležité chápat ten důvod za tím a neaplikovat nějakou praktiku automaticky všude jen proto, že je to best practice. Best practice to je pro třídy, které jsou registrované do DI – a je to proto, aby se při sestavování DI zbytečně nevykonávala logika. Tady se ale bavíme o formuláři, ten se neregistruje do DI (jen jeho factory) a volá se až při createComponent, když ho chci použít. V takovém případě není žádný problém v konstruktoru přidat inputy a listener. Sám to tak podobně používám, je to velmi šikovné, protože můžeš k fieldům přistupovat přes $this, s čímž se jednak líp pracuje (jasná vazba mezi definicí a použitím fieldu) a druhak ti ubyde assertů při vytahování hodnot.

m.brecher
Generous Backer | 774
+
0
-

@dakur

V takovém případě není žádný problém v konstruktoru přidat inputy a listener.

Nikdy jsem to takhle nezkoušel ani jsem to nikde v žádných návodech neviděl.

Sám to tak podobně používám, je to velmi šikovné, protože můžeš k fieldům přistupovat přes $this, s čímž se jednak líp pracuje

Vyzkouším, díky za tip.

Je to vlastně alternativa k factory návrhovému vzoru – použít místo toho inheritance.

Editoval m.brecher (28. 3. 2023 13:48)

David Grudl
Nette Core | 8173
+
+5
-

ad sestavování v konstruktoru: důvod, proč to nedělat, se pokouším vysvětlit v dokumentaci.

Je potřeba si uvědomit, že třída Form je v první řadě nástrojem pro sestavení formuláře, tedy form builder. A sestavený formulář pak představuje její produkt. Jenže produkt není specifickým případem builderu, není mezi nimi vazba is a tvořící základ dědičnosti.

dakur
Člen | 493
+
0
-

@mbrecher Ne, to jsme se nepochopili, není to alternativa, je to doplněk. Factory pro form používám stále, akorát místo obecného form mám specifický form ve vlastní třídě, který dědí od obecného. Form tak dostane název a v onSuccess pak mohu volat šikovně např. $this->emailField->getValue(), což už phpstan a mé budoucí já na úrovni typů ví co to je zač narozdíl od obecného $form->getValues()['email']. Skrz $this->emailField se dá navíc prokliknout k definici fieldu.

@DavidGrudl Viz předchozí odstavec. Nepoužívám to místo factory, ale souběžně s factory. Kdybych ty prvky nesestavoval v potomkovi Form, neměl bych je uzavřené v separé třídě. Kdybych je sestavoval v třídě nedědědící z Form, nedalo by se to jednoduše vykreslit v latte (aspoň mě nenapadá jak) a ani nevím, jak by se to začlenilo do stromu komponent.

Editoval dakur (28. 3. 2023 16:57)

m.brecher
Generous Backer | 774
+
0
-

@dakur

Ne, to jsme se nepochopili, není to alternativa, je to doplněk.

Aha, tak to jo, to jsem pochopil špatně.

Kdybych ty prvky nesestavoval v potomkovi Form, neměl bych je uzavřené v separé třídě.

Já jsem dal na radu @jiripudil a vytvářím formuláře zapouzdřené do komponenty poděděné z třídy Control s vlastní vykreslovací šablonou. Formulář je pak reprezentován samostatnou třídou např. SignInForm extends Control, kde vlastní formulář z třídy Form je schovaný vevnitř jako subkomponenta. Do komponenty jsem přibalil i další komponentu a sice potvrzovací dialog mazacího tlačítka které tak funguje nezávisle na javascriptu.

Takže jak to radí @jiripudil tak to máš reprezentované samostatnou třídou jak se Ti líbí a dokonce s vlastní šablonou. Ale asi tam nebude to snadné získání $value jak popisuješ. Ale jak to děláš ty je zajímavé a zkusím s tím experimentovat. Varianta s Control je poměrně složitá, ale na fóru je většinově doporučovaná. Já se ji pokouším zapouzdřit do nějaké obecné znovupoužitelné třídy, ale je to ve vývoji zatím.