Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Jádro / Atributy sloupců

Atributy pro zápis sloupců#

Kdo smí zvenčí přepsat konkrétní sloupec. Generické updateColumns je záměrně jen pro administrátora — front přes něj ale legitimně mění pár počítadel a příznaků. Výjimku proto nese sloupec sám, ve formě PHP atributu na property entity.

Atributy jsou v app/Core/Attributes/Hydration/, vyhodnocuje je app/Core/Traits/Api/Presenters/SaveTrait.php.

Dvě generace atributů vedle sebe#

Atribut Výskytů Role
#[ExternalReadonly] 474 sloupec nesmí přijít zvenčí (hodnotu určuje server)
#[ExternalAdminOnly] 16 smí ho zapsat jen privilegovaný volající
#[PubliclyUpdatable] 4 přes updateColumns ho smí zapsat kdokoli
#[ConditionallyUpdatable] 4 rozhodne pojmenovaná metoda Api presenteru
#[FieldAccess] 12 nová, sjednocující — tři osy read / insert / update

Stará čtveřice se neruší, jen se do ní nepřidává

#[FieldAccess] je cílový tvar a čte se oboje; plošná migrace 474 výskytů přijde až s testem parity. Do nového kódu piš FieldAccess, hotová místa nepřepisuj kvůli kosmetice. Podrobně o publiku polí v RBAC v API.

Stará čtveřice platí jen na EXTERNÍ cestě, FieldAccess všude

Staré stripy běží výhradně při $meta['__external'], což nastavuje jen actionSave(). FieldAccess je naopak podle role, takže platí i na interní cestě. Není to nedopatření, je to důvod, proč nový atribut vznikl.

Pořadí rozhodování u updateColumns#

  1. Oprávnění updateColumns — projde-li, atributy se vůbec nečtou.
  2. Teprve při zamítnutí se prochází sloupec po sloupci a hledá se atribut.
  3. Stačí jeden sloupec bez atributu a padá celé volání. Žádný částečný zápis.
Permission denied: column "banned" is not updatable without the updateColumns privilege

Povolení přes atribut se loguje

Do auth kanálu přibude řádek API_COLATTR. Bez něj by v logu bylo zamítnutí následované úspěšným zápisem — a log by lhal.

#[PubliclyUpdatable]#

„Tenhle sloupec smí zapsat kdokoli.“

#[ORM\Column(name: 'reported', type: 'boolean')]
#[ExternalReadonly]
#[PubliclyUpdatable]
protected bool $reported = false;

Typický případ je příznak „nahlášeno“ u inzerátu nebo příspěvku.

Povoluje LIBOVOLNOU hodnotu, ne jen rozumnou

Proto se nehodí na počítadla — votesUp by šlo nastavit na milion jedním požadavkem. Počítadla se zvyšují akcí registerEvent, která dělá serverový + 1.

Nekontroluje vlastnictví záznamu

Nezkoumá, komu záznam patří. Když na tom záleží, je to případ pro podmíněnou variantu.

#[ConditionallyUpdatable]#

„Pravidlo pro tenhle sloupec je tamhle.“

#[ConditionallyUpdatable(check: 'checkPermissionForRead')]
protected bool $read = false;

A na Api presenteru:

protected function checkPermissionForRead(int $id, mixed $value, ?int $userId): bool

Dnes to používá příznak „přečteno“ u odpovědi na inzerát a u odpovědi v katalogu firem — tedy případ, kdy vlastník není na tomhle záznamu, ale na jeho rodiči.

Rozhodujte proti databázi, ne proti požadavku

$id říká jen, o kterém záznamu se bavíme. Cokoli dalšího z požadavku je tvrzení klienta — kontrola nad hodnotou z téhož požadavku neověřuje nic.

Jméno metody se PÍŠE, neodvozuje

Konvence typu readcheckReadPermission() je past: překlep by se neprojevil chybou, ale tichým zamítnutím s hláškou „nemáš práva“. Existenci metod hlídá tests/Unit/Security/ConditionallyUpdatableContractTest.php.

Fail-closed: cokoli nejasného je zamítnutí

Jedno false u kteréhokoli id shodí celé volání. Chybějící záznam i výjimka v kontrole se počítají jako zamítnutí.

Postup: potřebuju pustit zápis jednoho sloupce zvenčí#

  1. Zeptej se nejdřív, jestli je to opravdu sloupec. Zvýšení počítadla je registerEvent; zápis s obchodní logikou je vlastní Api akce s vlastním klíčem oprávnění.
  2. Rozhodni podle tabulky níž, který atribut to je.
  3. Napiš ho na property ve sdílené base entitě — aby platil pro všechny sekce.
  4. U podmíněné varianty napiš metodu na Api presenteru a rozhodni v ní dotazem do databáze.
  5. Ověř obojí — že povolený zápis projde a že zamítnutý skutečně padá.
  6. Zkontroluj log: povolení přes atribut musí být vidět jako API_COLATTR.
Situace Řešení
zápis smí kdokoli a hodnota nemá stupně #[PubliclyUpdatable]
zápis smí jen někdo konkrétní #[ConditionallyUpdatable]
počítadlo, které se má zvýšit o 1 ani jeden — akce registerEvent
zápis s vlastní obchodní logikou ani jeden — vlastní Api akce
hodnotu určuje výhradně server #[ExternalReadonly]

Atribut na property znamená: kdokoli s přihlášením, kolikrát chce

Sedí na entitě a čte ho hranice, kterou procházejí obě cesty. Označit sloupec není „povolit jednomu formuláři“ — je to plošné povolení.

Property nesmí nést oba atributy naráz

Nebylo by jasné, které pravidlo platí. Hlídá to týž test.

Pokrytí hlídají strukturální testy#

Test Co kontroluje
ExternalReadonlyCoverageTest že server-počítaná pole mají #[ExternalReadonly]
ExternalAdminOnlyCoverageTest totéž pro admin-only sloupce
ConditionallyUpdatableContractTest že pojmenovaná kontrolní metoda existuje
FieldAccessCoverageTest že pole, jehož jméno vypadá jako tajemství, je anotované nebo má výjimku s důvodem

Pokrytí podle jména pole je síto, ne ochrana

Test pozná password nebo secret. Pole s nevinným jménem, které přesto nese tajemství (meta, config, note), musí anotovat člověk.

Kam sáhnout#

Chci Kde
definice atributů app/Core/Attributes/Hydration/
vyhodnocení při zápisu app/Core/Traits/Api/Presenters/SaveTrait.php
ořez polí podle oprávnění tamtéž → stripExternalReadonlyFields(), stripExternalAdminOnlyFields()
testy pokrytí tests/Unit/Security/

Navazující kapitoly: RBAC v API · Hydrátory · Životní cyklus manageru