Oprávnění na frontendu

Oprávnění na frontendu

Dokumentace konkrétních zdrojů v EdeeShop a vazba na oprávnění se nachází v referenční části.

Základní mechanismus oprávnění je následující:

  1. existují chráněné zdroje (SecuredResource) - typicky se jedná o druh objektu, ke kterému chceme řídit přístup pomocí oprávnění (příkladem buď třeba fakturační adresa, dodací adresa, osobní údaje uživatele atp.)
  2. existuje sada práv (Right), které opravňují k provedení akce nad chráněným zdrojem (příkladem je třeba právo vytvářet, editovat, mazat nebo prohlížet)
  3. vývojář funkcionality definuje jaké právo musí uživatel mít, aby mohl vykonat konkrétní operaci s chráněným zdrojem
  4. na konkrétním projektu se potom definují role, které seskupují nastavení sady práv k určitému chráněnému zdroji do tzv. seznamu přístup (AccessControlList)
  5. každý uživatel (ať přihlášený nebo anonymní) má sestavený unikátní seznam přístupů (AccessControlList), který je součinem seznamu přístupů všech jeho rolí (nepřihlášený uživatel má automaticky neviditelnou roli ANONYMOUS, ke které je také možné sestavit ACL)

Zkopírovat odkaz na sekciChráněné zdroje

Jsou definovány na úrovni konkrétní feature pomocí anotace @EdeeModuleFeatureSecuredResource a jsou určeny svým unikátním názvem. Pro pojmenování používejte velká písmena, slova oddělujte podtržítky. Každá feature si může definovat tolik chráněných zdrojů, kolik chce. Vždy ovšem definuje jen ty zdroje, které sama primárně spravuje.

Zdrojů může být nekonečné množství.

Zkopírovat odkaz na sekciPráva

Existuje základní sada oprávnění:

  • R - read, právo pro čtení/zobrazení dat
  • C - create, právo na vytvoření nového objektu daného typu
  • U - update, právo na úpravu/editaci existujícího objektu
  • D - delete, právo na odstranění existujícího objektu
  • S - super user, super uživatelské právo, žolík za všechna oprávnění, vypíná omezení v podmínkách načítání

Doporučuje se, aby vývojáři přidávali práva opatrně a spíše využívali existující sady oprávnění. Přidání nového práva je však možné pomocí anotace EdeeModuleFeatureRight. Každé právo musí být označeno unikátním písmenem.

Zkopírovat odkaz na sekciRole

Role se definují v konfiguraci registration feature a nikoliv v kódu. Pro ilustraci si pojďme ukázat kousek konfigurace:

