Autorizace uživatele

Školení, která pořádám

O autentizaci uživatele jsem už psal. V tomto článku bych chtěl popsat způsob, jak se dá provádět autorizace uživatele, tedy ověření, že má právo pracovat s nějakým objektem.

Přidání podmínky

Nejjednodušší je ke všem dotazům, které přistupují k datům, připojit podmínku, zda data patří danému uživatel. Tedy např.

<?php
$row = mysql_fetch_assoc(mysql_query("SELECT * FROM clanky WHERE id = " . intval($_GET["id"]) . " AND uzivatel = $_SESSION[uzivatel]"));
?>

Nevýhoda tohoto přístupu spočívá v tom, že na dodatečnou podmínku můžeme snadno zapomenout. Navíc se nedozvíme, jestli záznam neexistuje nebo jenom zda se uživatel pokouší pracovat se záznamem, který mu nepatří (to může být nicméně žádoucí, protože i informace o tom, že objekt existuje, se může dát nějak využít).

Test na začátku skriptu

Druhý způsob na začátku skriptu zjistí, jestli objekt patří uživateli a pokud ne, tak vypíše chybovou hlášku a skončí:

<?php
$row = mysql_fetch_assoc(mysql_query("SELECT * FROM clanky WHERE id = " . intval($_GET["id"])));
if ($row["uzivatel"] != $_SESSION["uzivatel"]) {
    // chybová hláška
    exit;
}
?>

Centralizovaný test

Třetí způsob tyto dotazy centralizuje. Nejprve zakáže veškerý přístup a potom ho u některých souborů povolí všem a u dalších dovolí pracovat jen se záznamem uživatele:

<?php
$povoleno = false;
$soubor = basename($_SERVER["SCRIPT_NAME"], ".php");
$tabulka = substr($soubor, 0, -2); // název souboru je odvozený z názvu tabulky
switch ($soubor) {
case "clanky":
case "diskuse":
case "obrazky":
case "obrazky-1":
    // s obrázky mohou pracovat všichni, výpis článků a diskuse omezíme až v dotazu
    $povoleno = true;
case "clanky-1":
    // s detailem článku dovolíme pracovat jen uživateli, kterému článek patří, vytvoření nového článku (bez ID) povolíme všem
    $povoleno = (!$_GET["id"] || mysql_result(mysql_query("SELECT COUNT(*) FROM $tabulka WHERE id = " . intval($_GET["id"]) . " AND uzivatel = $_SESSION[uzivatel]"), 0));
case "diskuse-1":
    // diskuse u sebe nemá přímo ID uživatele, ale musí ho načíst z článku
    $povoleno = mysql_result(mysql_query("
        SELECT COUNT(*)
        FROM clanky
        INNER JOIN $tabulka ON clanky.id = $tabulka.clanek
        WHERE $tabulka.id = " . intval($_GET["id"]) . " AND clanky.uzivatel = $_SESSION[uzivatel]
    "), 0);
}
if (!$povoleno) {
    // chybová hláška
    exit;
}
?>

Pohled

Další teoretický způsob spočívá ve vytvoření pohledu:

CREATE VIEW clanky_uzivatel AS SELECT * FROM clanky WHERE uzivatel = @uzivatel;

Pohled obsahující proměnnou ale MySQL bohužel neumožňuje vytvořit.

Jakub Vrána, Osobní, 15.12.2008, diskuse: 29 (nové: 0)

Diskuse

Jarda:

Tak to holt nebude view, ale funkce nebo storka :o)

Jinak, hezkej článek, jen většinou jsou práva daleko víc strukturována, takže je třeba použít ještě jiné techniky.

Otázkou jen je, jestli má databáze dělat nějakou logiku nebo po vzoru vrstvové struktury (např. MVC) je to na aplikační vrstvu?! Já osobně si myslím, že se o tuto low-level logiku starat databáze, ale bojím se, že budu kamenován ;o)

Petr F:

Házím první kámen.
Databáze se má starat o to, aby bylo jasně vidět co k čemu patří.
O to, kdo a jak se na data bude koukat, se má starat controller.
Kdo si hodí další?
Očekávám, že i na mě:)

toby:

Nevidím problém v přenesení autorizace na databázi. Pravidla autorizace se nemění moc často, ale musí být vždy k dispozici. Takže když si to schovám do databázové vrstvy (do views nebo procedur), hodně mi to zpřehlední aplikační kód.
A zjednodušší to testování, protože to mohu provádět přímo db dotazy.

kluvi:

