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í:
- 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.)
- 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)
- vývojář funkcionality definuje jaké právo musí uživatel mít, aby mohl vykonat konkrétní operaci s chráněným zdrojem
- 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)
- 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)
Chrá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í.
Prá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.
Role
Role se definují v konfiguraci registration feature a nikoliv v kódu. Pro ilustraci si pojďme ukázat kousek konfigurace:
<user> <registration> <roles> <role> <systemName>B2C</systemName> <name>Fyzická osoba</name> <description>Zákazník - fyzická osoba.</description> <assignWhen>eq(#user.userForm!INDIVIDUAL#,INDIVIDUAL)</assignWhen> <resources> <resource> <name>BILLING_ADDRESS</name> <rights>R</rights> </resource> <resource> <name>DELIVERY_ADDRESS</name> <rights>CRUD</rights> </resource> <resource> <name>INVOICE</name> <rights>R</rights> </resource> <resource> <name>ORDER</name> <rights>CR</rights> </resource> <resource> <name>PERSON</name> <rights>RUD</rights> </resource> </resources> </role> <role> <systemName>B2B</systemName> <name>Firma</name> <description>Zákazník - firma.</description> <assignWhen>eq(#user.userForm!INDIVIDUAL#,LEGAL_ENTITY)</assignWhen> <resources> <resource> <name>PERSON</name> <rights>RUD</rights> </resource> </resources> </role> <role> <systemName>ANONYMOUS</systemName> <name>Nepřihlášení uživatelé</name> <description>Role pro nepřihlášeného uživatele.</description> <resources> <resource> <name>COMPANY</name> <rights>C</rights> </resource> <resource> <name>COMPANY_STAFF</name> <rights>C</rights> </resource> <resource> <name>PERSON</name> <rights>C</rights> </resource> </resources> </role> </roles> </registration></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.
Automaticky 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.
Systé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 aplikaciANONYMOUS- roli má jakýkoliv nepřihlášený uživatelLOGGED_IN- roli má jakýkoliv přihlášený uživatel
Na tyto role je možné nastavit "sdílená" oprávnění uživatelů.
Ověř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í.
Hlí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ř.:
@HasCreateCompanyRight@HasUpdateCompanyRightpublic void storeAndApproveCompany(Company company, String approvedBy) {
}kde
@PreAuthorize(HasCreateCompanyRight.HAS_CREATE_RIGHT_TO_COMPANY)public @interface HasCreateCompanyRight { String HAS_CREATE_RIGHT_TO_COMPANY = "loggedInUser.hasRight(#C).to(#COMPANY)";}
@PreAuthorize(HasUpdateCompanyRight.HAS_UPDATE_RIGHT_TO_COMPANY)public @interface HasUpdateCompanyRight { String HAS_UPDATE_RIGHT_TO_COMPANY = "loggedInUser.hasRight(#U).to(#COMPANY, #company)";}Potom je tato kombinace vyhodnocena jako kdybyste zapsali:
(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).
Volá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í.
Omezené 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.
Vý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:
loggedInUser.hasRight(#C).to(#COMPANY)loggedInUser.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).
Vlastní 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:
-
PojoSecuredResourceKeeperumožňuje hlídat obecně libovolné POJO, definujete typ chráněného zdroje který hlídáte a implementujete vyhodnocení v metodáchhasRight -
AdamSecuredResourceKeeperumožňuje hlídat ADaM entity, definujete seznam ADaM entity (viewName), které touto implementací hlídáte a implementujete vyhodnocení v metodáchhasRighta metoděalterQueryMetoda
alterQueryvá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ávatAccessDeniedException, 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 jednomu typu chráněného zdroje!
Elevace 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
deliveryAddressabillingAddresa ty váže na konkrétního uživatele sloupcemcustomer - 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
customera definuje se separátní sloupeccompanyId - zároveň si feature pro správu společností definuje nové chráněné zdroje
companyDeliveryAddressacompanyBillingAddress - 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ě
CustomerAddressServicenijak 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í:
PojoElevatedSecuredResourceKeeperAdamElevatedSecuredResourceKeeper
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ému 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í.
Použ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:
public class CompanyInitializationJob extends AbstractGreedyUniqueJob<DailyJobConfiguration> implements ApplicationListener<CompanyCreatedEvent> { @Override public int getQueuedItemCount() { return executeAsSuperUser(companyService::getCompanyToInitializeCount); }
@Override protected int doUniqueJobTurn() { return executeAsSuperUser(() -> companyService.initializeCompanies(BATCH_SIZE)); }
}Použ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
@RunAsUsera@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:
// checks done in SUDO modePermissionService.executeAsSuperUser(() -> { assertNull(companyService.getCompanyById(newCompany.getId()));});V příkladu je využit SUDO režim, kde se práva vůbec nevyhodnocují.
final User companyOwner = new MockUser( permissionService, "RJE", company.getId(), COMPANY_OWNER, ROLE_B2B);executeInContextOf(companyOwner, applicationContext, () -> { companyService.storeAndApproveCompany(newCompany, "RJE");});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:
@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); assertNotNull(staff); assertEquals("seňor", staff.getCompanyRole());
companyStaff.setCompanyRole("miňor"); companyService.storeCompanyStaff(companyStaff); });}Ověř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:
restrictAccesspokud chcete ovlivnit přístup pouze ke konkrétní stráncerestrictAccessToSectionpokud chcete ovlivnit přístup k dané stránce a všem podstránkám
Jako hodnoty metadata můžete použít:
authenticated(přihlášený nebo přihlášený pomocí trvalého přihlášení)authenticatedFully(ručně přihlášený pro tuto session)unauthenticated(anonymní uživatel)role:roleA,roleB(přihlášený uživatel má roli roleA nebo roleB)#{expression}(výraz ve smyslu https://docs.spring.io/spring-security/site/docs/current/reference/html/el-access.html, lze použít i výrazy popsané v kapitole ověřování oprávnění na aplikační vrstvě)ip: 192.22.168.1,192.23.**(uživatel přichází z konkrétního IP rozsahu)authenticatedToEdee(uživatel je přihlášen do Edee administrace)
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).
securitys podřízenými elementyresourcedefinuje 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žít 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)rightdefinuje 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žít i RjEL výraz, je možné použít jak zkratku práva (např. R) tak i plný název (např. read)instancevyž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:
<security> <resource>deliveryAddress</resource></security>A na podřízených odkazech nebo tlačítkách potom definovat už jen:
<security> <right>update</right></security>