Skoncujte s anonymitou koncových uživatelů (1/2)

Znalost identity koncového uživatele ve všech vrstvách systému je základní nutností při tvorbě bezpečných aplikací. Dnes si ukážeme, jak může program přes Client Identifier předávat databázovému serveru tuto informaci i v případě, kdy aplikace sdílí stejné připojení do databáze pro všechny uživatele, jak je to běžné v dnešních webových aplikacích. Představte si ne úplně neobvyklou situaci. Aplikace, kterou máte na starost, najednou přestala fungovat. Po prvním ohledání zjistíte, že důvodem je prázdná tabulka zákazníků. Normálně byste nejspíš nejprve řešil obnovu ztracených dat, ale předpokládejme, že jste měli dobře nastavenou zálohovací strategii a data se vám podařilo obnovit. Přesto vám v hlavě nepřestává hryzat neodbytná otázka - Jak se to mohlo stát? Jste si téměř jistý, že jste poslední dobou nedělal žádnou údržbu, v rámci které by mohl obsah tabulky zmizet. Jenže on zmizel a vy nevíte, kdo to má na svědomí.
Naštěstí jste tabulku zákazníků považoval za kritickou a nastavil audit tak, aby se evidovaly všechny potenciálně nebezpečné operace. Stačí se tedy podívat v databázi do auditního logu, co se dělo s tabulkou zákazníků:


SELECT timestamp, os_username, username, userhost, obj_name, action_name
FROM dba_audit_trail
WHERE obj_name='CUSTOMERS' and ACTION_NAME='DELETE'
and timestamp>sysdate-1;

timestampos_usernameusernameuserhostobj_nameaction_name
17:50oracleappservmujappserverCUSTOMERSDELETE

Měl jste štěstí - za poslední den se v tabulce CUSTOMERS mazala data jen jednou. Víte tedy, kdy k tomu došlo. Jenže kdo k tomu dal příkaz? Uživatelské jméno i jméno klientského počítače vás navedlo pouze na jednu z vašich webových aplikací. Ale s tou pracuje 500 lidí! Jak poznáte, který z nich to byl?
Tento příklad ukazuje nepříjemný vedlejší efekt obvyklé architektury současných webových aplikací. Všude se dočteme o tom, jak je z výkonnostních důvodů vhodné využívat Connection Pool a sdílet tak stejné databázové spojení mezi různými koncovými uživateli. Prakticky vůbec se ale nemluví o tom, jak velkou cenu za to platíme - významně totiž oslabujeme úroveň zabezpečení aplikace. Na zabezpečení se svým dílem musí podílet každá komponenta informačního systému. A tím, že databázovému serveru nepředáme identitu koncového uživatele, se připravujeme o možnost zabezpečit data hned u zdroje. Databáze pak není schopna detailně řídit přístup jednotlivých koncových uživatelů ani vést audit operací.
Oracle Database přitom nabízí hned dvě technologie, které lze se sdílenými spojeními využít k tomu, aby propagovaly identitu uživatelů až na databázovou vrstvu - Client Identifier a Proxy Authentication.
Dnes se podíváme na jednodušší z obou mechanismů - Client Identifier. Tento mechanismus nevyžaduje, aby koncoví uživatelé byly skutečnými uživateli databáze. Databáze věří aplikaci, že si identitu uživatele ověřila, přebírá jí od ní a uvádí ji v auditním logu či v seznamu aktuálních spojení. Také z pohledu základních metod řízení přístupu (systémových a objektových přístupových práv a rolí) se nic nemění oproti architektuře s běžným sdíleným technickým účtem. Přístupová práva a role lze přidělovat pouze technickému účtu a nikoliv konkrétním koncovým uživatelům.
Client Identifier je ve skutečnosti prostá vlastnost spojení, kterou klient Oracle předává s každým SQL příkazem, a říká tím databázovému serveru: „Teď zrovna používám toto spojení pro uživatele X." Tuto vlastnost můžete kdykoliv změnit. Ideální tedy je nastavit jméno aktuálního koncového uživatele ve chvíli, kdy si berete (například na začátku webové stránky) spojení z Connection Poolu. To je celé, další práce se spojením se už nijak nemění. Snad jen malá poznámka - je dobré před vrácením spojení do Connection Poolu zase Client Identifier vymazat. Zde je postup v Javě, podobné možnosti máte však i v .NET a přímém volání OCI (z ukázky je pro jednoduchost vynecháno obvyklé zpracování výjimek):


