Statické ACL v modulárnej aplikácii

V tomto tutoriáli sa naučíme vytvoriť modulárnu aplikáciu v Nette Framework, rozdelenú na verejnú a admin sekciu. Vstup a pohyb po admin sekcii bude kontrolovaný pomocou statického ACL (access control list). Ukážeme vám ako nastaviť ACL v súčinnosti s tzv. roles, resources a privileges.

Tento tutoriál bol 1. 2. 2013 aktualizovaný pre Nette 2.0.8. Je dostupný na Gitu.

Predhovor

Užívatelia, ktorí začnú využívať niektorý webový PHP framework čoskoro zistia, že zložitejšiu aplikáciu je nutné rozdeliť na viac častí (modulov). Najčastejšie sa aplikácia rozdeľuje na dva moduly – Frontend a Backend (niekedy tiež označovaný ako administrácia).

  • Frontend je verejne prístupná časť aplikácie, ktorú vidí bežný návštevník.
  • Backend predstavuje uzavretú časť aplikácie, do ktorej majú mať prístup iba oprávnení užívatelia (admin, redaktor, moderátor). Pre verejnosť je uzavretá.
  • Oba moduly využívajú ten istý Model (DATA) na manipuláciu s biznis logikou aplikácie.

Frontend DATA zvyčajne iba zobrazuje (preto jednosmerná šípka), Backend poskytuje funkcie aj na ich manipuláciu (pridávanie, editovanie, mazanie). (Aplikácie umožňujú manipuláciu s dátami aj na Frontende. Jedná sa napr. o pridávanie diskusných príspevkov bežnými návštevníkmi.)

Čo to je ACL a načo ho potrebujeme

Mnoho programátorov webových aplikácií už výraz ACL možno videlo, ale jej správne uchopiť a použiť vo webovej aplikácií. V dokumentácii se možete dočítat o jeho základnej logike a principech. Ako jej zakomponovať do komplexnej webovej aplikácie si povíme tu.

ACL a Nette Framework

Najjednoduchšie využitie ACL v Nette Framework je v súčinnosti s MVP konceptom. Ako zdroje použijeme názvy Presenterov a privilégia budú zastupovať názvy akcii/view metód.

if ($this->user->isAllowed($this->name, $this->view)) {
    // chraneny kod
}

Čo budeme potrebovať?

  1. Zo stránky Download si stiahnite archív Nette Framework 2.0.8 pro PHP 5.3+
  2. Pred začatím si prosím otestujte svoj webový server pomocou nástroja requirement-checker, či disponuje šetkým, čo Nette pro svoj beh vyžaduje.
  3. Vytvorte si pracovnú zložku pre projekt, napr. nette-acl, a nakopírujte do ní obsah zložky examples/Modules-Usage zo stiahnutého archívu.
  4. Zkopírujeme Authenticator.php, ze složky examples/CD-collection/app/model do naší (tedy nette-acl/app/model/)

Čo najskor upravíme?

  1. vytvorímte složku libsa pridáme do ní knižnicí Nette Framework (zložka Nette v balíku)
  2. v zložke libs ešte vytvorte jednoduchú štruktúru podadresárov s názvami AclProj/Security

Názov AclProj predstavuje názov vášho projektu. V tejto zložke budú umiestnené definície vlastných tried projektu, tedy po namespacom AclProj. V reálnej situácii svoj projekt zvyčajne pomenujete nejakým zmysluplnejším menom. Toto meno použite ako názov namiesto AclProj.

Celá štruktúra zložek vyzerá takto:

/nette-acl
    /app
        /AdminModule
        /FrontModule
        /presenters
    /libs
        /AclProj
            /Security
        /Nette
    /log
    /temp
    /www

3. nahradíme obsah súboru libs/autoload.php týmto:

require __DIR__ . '/Nette/loader.php';

4. do RobotLoaderu v app/boostrap.php pridáme adresár libs:

$configurator->createRobotLoader()
    ->addDirectory(__DIR__)
    ->addDirectory(__DIR__ . '/../libs') // pridajte tento riadok
    ->register();

5. do súboru app/AdminModule/templates/@layout.latte pridáme pod <h1>Modules demo</h1>:

<div n:foreach="$flashes as $flash" class="flash flash-{$flash->type}">
    <p>{$flash->message}</p>
</div>

{if $user->isLoggedIn()}
    {if $user->isAllowed('Admin:Page')}
        <a n:href="Page:">Page</a> (can see only admin) |
    {/if}
    <a n:href="Page:">Page</a> (can see everyone) |
    logged as <em>{$user->getIdentity()->login}</em> (<a n:href="logout!">logout</a>)

{else}
    <p>You are not logged in.</p>
{/if}