Tak prave jsem tak nahrubo dodelal svuj autorizacni system... neni to jeste dodelany ale vicemene to funguje jak ma zatim :-) Pripominky rad prijmu :-)

IDEA:
Univerzalni autorizacni system. V systemu opravneni se definuje pro jednotlive akce. Kazda akce patri do nejake sekce. tj:
text (sekce)
--cist (akce)
--editovat
--zobrazit
--smazat
menu
--zobrazit
--editovat
--smazat

Opravneni je bud allow/deny.

DB:
Tabulka: acl_akce
id    int(10)
sekce    int(10)
akce    varchar(255)

Tabulka: acl_opravneni
id    int(10)
id_akce    int(10)
id_polozky    int(10)
uid_gid    int(10)
user_group    enum('user','group')
platnost_od    int(20)
platnost_do    int(20)
hodnota    enum('allow','deny')

Tabulka: acl_sekce
id    int(10)
nazev    varchar(255)

Tabulka: acl_skupiny
id    int(10)
nazev    varchar(255)
popis    varchar(255)

Tabulka: acl_uzivatele
id    int(11)
login    varchar(25)
heslo    varchar(40)

Tabulka: acl_uzivatele_skupiny
id_skupiny    int(10)
id_uzivatele    int(10)

<?php
//class data se stara o pripojeni k DB
//get_action_id najde id akce podle nazvu sekce a nazvu akce
//get_groups vraci pole s ID skupin ve kterych je uzivatel
class acl extends data{
  function ask($user, $sekce, $akce, $id_polozky){
    $id_akce = $this->get_action_id($sekce, $akce);
    @$x_user = mysql_result(mysql_query("SELECT hodnota FROM acl_opravneni WHERE id_akce='$id_akce' AND uid_gid='$user' AND user_group='user' AND id_polozky='$id_polozky' AND (platnost_od<=".time()." OR platnost_od=0) AND (platnost_do>=".time()." OR platnost_do=0)"),0,0);

    if(!$x_user){
      foreach($this->get_groups($user) as $group){
        @$x_group = mysql_result(mysql_query("SELECT hodnota FROM acl_opravneni WHERE id_akce='$id_akce' AND uid_gid='$group' AND user_group='group' AND id_polozky='$id_polozky' AND (platnost_od<=".time()." OR platnost_od=0) AND (platnost_do>=".time()." OR platnost_do=0)"),0,0);
        if($x_group == 'allow') break;
      }
      if(!$x_group){
        $x_all = mysql_result(mysql_query("SELECT hodnota FROM acl_opravneni WHERE id_akce=0 AND uid_gid=0"),0,0);
        if($x_all == 'allow') return true;
        else return false;
      }
      else{
        if($x_group == 'allow') return true;
        else return false;
      }
    }
    else{
      if($x_user == 'allow') return true;
      else return false;
    }
  }

?>

Pouziti:
if($data->acl->ask($_SESSION['UZ_id'], 'text', 'cist', $text_j[0]['id'])){
//....
}

Mastodont:

To při každém dotazu na oprávnění spouštíš dotaz? Hm, tak to není moc dobré řešení. Já si načtu pole s právy při přihlášení uživatele.

Jakub Suchy:

To je vtip v tomhle a predchozim clanku, ty SQL Injection? Ja vim, ze skripty predpokladaji magic_quotes_gpc = On, ale od PHP6 nic takoveho nebude, PHP 5.2.7 to melo rozbite...V predchozim clanku je i spatne pouzito addslashes()...

kluvi:

Ja si myslim ze lidi co tohle ctou uz neco malo o PHP vi a s touhle problematikou jsou obeznameni ;-)

Jakub Suchy:

Ja si myslim presny opak...Prijde 15lety kluk, udela copy paste a tim to pro nej hasne.

LuKo:

Jeho boj. Už kdysi dávno tu myslím Jakub psal, že jeho cílem je nastínit myšlenku a trochu ji rozebrat, ne produkovat kompletní kódy, které pak jen někdo bezmyšlenkovitě copy-paste použije.

ikona Jakub Vrána OpenID:

I tak je ale samozřejmě mým cílem, aby kódy byly bezchybné a bezpečné. Což jsou, pokud je respektováno nastavení, které tento blog používá a uvádí na každé stránce.

ikona Jakub Vrána OpenID:

Tento blog jsem bohužel začal psát se zapnutým magic_quotes_gpc, protože v době jeho vzniku to byla zcela běžná volba. Pokud mi pošleš patch, který všechny články a diskuse upraví tak, aby počítaly s opačným nastavením, milerád ho nasadím (celý obsah webu lze stáhnot v pravém menu).