xml
1 <user>2    <registration>3        <roles>4            <role>5                <systemName>B2C</systemName>6                <name>Fyzická osoba</name>7                <description>Zákazník - fyzická osoba.</description>8                <assignWhen>eq(#user.userForm!INDIVIDUAL#,INDIVIDUAL)</assignWhen>9                <resources>10                    <resource>11                        <name>BILLING_ADDRESS</name>12                        <rights>R</rights>13                    </resource>14                    <resource>15                        <name>DELIVERY_ADDRESS</name>16                        <rights>CRUD</rights>17                    </resource>18                    <resource>19                        <name>INVOICE</name>20                        <rights>R</rights>21                    </resource>22                    <resource>23                        <name>ORDER</name>24                        <rights>CR</rights>25                    </resource>26                    <resource>27                        <name>PERSON</name>28                        <rights>RUD</rights>29                    </resource>30                </resources>31            </role>32            <role>33                <systemName>B2B</systemName>34                <name>Firma</name>35                <description>Zákazník - firma.</description>36                <assignWhen>eq(#user.userForm!INDIVIDUAL#,LEGAL_ENTITY)</assignWhen>37                <resources>38                    <resource>39                        <name>PERSON</name>40                        <rights>RUD</rights>41                    </resource>42                </resources>43            </role>44            <role>45                <systemName>ANONYMOUS</systemName>46                <name>Nepřihlášení uživatelé</name>47                <description>Role pro nepřihlášeného uživatele.</description>48                <resources>49                    <resource>50                        <name>COMPANY</name>51                        <rights>C</rights>52                    </resource>53                    <resource>54                        <name>COMPANY_STAFF</name>55                        <rights>C</rights>56                    </resource>57                    <resource>58                        <name>PERSON</name>59                        <rights>C</rights>60                    </resource>61                </resources>62            </role>63        </roles>64    </registration>65</user>

Po nastartování aplikace se tyto role automaticky založí v registration modulu, případně se zaktualizují jejich názvy a popisky podle dat uvedených v této konfiguraci. Výše uvedené nastavení znamená, že:

Nepřihlášený uživatel

bude mít právo založit společnost (tj. registrovat se jako firma) a založit svůj účet oprávněného uživatele k této firmě. Dále má právo se registrovat jako obyčejný uživatel (právo CREATE na PERSON)

B2B uživatel

Má právo zobrazit, editovat a odstranit svůj účet, ale nic víc.

B2C uživatel

Zobrazit fakturační adresu. Zobrazit, editovat a odstranit dodací adresu. Vytvořit a zobrazit objednávku. Zobrazit fakturu. Má právo zobrazit, editovat a odstranit svůj účet, ale nic víc.

Pokud by měl uživatel všechny role, práva k jednotlivým chráněným zdrojům se sečtou.

Zkopírovat odkaz na sekciAutomaticky nastavované role

V definici rolí je možné pomocí atributu assignWhen definovat podmínku, která se vyhodnocuje nad objektem uživatele (proměnná user) a v případě, že je platná, je uživateli při uložení přiřazena automaticky tato role. Pokud splněná není je automaticky tato role odebrána. Toto umožňuje zjednodušit byznys logiku aplikace.

Zkopírovat odkaz na sekciSystémové role

Existují dvě systémové role, které se neukládají v databázi ale je na ně možné nastavovat oprávnění:

  • ALL - roli mají všichni uživatelé - přihlášení i nepřihlášení, jedná se o nejnižší úroveň oprávnění v aplikaci
  • ANONYMOUS - roli má jakýkoliv nepřihlášený uživatel
  • LOGGED_IN - roli má jakýkoliv přihlášený uživatel

Na tyto role je možné nastavit "sdílená" oprávnění uživatelů.

Zkopírovat odkaz na sekciOvěřování oprávnění na aplikační vrstvě

Každá feature která definuje chráněný zdroj je odpovědná si přístupy k němu také ohlídat. Primární místo, kde se přístupy hlídají je servisní vrstva! Existuje záložní mechanismus na datové vrstvě v podobě třídy DaoFirewall, která je ovšem použitelná pouze pro ADaM implementace a která přístupy hlídá na úrovni typů ADaM entit. Osobně se mi osvědčil přístup zápisové metody (tj. metody s vedlejšími dopady) hlídat na servisní vrstvě a čtecí metody nechat na DaoFirewall. Obecně DaoFirewall kontroluje všechny přístupy k entitám v ADaMovi, které nejsou výše ohlídány bezpečnostními anotacemi na servisní vrstvě. Tj. pokud nehlídáte ručně vy, snaží se automaticky hlídat on.

Jakmile je uživatel přihlášený do administrace Edeeho a kouká na stránky Edeeho je zapnut tzv. super administrátorský režim, kdy se žádná frontendová oprávnění nevyhodnocují.

Zkopírovat odkaz na sekciHlídání metod servisní vrstvy

Probíhá pomocí Spring Security anotací @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter - dokumentace je k nim dostupná na stránkách Spring Security knihovny. Doporučuji vytvářet si vlastní anotace, kde je SpEL výraz obalený do nějakého smysluplného pojmenování a není třeba dlouze studovat, co vlastně kontroluje. Doporučuji si přečíst tento článek, ve kterém rozebírám, proč je to dobrý nápad. Náš registration modul umí navíc oproti standardní Spring Security tyto anotace kombinovat - např.:

java
1 @HasCreateCompanyRight2@HasUpdateCompanyRight3public void storeAndApproveCompany(Company company, String approvedBy) {4
5}

kde

java
1 @PreAuthorize(HasCreateCompanyRight.HAS_CREATE_RIGHT_TO_COMPANY)2public @interface HasCreateCompanyRight {3	String HAS_CREATE_RIGHT_TO_COMPANY = "loggedInUser.hasRight(#C).to(#COMPANY)";4}5
6@PreAuthorize(HasUpdateCompanyRight.HAS_UPDATE_RIGHT_TO_COMPANY)7public @interface HasUpdateCompanyRight {8	String HAS_UPDATE_RIGHT_TO_COMPANY = "loggedInUser.hasRight(#U).to(#COMPANY, #company)";9}

Potom je tato kombinace vyhodnocena jako kdybyste zapsali:

1 (loggedInUser.hasRight(#C).to(#COMPANY)) or (loggedInUser.hasRight(#U).to(#COMPANY, #company))

Typicky budete chtít kombinovat logickým OR, ale pokud byste potřebovali AND je možné využít anotace @RulesRelation(AND).

Pokud se výraz nevyhodnotí kladně, bude v daném místě vyvolána výjimka AccessDeniedException a pokud se ještě nezačala vykreslovat stránka, tak uživatel standardně uvidí HTTP 403 stránku.

Tímto způsobem je možné hlídat provolávání public metod na servisní vrstvě. Nezapomínejte, že aby Spring dokázal zasáhnout do volání metody, musí se jednat o volání beany z vnějšku. Volání mezi různými metodami jedné beany už Springem kontrolované není (tj. je to stejné chování jako i u jiných AOP anotací jako je @Transactional nebo @Cacheable).

Zkopírovat odkaz na sekciVolání povoleno všem

Pokud potřebujete na servisní vrstvě povolit přístup k metodě z pohledu libovolného uživatele použijte anotaci @AllowedForAll. Pozor použití této anotace také znamená, že nebudou automaticky do Query objektu, který se dotazuje do databáze vložené ochranné podmínky kontextu aktuálního uživatele. Volání se zachová stejně, jako kdyby metodu volal administrátor se super administrátorskými oprávněními. Je tedy nanejvýš vhodné tyto anotace nezneužívat a využívat je pouze na místech, kde se např. zjišťuje existence nějakého objektu nebo v takovém kontextu, který vylučuje zneužití.

Zkopírovat odkaz na sekciOmezené vyhodnocení oprávnění

Pokud se ověřují oprávnění pomocí anotace na servisní vrstvě, má se na nižších vrstvách za to, že již byla oprávnění prověřena a žádné další ověření se již neprovádí. Existují však situace, kdy je možné na servisní vrstvě ověřit pouze část oprávnění ke známé sadě chráněných zdrojů. Příkladem budiž objednávka, kde se při vytváření objednávky ověří oprávnění CREATE na zdroj ORDER. V rámci dynamických validátorech se ale provádí předem neznámá práce s dalšími zdroji, ke kterým nelze takto na úrovni OrderService oprávnění vyhodnotit. Proto je vhodné použít další anotaci AuthorizationAppliesOnlyTo, kterou je možné specifikovat seznam zdrojů, které byly skutečně ověřeny.

Zkopírovat odkaz na sekciVýrazový jazyk v podmínkách

V podmínkách se používají standardní Spring Security výrazy, ale nepoužíváme jejich abstrakci oprávnění. Oproti základním možnostem je k dispozici navíc výraz:

loggedInUser zastupující objekt přihlášeného uživatele

Na objektu je nově možné použít metody traitu UserWithPermissions pro vyhodnocení oprávnění přístupu a to konkrétně v tomto formátu:

1 loggedInUser.hasRight(#C).to(#COMPANY)2loggedInUser.hasRight(#U).to(#COMPANY, #company)

Jako proměnné jsou (tj. uvozené znakem #) jsou automaticky zaregistrovány všechny typy práv (tj. v příkladu je použit výraz #C, který se přeloží do oprávnění create) a také všechny chráněné zdroje (tj. v příkladu je použit výraz #COMPANY, který se přeloží jako chráněný zdroj COMPANY). Vyhodnocení oprávnění se deleguje na třídu PermissionService a ta dále vyvolá vaši implementaci SecuredResourceKeeper (viz. kapitola vlastní logika ověření oprávnění).

Jako proměnné (tj. uvozené znakem #) můžete použít argumenty volání dané metody (např. ve výše uvedeném příkladě je to argument #company, který obsahuje objekt předaný při volání metody v argumentu pojmenovaném company).

Zkopírovat odkaz na sekciVlastní logika ověření oprávnění

Pokud potřebujete vyhodnocovat oprávnění přístupu nikoliv obecně na typ chráněného zdroje ale konkrétně na nějakou instanci tohoto zdroje (tj. dejme tomu, že typ chráněného zdroje je firma, potom instance zdroje je firma FG Forrest), pak musíte implementovat vlastní SecuredResourceKeeper, ve kterém si toto oprávnění vyhodnotíte ručně.

Jsou 2 typy rozhraní tohoto typu:

  • PojoSecuredResourceKeeper umožňuje hlídat obecně libovolné POJO, definujete typ chráněného zdroje který hlídáte a implementujete vyhodnocení v metodách hasRight

  • AdamSecuredResourceKeeper umožňuje hlídat ADaM entity, definujete seznam ADaM entity (viewName), které touto implementací hlídáte a implementujete vyhodnocení v metodách hasRight a metodě alterQuery

    Metoda alterQuery vám umožňuje upravit každý dotaz směřující na sledovaný typ entity tak, aby obsahoval podmínky, které zajistí, že aktuální uživatel dostane vrácené pouze ty instance, pro které má požadované právo. Pokud má uživatel super administrátorské právo tato metoda se vůbec nevyvolává. Pokud by metoda nedokázala upravit vstupní dotaz na bezpečnou formu MUSÍ vyvolávat AccessDeniedException, jinak ručí za to že výsledný dotaz je z pohledu oprávnění bezpečný.

Tyto dvě rozhraní lze i kombinovat (a bude to časté). Současně nesmí existovat dva různé SecuredResourceKeeper, které by hlídaly přístup k jednom typu chráněného zdroje!

Zkopírovat odkaz na sekciElevace vlastní logiky ověření oprávnění

Díky dekompozici eshop modulu do maličkých feature dochází k problému, že základní logika nedokáže vyhodnotit správně oprávnění, která jsou rozšířená jinou featurou. Pro lepší vysvětlení si pomůžeme konkrétním příkladem:

  • featura pro správu adres uživatele definuje zdroje deliveryAddress a billingAddres a ty váže na konkrétního uživatele sloupcem customer
  • další featura pro správu společností, která je závislá na adresách uživatele (nikoliv opačně) přidává novou funkcionalitu, ve které umožňuje některým uživatelům spravovat adresy za celou firmu a tím se adresy mezi uživateli stejné firmy sdílí
  • tím pádem není již možné využít sloupce customer a definuje se separátní sloupec companyId
  • zároveň si feature pro správu společností definuje nové chráněné zdroje companyDeliveryAddress a companyBillingAddress
  • o tomto nemohla featura správy adres uživatele vědět a díky obrácené závislosti ani nemůže na třídě CustomerAddressService nijak počítat se security anotacemi, které jsou definovány v JARu feature pro správu společností (nevidí na ně a je to tak i správně)

Jak je vidět na výše uvedeném případě, jakékoliv volání na třídě CustomerService by skončilo s AccessDeniedException pro uživatele, který by měl nastaveny práva pouze pro chráněné zdroje companyDeliveryAddress a companyBillingAddress.

Pro tyto účely existují tzv. ElevatedSecuredResourceKeeper - konkrétně tyto dvě rozhraní:

  • PojoElevatedSecuredResourceKeeper
  • AdamElevatedSecuredResourceKeeper

Obě jsou analogická k dříve představeným typům rozhraní SecuredResourceKeeper a mají i podobný význam. Slouží k tomu, aby zvrátily negativní rozhodnutí přístup na základní typy chráněných zdrojů umožnili do již zakompilovaných pravidel vstoupit a ovlivnit je z externího místa (feature).

Zjednodušeně elevační implementace dokáže říci: "pokud dojde k zamítnutí přístupu uživatele k chráněném zdroji deliveryAddress zeptej se ještě mojí implementace, jestli náhodou nemá uživatel oprávnění na zástupný chráněný zdroj companyDeliveryAddress". Toto analogicky platí i pro metodu alterQuery, která v tomto případě upraví ADaM dotaz s podmínkou na eq(customer,#lookup[loggedInUser].id#) na dotaz s podmínkou eq(companyId,#lookup[loggedInUser].companyId#). Tím pádem se firemnímu uživateli začnou na frontendu na místech, kde se původně zobrazovaly "osobní" adresy uživatele zobrazovat adresy firmy.

Elevační implementace lze i kombinovat, ale prozatím pro to nemáme využití.

Zkopírovat odkaz na sekciPoužití v administračních úlohách

Jelikož v asynchronních úlohách není přihlášený ani administrátor v Edeem (což by automaticky vyvolávalo superadministrátorský režim) ani frontendový uživatel (což by vyhodnocovalo práva toho konkrétního uživatele), skončila by všechna volání chráněných metod výjimkou AccessDeniedException. Proto je nutné volání v asynchronních úlohách na pozadí obalit klauzulí super uživatelského přístupu nebo nějakého konkrétního uživatele. Tj. typický modus operandi bude:

java
1 public class CompanyInitializationJob extends AbstractGreedyUniqueJob<DailyJobConfiguration> implements ApplicationListener<CompanyCreatedEvent> {2	3	@Override4	public int getQueuedItemCount() {5		return executeAsSuperUser(companyService::getCompanyToInitializeCount);6	}7
8	@Override9	protected int doUniqueJobTurn() {10		return executeAsSuperUser(() -> companyService.initializeCompanies(BATCH_SIZE));11	}12
13}

Zkopírovat odkaz na sekciPoužití v automatických testech

Testy jsou nastaveny tak, že oprávnění je NUTNÉ splnit i v automatických testech. Je to tak cíleně - chceme psát už i automatické testy tak, že vezmeme oprávnění v potaz. Tj. pro testy existuje analogická konfigurace registration featury na classpath testů, kterou je možné o další role, zdroje a práva doplňovat.

V testech lze využívat funkcionalit z Registration modulu:

  • anotace @RunAsUser a @RunAsAdmin
  • statické metody z třídy com.fg.registration.test.RunAsSupportTestExecutionListener
  • taktéž je možné vyvolat režim super administrátora (kdy se oprávnění vůbec nevyhodnocují) pomocí metody PermissionService.executeAsSuperUser

V testech se počítá s tím, že vracení uživatelů se bude mockovat a proto je hlavní reference userManager instance mockována pomocí Mockito. Místo mocku se však jedná spíše o spy objekt, který deleguje nenamockovaná volání přímo na reálnou instanci userManager z registration modulu, který v testech také startuje.

Mockované instance uživatelů lze předávat i jen pro volání chráněné metody. Ukázky kódu:

java
1 // checks done in SUDO mode2PermissionService.executeAsSuperUser(() -> {3    assertNull(companyService.getCompanyById(newCompany.getId()));4});

V příkladu je využit SUDO režim, kde se práva vůbec nevyhodnocují.

java
1 final User companyOwner = new MockUser(2    permissionService, "RJE", company.getId(), COMPANY_OWNER, ROLE_B2B3);4executeInContextOf(companyOwner, applicationContext, () -> {5    companyService.storeAndApproveCompany(newCompany, "RJE");6});

V příkladu si vytváříme Mock uživatele, který si hraje na uživatele s rolí B2B a vlastníka firmy a zároveň mu nastavujeme identifikátor firmy na konkrétní, kterou v testu používáme. Tím pádem se povede provolání metody storeAndApproveCompany, kterou může vyvolat pouze vlastník společnosti (respektive ten, kdo má právo U na COMPANY).

Uživatele a sekce můžete volně kombinovat a v rámci jednoho testu mít v různých částech kontexty přihlášených různých uživatelů.

Nezapomeňte testovat i negativní případy, kdy je naopak přístup zamítnut - příklad:

java
1 @Test(expected = AccessDeniedException.class)2public void shouldFailToChangeUserRoleInCompanyAsUnauthorized() {3    final Company newCompany = createAndApproveCompanyAsAdmin();4    final CompanyStaff companyStaff = createAndApproveCompanyStaffAsOwner(newCompany);5
6    executeInContextOf(getBfuUser(newCompany), applicationContext, () -> {7        final CompanyStaff staff = companyService.getCompanyStaff("JNO", newCompany);8        assertNotNull(staff);9        assertEquals("seňor", staff.getCompanyRole());10
11        companyStaff.setCompanyRole("miňor");12        companyService.storeCompanyStaff(companyStaff);13    });14}

Zkopírovat odkaz na sekciOvěřování oprávnění v GUI

Na aplikační vrstvě používejte pro omezení přístupu na stránky metadata:

Z registration modulu:

Lze použít pouze na úrovni stránky:

  • restrictAccess pokud chcete ovlivnit přístup pouze ke konkrétní stránce
  • restrictAccessToSection pokud chcete ovlivnit přístup k dané stránce a všem podstránkám

Jako hodnoty metadata můžete použít:

Z eshop modulu:

Lze použít jak na úrovni stránky (pokud se vyhodnotí jako neplatné uživatel dostává HTTP 403 - Access forbidden), tak i na úrovni stateful komponenty (pokud se vyhodnotí jako neplatné, komponenta se chová jako disabled).

  • security s podřízenými elementy
    • resource definuje chráněný zdroj, více typů může být odděleno čárkami (vazba je logické OR) nebo plusem (vazba logické AND), lze použí i RjEL výraz, je možné použít jako přímo název zdroje (např. DELIVERY_ADDRESS), tak i camel case notaci (např. deliveryAddress)
    • right definuje chráněný zdroj, více typů může být odděleno čárkami (vazba je logické OR), nebo plusem (vazba logické AND), lze použí i RjEL výraz, je možné použít jak zkratku práva (např. R) tak i plný název (např. read)
    • instance vyžaduje RjEL výraz, který vrátí konkrétní objekt vůči, kterému má být výpočet oprávnění proveden

Bezpečnostní metadata na komponentách se dědí směrem dolů. Je možné na nadřízené komponentě např. definovat pouze metadata:

xml
1 <security>2   <resource>deliveryAddress</resource>3</security>

A na podřízených odkazech nebo tlačítkách potom definovat už jen:

xml
1 <security>2   <right>update</right>3</security>