void sqlWithClientId (OracleDataSource ds,
String endUserName) throws SQLException {
Connection conn=ds.getConnection();
 
String[] metrics = new String[OracleConnection.END_TO_END_STATE_INDEX_MAX];
metrics[OracleConnection.END_TO_END_CLIENTID_INDEX] = endUserName;
((OracleConnection) conn).setEndToEndMetrics(metrics,(short)0);
 
Statement stmt=conn.createStatement();
stmt.execute("DELETE FROM oe.customers WHERE customers_id=120");
ResultSet rs = stmt.executeQuery("SELECT count(*) FROM oe.customers");
rs.next();
System.out.println("Počet záznamů:" + rs.getLong(1));
rs.close();
stmt.close();
 
metrics[OracleConnection.END_TO_END_CLIENTID_INDEX] = null;
((OracleConnection) conn).setEndToEndMetrics(metrics,(short)0);
 
conn.close();
}

Vidíte, že použití je opravdu jednoduché - 3 řádky kódu při získání spojení a 2 při jeho vracení do Connection Poolu. Navíc v reálné aplikaci byste si nejspíš vytvořili vlastní metodu getConnection a close, která by již přímo nastavovala i Client Identifier a zásah do jednotlivých modulů aplikace by tak byl ještě menší.
Zajímavé je, že samotné nastavení Client Identifier voláním metody setEndToEndMetrics (resp. setClientIdentifier ve starších verzích JDBC driveru), neznamená automaticky volání databázového serveru. Tato informace se uchová na klientu a pošle se databázovému serveru až spolu s následujícím SQL příkazem. Režie této operace je tak prakticky nulová.

Jde o drobnou změnu, ale její efekt je velký:


  1. V auditním logu jste schopni u sledovaných operací identifikovat jak aplikaci (díky technickému účtu), tak i koncového uživatele který operaci provedl. Příklad z úvodu článku by tedy bylo možno rozšířit takto:

    SELECT timestamp, os_username, username, userhost,
    obj_name, action_name, client_id
    FROM dba_audit_trail
    WHERE obj_name='CUSTOMERS' and ACTION_NAME='DELETE'
    and timestamp>sysdate-1;


    timestampos_usernameusernameuserhostobj_nameaction_nameclient_id
    17:50oracleappservmujappserverCUSTOMERSDELETEdavid.krch

    (Více o tom, jak nastavit auditování najdete v Security Guide.)

  2. Ve výpisu databázových spojení (ať již v Enterprise Manageru, nebo pomocí SQL) jste schopni identifikovat, jaký koncový uživatel dané spojení aktuálně používá. To vám může usnadnit diagnostiku a ladění výkonu. Jen je třeba si uvědomit, že změna Client Identifier se do databáze posílá až při dalším SQL příkazu - spojení vrácená do Connection poolu tedy nejspíš budou v databázi stále mít v Client Identifier předchozího koncového uživatele.

    SELECT sid, serial#, username, osuser, machine, client_identifier
    FROM v$session;

    SidSerial#UsernameOsUserMachineclient_identifier
    126535appservoraclemujappserverdavid.krch
    140565appservoraclemujappserverkarel.novak
    13518sysdkrchdkrch-cz 

  3. Pomocí PL/SQL funkce DBMS_MONITOR.CLIENT_ID_TRACE_ENABLE můžete zajistit automatické trasování SQL operace vybraného koncového uživatele do souboru, aniž byste museli trasování explicitně zapínat v každém spojení. To vám opět může usnadnit ladění aplikace.

  4. Na úrovni SQL lze kdykoliv zjistit jméno koncového uživatele používajícího aktuální spojení čtením SQL kontextu:
    UserSession_userclient_identifier
    appservappservdavid.krch