Teraz už môžeme otestovať základnú funkčnosť aplikácie.

Pokud niečo nefunguje, zkuste najskor premazať zložku temp/cache.

Zabezpečený Backend

Ako už bolo spomenuté vyššie, na riadenie prístupu do Backend modulu (v ďalšom texte bude použité spojenie AdminModule alebo admin sekcia) bude využívané statické ACL. Statické znamená, že všetky privilégia, zdroje a role budú zapísané v config súboru. Oprávnených užívateľov a ich roly si však uložíme do databázy a následne využijeme Nette Database na získanie týchto údajov z databáze.

Obsah databáze

Vytvorte novú databázu (napr. nette_acl) a tabuľku user, ktorá bude udržiavať údaje o oprávnených užívateľoch. Všimnite si, že tabuľka obsahuje stĺpec role, kde je uložená rola užívateľa. Prihlasovacé jméno, heslo i rola má vždy stejnú hodnotu (pre názorňajšú ukázku).

Spustite tento SQL príkaz nad vašou databázou, čím vytvoríte a naplníte tabuľku users.

CREATE TABLE `user` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `login` varchar(255) NOT NULL,
    `password` char(40) NOT NULL,
    `role` varchar(255) NOT NULL,
    PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;

INSERT INTO `user` (`login`, `password`, `role`) VALUES
('admin', sha1('admin'), 'admin'),
('editor', sha1('editor'), 'editor');

Pripojenie k databázi

Pripojenie ku databáze je potrebné nakonfigurovať ako službu. Bude tak kedykoľvek dostupné v systémovom DI kontajneri. Konfiguráciu zapíšte do app/config.neon (prihlasovacie údaje upravte podľa svojho DB servera).

parameters:
    database:
        driver: mysql
        host: localhost
        dbname: nette_acl
        user: # doplnte meno
        password: # doplnte heslo

services:
    database:
        class: Nette\Database\Connection
        arguments: [
            '%database.driver%:host=%database.host%;dbname=%database.dbname%',
            %database.user%,
            %database.password%
        ]

Vytvorenie ACL

Ako sme už spomenuli, ACL je v tomto tutoriáli zapísaný v config.neon ako služba authorizator vytvorená z Nette\Security\Permission. Jeho úlohou je definovanie rolí, zdrojov a oprávnení. Použité role sa musia zhodovať s rolami užívateľov uložených v databáze v tabuľke user.

services:
    authorizator:
        class: Nette\Security\Permission
        setup:
            - addRole('guest')
            - addRole('editor')
            - addRole('admin')
            - addResource('Admin:Default')
            - addResource('Admin:Page')
            - allow('editor', 'Admin:Default') # šetky akcie resource Admin:Default
            - allow('admin') # šetky resources a ich akcie

Detailne o dedení rolí, definici zdrojov specifikovanie privileges pojednává opet dokumentácie.

V tomto prípade sa jedná o najjednoduchšiu definíciu služby – keď si ktokoľvek od DIC vyžiada authorizator, dostane inštanciu Nette\Security\Permission.

Prihlásenie

Pro prihlásenie použijeme službu authenticator (umístenú v app/models), ktorý obstará overenie údajov zadaných do prihlasovacieho formulára oproti našej DB tabuľke. Službu je iba potreba zaregistrovať v app/config.neon a možme používať.

services:
    authenticator: Authenticator

Pokud vás zaujímá, ako presne authenticator funguje, odporučám Quickstart – Přihlašování uživatelů

Login action

Logika aplikácie v AdminModule bude taká, že ak sa užívateľ pokúsi spustiť nějakú akciu:

  • overí sa, či je užívateľ prihlásený
  • ak nie je, bude presmerovaný na AdminModule\AuthPresenter:login, ktorý mu zobrazí prihlasovací formulár
  • ak je užívateľ už prihlásený, aplikácia v súčinnosti s ACL overí, či má užívateľ oprávnenie spustiť žiadanú Presenter:action

Vytvorte najskôr AdminModule\BasePresenter, od ktorého budú dediť všetky Presentre v AdminModule:

app/AdminModule/presenters/BasePresenter.php

namespace AdminModule;

use Nette\Security\User;

abstract class BasePresenter extends \BasePresenter
{
    public function startup()
    {
        parent::startup();

        if ($this->name != 'Admin:Auth') {
            if (!$this->user->isLoggedIn()) {
                if ($this->user->getLogoutReason() === User::INACTIVITY) {
                    $this->flashMessage('Session timeout, you have been logged out');
                }

                $this->redirect('Auth:login', [
                    'backlink' => $this->storeRequest()
                ]);

            } else {
                if (!$this->user->isAllowed($this->name, $this->action)) {
                    $this->flashMessage('Access denied');
                    $this->redirect('Default:');
                }
            }
        }
    }


