Zabezpečení veřejné části Edee.one pro B2B

Pro B2C e-shopy, vyjma administrační části, extra zabezpečení nepotřebujete. Realizace B2B e-shopů nás však donutila k implementaci robustnější bezpečnostní vrstvy nad frontendovou částí aplikace.

Na zabezpečení klademe následující požadavky:

  • bude co nejjednodušší pro vývojáře - komplexita znamená skrytý vnitřní odpor se bezpečností zabývat
  • bude mít více úrovní jištění - i kdyby vývojář zapomněl některou z částí kódu pokrýt, bude existovat automatizovaný záložní systém, který zasáhne za něj
  • bude využívat obecně známé knihovny / principy, aby byl dobře stravitelný i pro nováčky
  • bude přizpůsobitelný každé implementaci na míru

Jak se nám to podařilo, můžete posoudit sami, když budete číst dále.

Mechanismus definice práv

Jednotlivé části aplikace si definují takzvané chráněné zdroje. Těch může být v aplikaci velké množství. Pokud si daná část aplikace definuje chráněný zdroj, je také primárně zodpovědná za jeho správu (tj. je primární držitel a správce tohoto typu dat). Příkladem může být např. objednávkový modul, který definuje chráněný zdroj objednávka a je zodpovědný za veškerý přístup k objednávkám a práci s nimi.

V aplikaci je také definována sada práv, do které patří tato minimální sada oprávnění:

  • create - vytvoření nového záznamu
  • update - aktualizace existujícího záznamu
  • read - čtení existujícího záznamu
  • delete - odstranění existujícího záznamu
  • super user - žolíkové oprávnění, které zastupuje libovolné oprávnění

Jednotlivé části aplikace si mohou definovovat i nové typy oprávnění, ale doporučení je, snažit se přidržet pokud možno této základní sady práv a teprve pokud potřebujeme granulárnější členění, je možné si přidat oprávnění další.

Posledním použitým termínem je role. Role slouží pouze k tomu, aby umožňovala definovat, jaká oprávnění ke konkrétním chráněným zdrojům zpřístupňuje. Definice rolí je implementační záležitost na konkrétní aplikaci. Je možné například definovat role:

Vlastník společnosti

Přidělíme mu úplná práva (CRUD) k adresám společnosti (fakturační a dodací údaje), práva vytvořit a prohlížet objednávky (CR), právo vidět faktury (C).

Zaměstnanec společnosti

Přidělíme mu práva pouze ke čtení adres společnosti (fakturační a dodací údaje), práva vytvořit a prohlížet objednávky (CR) a víc už nic.

Každý uživatel (i ten anonymní - tj. nepřihlášený) má při prvním přístupu na e-shop sestavený tzv. access control list (neboli ACL), který zkombinuje všechna nastavení (práv k chráněným zdrojům) ze všech rolí, které jsou tomuto uživateli přiřazeny.


Proč jsme zvolili právě tento mechanismus?

V počítačovém světě je zcela běžný a uživatelé jsou na něj zvyklí. V rámci analýzy s klienty řešíme pouze základní role - tedy standardizované typy uživatelů, kteří se systémem budou pracovat a k čemu budou oprávněni. Klient tak typicky pracuje s termíny, se kterými je zvyklý pracovat i ve svém byznysu a nemusí se ani terminologicky ničemu přizpůsobovat (přizpůsobujeme se my).

Programátory zase vůbec nezajímají role, ale pracují pouze na úrovni chráněný zdroj + oprávnění. To je pro ně opět blízký a jednoduchý koncept.

O tvorbu a vyhodnocení ACL se stará univerzální mechanismus, který je napsán a otestovan pouze jednou a zákazníky ani programátory nijak extra nezajímá. Koncept ACL je ovšem opět všeobecně známým mechanismem a je jednoduše stravitelný i pro nováčky, kteří s naším systémem zabezpečení přichází do kontaktu poprvé.

Tři úrovně zabezpečení

