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.
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ě:)
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.
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.
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()...
Ja si myslim ze lidi co tohle ctou uz neco malo o PHP vi a s touhle problematikou jsou obeznameni ;-)
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.
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.
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().
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.
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 :/
MySQL dovoluje i čísla předávat jako řetězce, čehož se dá právě spolu s magic_quotes_gpc využít.
I kdyz to dovoluje nedochazi potom ke zbytecne konverzi retezce na int?
K tomu dochází tak jako tak. MySQL dostane buď řetězec "123" nebo "'123'" a převod na číslo musí udělat stejně.
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...
No ano, ale z řetězce na číslo se to převede tak jako tak.
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.
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.
Jakub Vrána :
Kde přesně podle tebe musí být jaké uvozovky?
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.
v6ak:
Doháje, teď se mi Opera Mini snaží popřít determinismus!
Jakub Vrána :
Pro úplnost dodám, že místo apostrofů by tam mohlo být i intval($_GET["id"]).
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.
Diskuse je zrušena z důvodu spamu.