    /**
     * Logout user
     */
    public function handleLogout()
    {
        $this->user->logOut();
        $this->flashMessage('You were logged off.');
        $this->redirect('this');
    }

}

V Presenteri sme prepísali metódu startup(). Ak sa pozorne pozrieme na životný cyklus Presentera, zistíme, že metóda startup() sa volá na úplnom začiatku behu Presentera. Ak teda chcete zabrániť vykonaniu akcie Presentera, musíme to urobiť práve tu.

Kód presne implementuje logiku, ktorú sme spomínali o pár odstavcov vyššie:

  • overí sa, či je užívateľ prihlásený
  • ak nie, overí sa či už neuplynul logout interval, v tomto prípade nastavíme flashMessage s informáciou
  • každý neprihlásený užívateľ je potom presmerovaný na AdminModule:AuthPresenter:login, ktorý zabezpečí vykreslenie prihlasovacieho formulára
  • predtým je ešte do Session uložený aktuálny request, vďaka čomu bude užívateľ po prihlásení hneď presmerovaný na destination, z ktorej sme ho v tomto kroku vyhodili
  • ak áno, je s pomocou ACL rozhodnuté či má užívateľ privilégiumzdroju

Ako ste si mohli všimnúť z popisu a kódu, neprihlásení užívatelia sú presmerovaní na AdminModule:AuthPresenter:login, kde sa im zobrazí prihlasovací formulár.

app/AdminModule/presenters/AuthPresenter.php

namespace AdminModule;

use Nette\Application\UI\Form;
use Nette\Security\AuthenticationException;

class AuthPresenter extends BasePresenter
{
    /** @persistent */
    public $backlink;


    /**
     * Login form factory
     * @return Nette\Application\UI\Form
     */
    protected function createComponentLoginForm()
    {
        $form = new Form;
        $form->addText('name', 'Name:')
            ->addRule(Form::FILLED, 'Enter login');
        $form->addPassword('password', 'Password:')
            ->addRule(Form::FILLED, 'Enter password');
        $form->addSubmit('send', 'Log in');

        $form->onSuccess[] = [$this, 'processLoginForm'];
        return $form;
    }


    /**
     * Process login form and login user
     * @param Nette\Application\UI\Form
     */
    public function processLoginForm(Form $form)
    {
        $values = $form->getValues(true);
        try {
            $this->user->login($values['name'], $values['password']);
            $this->restoreRequest($this->backlink);
            $this->redirect('Default:default');

        } catch (AuthenticationException $e) {
            $form->addError($e->getMessage());
        }
    }

}

Metoda processLoginFormsa vykoná iba ak je odoslaný formulár valídny (prejde validačnými pravidlami definovanými v konštruktore formulára). V tele callback funkcie sa pokúsime užívateľa prihlásiť (na pozadí sa zavolá metóda authenticate() z nášho Authenticatora viď. vyššie). V prípade, že je prihlásenie úspešné, užívateľ je presmerovaný späť.

Všimnite, si že volanie $user->login() je obalené try/catch blokom. Ak teda metóda authenticate() vyhodí nejakú výnimku (a že ich vyhadzuje sa presvedčte vyššie v definícii triedy AclProj\Security\Authenticator), je message výnimky vložená do formulára a ten je nanovo vykreslený užívateľovi (samozrejme užívateľa neprihlásime), kde je o svojej chybe informovaný.

Ostáva doplniť kód šablóny AuthPresentera, kde sa nachádza vykreslenie prihlasovacieho formulára.

app/AdminModule/templates/Auth/login.latte

{block #content}
{control loginForm}

Vyzkušajte

Teraz sa už môžete vyskúšať prihlásiť do admin sekcie

  • klikněte na odkaz :Admin:Default:
  • ak sa chcete prihlásiť ako admin zadajte login admin a heslo admin.

Aby ste mohli vyskúšať funkciu ACL, je treba do AdminModulu pridať další Presenter, ktorý je definovaný v ACL.

app/AdminModule/presenters/PagePresenter.php

namespace AdminModule;

class PagePresenter extends DefaultPresenter
{

}

app/AdminModule/templates/Page/default.latte

{block #content}
Yes! You can access to <strong>{$presenter->name}:{$presenter->view}</strong>

Otestuje prístup na nový Presentery pomocou obou užívateľov. Odhláste administratora a prihláste sa ako editor. Editor má podľa ACL povolenie na pohyb po DefaultPresenter. Nemá oprávnenie na vstup do PagePresenter. Skúste klepnúť na jeho odkaz a uvidíte čo sa stane. Zapracoval ACL a Presenter ochránil pred nepovoleným vstupom neoprávenej role. Aplikácia je v tomto momente hotová.