Kontrolu zabezpečení je třeba aplikovat hned na několika místech aplikace, která se vzájemně doplňují.

Prezentační vrstva

Komponentový systém na naší horní vrstvě, která vytváří uživatelské rozhraní pro uživatele, umožňuje pomocí metadat definovat na úrovni jednotlivé komponenty (či bloku komponent), jaká práva k jakým chráněným zdrojům musí uživatel mít, aby se mu zpřístupnily (čili zobrazily) konkrétní tlačítka, sloupce, odkazy či jiné informace.

Ukázkové nastavení vypadá například takto:

<security>
    <resource>deliveryAddress</resource>
    <right>read</right>
</security>

Nastavení umožňuje vyhodnocovat metadata dynamickým výrazem, kombinovat oprávnění (např. pro tento blok potřebuje uživatel oprávnění create nebo update pro zdroj deliveryAddress) atp.

Vyřešení oprávnění na prezentační vrstvě však spočívá v pouhém “označkování” existujících komponent a je velice triviální jak pro Java vývojáře, tak i HTML kodéry.

Na autorizaci na prezentační vrstvě se samozřejmě nelze vůbec spoléhat. Jeden z nejčastějších útoků na autorizaci je záměna identifikátorů v HTTP požadavcích a tomuto útoku je třeba zabránit na vrstvách nižších.

Aplikační vrstva

Na aplikační vrstvě používáme osvědčenou knihovnu Spring Security, kterou jsme si pouze přizpůsobili a rozšířili tak, aby se nám s ní pracovalo dobře. Aplikování bezpečnostního pravidla znamená pro programátora na aplikační vrstvě přidat jednu nebo více bezpečnostních anotací.

@PreAuthorize("loggedInUser.hasRight(#U).to(#COMPANY_ADDRESS, #company)")
void storeDeliveryAddress(Company company, Address address) {
    … aplikační logika …
}

Jelikož se při tomto zápisu brzy ztrácí přehlednost, je lepší bezpečnostní pravidla zabalit do vlastních anotací a metoda pak může vypadat nějak takto:

@HasUpdateCompanyAddressRight
void storeDeliveryAddress(Company company, Address address) {
    … aplikační logika …
}

Anotace následně podléhají analýze anotačních procesorů a pomocí AOP techniky Spring Framework vkládá do automatizovaně vytvářených proxy kousky autorizační logiky, aniž bychom museli “špinit” aplikační kód detailní autorizační logikou.

Téměř všechny metody na aplikační vrstvě by na sobě měly mít autorizační anotaci. Jsou však i legální případy, kdy tomu tak být nemůže nebo to postrádá smysl. Proto není možné napsat nějaké ověřovací lintovací pravidlo.

Pokud by programátor omylem zapomněl přidat autorizační anotaci na aplikační vrstvě, máme ještě záložní kontrolu na vrstvě datové.

Datová vrstva

Díky použití vlastní Data Mapper knihovny máme plnou kontrolu nad komunikací na úrovni datové vrstvy. Data v databázi jsou organizována do tzv. entit a jednotlivé části aplikace, které definují chráněné zdroje, zároveň specifikují, které entity v datovém modelu tyto chráněné zdroje reprezentují.

Při pokusu o přístup k entitě na datové vrstvě dokážeme automatizovaně ověřit - aniž by programátor musel cokoliv dělat - zda má uživatel odpovídající základní právo (CRUD) na entitu, která se do databáze ukládá nebo se z ní načítá. To nám poskytuje záchytnou síť pro momenty, kdy by programátor opomněl na autorizační anotaci na aplikační vrstvě.

Specifickou funkcionalitu je nutné dodat pro čtecí operace. Prosté právo “číst faktury” nestačí - potřebujeme rozlišit, které konkrétní faktury (tj. faktury patřící danému uživateli nebo firmě, kterou zastupuje) může uživatel číst.