A co se špatného použití addslashes() týče, můžeš být prosím konkrétnější? Předchozí článek http://php.vrana.cz/v-php-5-2-7-nefunguje-magic_quotes_gpc.php addslashes() vůbec nepoužívá.

Jakub Suchy:

Addslashes nerespektuje ruzna obskurni kodovani jako treba UTF-7, neni tedy uplne bezpecne, narozdil od napr. mysql_real_escape_string().

ikona Jakub Vrána OpenID:

UTF-7 nepodporuje ani MySQL. UCS-2 MySQL podporuje pouze pro uložení dat a nikoliv pro připojení, problém se ho tedy taky netýká. Problém je tedy jen s kódováními jako Shift-JIS, které se u nás nepoužívá.

Pro nastavení kódování je navíc potřeba použít funkci mysql_set_charset(), která je k dispozici až od PHP 5.2.3, takže ji používá málokdo. Kódování nastavené přes mysql_query("SET CHARACTER SET") funkce mysql_real_escape_string() nezohledňuje.

ikona Jakub Vrána OpenID:

Ještě co se verzí týče: PHP 5.2.7 oficiálně neexistuje a v PHP 6 lze použít filter.default=magic_quotes.

Lamicz:

Mne spis zarazily ty single quotes u Id (int). To je tak spravne? Kazdy to pise jinak a jsem z toho jelen :/

ikona Jakub Vrána OpenID:

MySQL dovoluje i čísla předávat jako řetězce, čehož se dá právě spolu s magic_quotes_gpc využít.

ikona Karel Dytrych:

I kdyz to dovoluje nedochazi potom ke zbytecne konverzi retezce na int?

ikona Jakub Vrána OpenID:

K tomu dochází tak jako tak. MySQL dostane buď řetězec "123" nebo "'123'" a převod na číslo musí udělat stejně.

ikona Karel Dytrych:

Nebude to spise implementovano tak, ze MySQL nejprve z obdrzeneho retezce udela retezec jelikoz je hodnota v uvozovkach a pote ho prevede na int kvuli porovnani? V Oraclu by to tak bylo...

ikona Jakub Vrána OpenID:

No ano, ale z řetězce na číslo se to převede tak jako tak.

ikona david@grudl.com:

Pokud se dá věřit lidem z MySQL Performance Blog, tak výkonnostní rozdíl tam není, viz
http://forum.percona.com/s/m/1573/#msg_num_3

ikona v6ak:

Uvozovky tam musí být. Jakub Vrána používá magic_quotes_gpc a toto je jeden z negativních dopadů této direktivy - mate (viz v6ak.profitux.cz/clanky/co-je-spatneho-na-magic-quotes-gpc.php ). Je to, myslím, mezi mýty o magic_quotes_gpc.

ikona v6ak:

Tak teď jsem si v mobilním zobrazení Opery Mini dával velký pozor, abych použil [reagovat] a stejně se to podle toho nechová. Uvidím teď, tak to bude v desktopovém.

ikona Jakub Vrána OpenID:

Kde přesně podle tebe musí být jaké uvozovky?

ikona v6ak:

To měla být reakce na uživatelé Lamicz. Bohužel, Opera Mini v Mobilním zobrazení asi vynechá nějaký field nebo co a nepošle se to jako reakce. Bez mobilního zobrazení je to OK, jak jsem zjistil, takže budu přepínat.

ikona v6ak:

Doháje, teď se mi Opera Mini snaží popřít determinismus!

ikona Jakub Vrána OpenID:

Pro úplnost dodám, že místo apostrofů by tam mohlo být i intval($_GET["id"]).

Srigi:

IMHO nie je nad Zend_Acl. Konfiguraciu je mozne okrem toho ulozit do XML, naco tuto cast aplikacnej logiky ukladat do DB? Zend_Acl je velmi malo zavisla, vyzaduje len Zend_Exception.

http://forum.zendframework.cz/index.php?topic=271.msg1376#msg1376

http://www.roetgers.org/screencast-index/zend_acl-quickstart/

jirka:

A jak se system zesloziti, kdyz je navic potreba autorizace na zaklade nejakych casovych pravidel...

Napr. zakaznik ze skupiny X muze zapisovat do objektu Y pouze do patku kazdy tyden, ale jen pokud nema priznak VIP :)

Na univerzalni reseni bych moc nevsazel. Je sranda si neco takoveho naprogramovat, ale v praxi je pak programator zklaman, kdyz zakaznik chce nejakou ichtylovinu, ktera do domeny jeho reseni jaksi nepasuje.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.