Jak jsem již uvedl výše, pokud předáváte identitu koncového uživatele pouze pomocí vlastnosti Client Identifier, nemůžete přidělovat běžná databázová práva a role koncovým uživatelům individuálně. Lze je přidělovat jen technickému účtu sdílenému všemi uživateli aplikace. V Oracle Database Enterprise Edition ale máte možnost k řízení přístupu využít Virtual Private Database. O této technologii jste si mohli přečíst více na Databázovém světu ve 12. dílu seriálu Tipy/Triky. V tehdy uváděném příkladu bychom provedli jen velmi malou změnu v PL/SQL funkci aby identitu získávala z Client Identifier:


CREATE OR REPLACE FUNCTION obj_vpd_sec (
schema_name IN VARCHAR2,
table_name IN VARCHAR2) RETURN VARCHAR2 AS
l_user_id uzivatele.user_id%TYPE;
l_typ uzivatele.typ%TYPE;
l_podminka VARCHAR2(4000);
BEGIN
-- Zjištění informací o aktuálním uživateli
SELECT user_id,typ
INTO l_user_id,l_typ
FROM uzivatele
WHERE username = sys_context('USERENV','CLIENT_IDENTIFIER');
 
IF (l_typ=1 or l_typ=2) THEN
--Obchodník či manager smí pracovat s objednávkami, které prodal
l_podminka := l_podminka ||'(obchodnik='
|| l_user_id||')';
IF (l_typ = 2) THEN
-- Manager smí navíc pracovat s objednávkami svých podřízených
l_podminka := l_podminka || ' OR obchodnik in ('
|| 'SELECT user_id
FROM objednavky.uzivatele '
|| ' CONNECT BY nadrizeny = PRIOR user_id'
|| ' START WITH nadrizeny ='
|| l_user_id || ')';
END IF;
ELSE
--Zákazník smí pracovat jen se svými objednávkami
l_podminka:= '(zakaznik=' || l_user_id ||')';
END IF;
RETURN l_podminka;
EXCEPTION
WHEN no_data_found THEN
-- Ani zákazník, ani zaměstnanec - nevracet žádná data
return '(1=2)';
END;
/

Při použití VPD si musíte uvědomit, že jedno spojení je ve vaší aplikaci postupně používáno různými koncovými uživateli. VPD politiky proto nelze definovat jako statické (kdy se funkce spouští jen při prvním přístupu spojení k objektu). Musíte je definovat jako dynamické (funkce se spouští pokaždé), nebo tzv. context sensitive (funkce se spustí, pokud předtím došlo ke změně Client Identifier).

Pokud vámi používané rozhraní neumožňuje nastavit Client Identifier přímo, lze jej nastavit pomocí PL/SQL procedury DBMS_SESSION.SET_IDENTIFIER. To už ale samozřejmě znamená běžné volání databáze a režie tak může být vyšší než ve výše popisovaném postupu.
Pokud používáte při programování objektově-relační perzistenční frameworky (jako např. Oracle TopLink/EclipseLink, Hibernate, EJB Entity Beans), může se vám na první pohled zdát, že nelze Client Identifier použít, protože s vlastním objektem Connection nepracujete. Většina z těchto prostředí ale umožňuje nastavovat Client Identifier buď pouhou změnou v konfiguračním souboru, nebo doplněním vlastní třídy, která se volá před a po použití spojení.

Comments:

Post a Comment:
  • HTML Syntax: NOT allowed
About

Česky o všem co se točí kolem Oracle Database.

Autoři:

Patrik Plachý
Technology Sales Consultant

David Krch
Principal Consultant
Oracle Expert Services

Oracle Czech

Search

Archives
« duben 2014
PoÚtStČtSoNe
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today