Aby nám správně fungovalo stránkování a kód i z hlediska výkonnosti běžel optimálně, nemůžeme vylučovat entity, na které nemá uživatel právo, až po jejich načtení z databáze. Musíme už zkonstruovat dotaz do databáze tak, aby byly vráceny pouze objekty, na které má aktuální uživatel právo ke čtení. Zároveň nechceme, aby toto musel na více různých místech programátor hlídat ručně, protože tím otevíráme prostor k lidské chybě.

Pro tyto účely programátor dodá implementaci jediného rozhraní, ve kterém zajistí úpravu všech dotazů do relační databáze tak, aby dotazy mohly vrátit pouze objekty určené pro přihlášeného uživatele. Úprava dotazu je možná jen díky tomu, že naše data mapper knihovna používá jako meziformát objektovou formu dotazu podobnou AST stromu, který známe ze světa kompilátorů. Tato ochranná vrstvička prohledá každý dotaz a zjistí, jestli je tam potřebná podmínka, která omezuje přístup k objektům pro přihlášeného uživatele, a pokud nikoliv - tuto podmínku doplní.

Podobným způsobem jsou chráněny i další operace jako je uložení nebo výmaz objektů.

Automatizované testy a plánované úlohy

Na autorizaci je nutné myslet i v automatizovaných testech. Integrační automatizované testy již musí počítat s autorizací a je třeba v testech “mockovat” přihlášeného uživatele, případně super uživatele v situaci, kdy nějaký krok integračního testu vyžaduje akci administrátora. Výhoda tohoto přístupu je, že programátor spolu s funkcionalitou zároveň kontroluje i správně nastavené autorizační výrazy. Ačkoliv by se mohlo zdát, že změna kontextů přihlášených uživatelů testy nezpřehlední - opak je pravdou. Posuďte sami, takto vypadá zavolání metody pro aktualizaci firmy v kontextu jejího vlastníka:

final User companyOwner = new MockUser(
    permissionService, "RJE", company.getId(), COMPANY_OWNER, ROLE_B2B
);
PermissionService.executeInContextOf(companyOwner, applicationContext, () -> {
    companyService.storeAndApproveCompany(newCompany, "RJE");
});

Pro činnosti prováděné administrátorem nebo systémem stačí logiku obalit takto:

PermissionService.executeAsSuperUser(() -> {
    assertNull(companyService.getCompanyById(newCompany.getId()));
});

Stejný přístup je třeba používat i v asynchronních úlohách, které spouští na pozadí systém mimo kontext jakéhokoliv uživatele. Buď musí emulovat konkrétního uživatele, pro kterého danou operaci provádí nebo musí spustit režim super administrátora, který ověřování práv po dobu běhu úlohy vypne.

Je třeba, aby vývojář v testech pokryl i situaci, kdy konkrétní akce nebude z důvodu nedostatečných oprávnění povolena:

@Test(expected = AccessDeniedException.class)
public void shouldFailToChangeUserRoleInCompanyAsUnauthorized() {
    final Company newCompany = createAndApproveCompanyAsAdmin();
    final CompanyStaff companyStaff = createAndApproveCompanyStaffAsOwner(newCompany);
    executeInContextOf(getBfuUser(newCompany), applicationContext, () -> {
        final CompanyStaff staff = companyService.getCompanyStaff("JNO", newCompany);
        companyService.storeCompanyStaff(companyStaff);
    });
}

Závěrem

Věříme, že takto navržená autorizační vrstva přinese pro frontendovou část aplikace, která je vystavena největšímu zájmu útočníků, dostatečnou úroveň bezpečnosti a zároveň příliš nezatíží naše programátory a tudíž nezvýší implementační náklady projektu.

Samozřejmě nezůstáváme pouze u víry a naše řešení budeme verifikovat s penetračními testery (white hat hacker), se kterými dlouhodobě spolupracujeme.

Jan Novotný, Senior Application Developer

Chcete zapojit Edee.one
do budování úspěchu
své společnosti?