adt/exporter

Unified data export pipeline: audit log (export_log) + synchronous download or background processing with e-mail delivery.

Maintainers

Package info

github.com/AppsDevTeam/exporter

pkg:composer/adt/exporter

Transparency log

Statistics

Installs: 7

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.3 2026-09-01 20:12 UTC

This package is auto-updated.

Last update: 2026-09-01 20:13:00 UTC


README

Jednotne hrdlo vsech exportu dat: auditni zaznam (export_log) + synchronni download nebo background zpracovani s dorucenim e-mailem.

Proc

Auditni pozadavek "kdo, kdy a co presne exportoval" musi platit pro KAZDY export - grid, formular i konzolovy prikaz. Misto per-misto logovani vola vsechno jednu funkci.

Provozni a auditni data jsou ODDELENA (stejny vzor jako session vs. auth log):

  • export (provozni): ridi background regeneraci, soubor, doruceni, download; soubor spravuje ExportFileStorage (aplikace typicky vlastni File ekosystem); po doruceni a retenci souboru muze zaznam casem zaniknout
  • export_log (audit): append-only stream udalosti BEZ vazby na soubor - odvazi ho mover do dlouhodobeho auditniho uloziste. Sloupec action rozlisuje typ udalosti:
    • export - zadani exportu, "kdo/kdy/co presne"
    • download - kazdy VYDEJ souboru zvlast; data opousteji system az stazenim, opakovane a klidne nekym jinym nez zadavatelem

Oba zaznamy i pripadny background job vznikaji v JEDNE transakci (outbox garance adt/background-queue).

Pouziti

Format (CSV/Excel/...) urcuje predany generator; pocet sekci urcuje obsah (Excel: sheet per sekce, CSV: prave jedna sekce).

// jednoducha tabulka (grid):
$export = $this->exporter->export(new ExportRequest(
    identifier: 'smart-cards',
    sections: new ExportSection('items', $queryObject, ['number' => 'Cislo', ...]),
    generator: $excelGenerator,       // vs. $csvGenerator = volba formatu
    email: $user->getEmail(),
));

// vicesheetovy report (ruzne zdroje, vcetne agregatu bez entit):
$export = $this->exporter->export(new ExportRequest(
    identifier: 'order-payments',
    sections: [
        new ExportSection('Items', $orderItemsQb, $itemColumns),
        new ExportSection('Payments', $paymentsQb, $paymentColumns),
        new ExportSection('Summary', $summaryRows, $summaryColumns), // pole poli = snapshot primo v auditu
    ],
    generator: $excelGenerator,
    email: $user->getEmail(),
));

if (!$export->isInBackground()) {
    $this->sendResponse(new FileResponse($this->exporter->getFilePath($export), $export->getFileName()));
}
// jinak flash "export prijde e-mailem"

Instalace v aplikaci

  1. Entita Export (ExportTrait + vlastni id/createdBy atributy) v aplikaci; ExportLog je FINALNI entita knihovny - staci pridat mapping (vendor/adt/exporter/src/Model/Entities) a migraci. Volitelne vlastni ExportFileStorage (soubory pres aplikacni File ekosystem) a ExportActorProvider (akter auditu ze SecurityUser) - obe sluzby si extension najde podle typu.
  2. Neon:
    extensions:
        exporter: ADT\Exporter\DI\ExporterExtension
    exporter:
        syncRowLimit: 500
        fileDir: %appDir%/../data/exports
        downloadLink: ':Portal:Export:download'
    backgroundQueue:
        callbacks:
            processExport: [@exporter.exporter, processExport]
  3. Generatory souboru registrovat jako sluzby (implementuji ExportFileGenerator, extension je poskytne background handleru automaticky).

Akter auditu

Kazdy projekt identifikuje uzivatele jinak - nekde jmeno a e-mail, jinde jen prihlasovaci jmeno, servisni ucet nebo API klic. Knihovna proto nepredepisuje zadna konkretni pole a uklada:

sloupec k cemu
created_by_id klic aktera ve zdrojovem systemu - spojovaci klic auditu (podle nej se v auditnim ulozisti joinuje napric logy)
created_by_label jedno lidsky citelne oznaceni - aby sel log cist bez znalosti tvaru created_by
created_by JSON: cimkoliv dalsim projekt aktera identifikuje

Ploche jsou tedy jen ty dve veci, ktere maji smysl v kazdem systemu; promenna cast jde do JSONu. Aplikace dodava ExportActorProvider:

public function getActor(): ?ExportActor
{
    if (!$this->securityUser->isLoggedIn()) {
        return null;    // cron, konzument fronty, CLI
    }
    $identity = $this->securityUser->getIdentity();
    return new ExportActor(
        id: $identity->getId(),
        label: $identity->getName() ?: $identity->getEmail(),
        data: ['name' => $identity->getName(), 'email' => $identity->getEmail()],
    );
}

E-mail

Obsah e-mailu vlastni projekt: implementuj ExportMailFactory jako sluzbu (preklady, Latte sablona, branding) - extension ji pouzije automaticky misto vestaveneho defaultu. Background job ji dostane z DI, nic se nepredava.

Bezpecnost stahovani

Odkaz v e-mailu NEVEDE na soubor, ale na aplikacni routu (downloadLink). Soubor lezi ve fileDir MIMO docroot - jedina cesta k nemu je pres presenter, ktery MUSI overit prihlaseni a vlastnictvi:

public function actionExport(int $id): void
{
    $export = $this->exportQueryFactory->create()->byId($id)->fetchOneOrNull();
    if (!$export || !$export->getFile()) {
        $this->error();
    }
    // stahnout smi jen autor exportu (pripadne rozsirit o admin ACL)
    if ($export->getCreatedBy()?->getId() !== $this->securityUser->getId()) {
        $this->error('', \Nette\Http\IResponse::S403_Forbidden);
    }
    $this->exporter->logDownload($export);   // az po overeni, pred vydanim
    $this->sendResponse(new FileResponse($export->getFile()->getPath(), $export->getFileName()));
}

Overeni vlastnictvi je PROVOZNI vec, proto FK created_by na aplikacniho uzivatele zustava na Export - narozdil od auditu, ktery zadnou relaci nema.

Presenter MUSI pred vydanim souboru zavolat $this->exporter->logDownload($export) (viz priklad vyse). Zadani exportu a vydej souboru jsou dve ruzne udalosti: odkaz plati po celou retenci souboru, da se pouzit opakovane a klidne nekym jinym nez zadavatelem - bez toho vypada deset stazeni v auditu jako zadne. logDownload() zamerne neodchytava vyjimky: kdyz selze audit, soubor se nevyda.

Neprihlaseneho uzivatele posle bezny auth mechanismus aplikace na login a po prihlaseni zpet - odkaz z e-mailu tak funguje kdykoli behem retence souboru, ale vzdy jen pro opravneneho.

Retence souboru

Vygenerovane soubory nesmi na disku lezet dele, nez je nutne pro doruceni (obsahuji exportovana, casto osobni data). Denni cron:

0 3 * * * php bin/console exporter:purge-files

maze soubory starsi nez fileRetentionDays (default 7). Auditni zaznam zustava po celou svou retenci - jen prijde o soubor; download expirovaneho exportu vrati chybu.

Auditni vlastnosti zaznamu

  • vznika VZDY, i pro maly synchronni download
  • zaznam je NEMENNY (final entita bez setteru) a aktera nese DENORMALIZOVANE (snapshot v okamziku akce, zadna FK relace) - nezavisi na zbytku databaze a prezije odvoz do externiho auditniho uloziste
  • provozni beh na auditni zaznam NIKDY nesaha (po odvozu moverem tu neni); vse, co potrebuje background regenerace, je na Export
  • selekce KAZDE sekce se materializuje V OKAMZIKU volani: entity sekce nesou presny vycet ID + DQL s parametry, agregatove sekce primo snapshot radku (query nejde serializovat do jobu a pozdejsi prehrani by neodpovidalo dorucenemu souboru)
  • CIM se vybiralo, nese vyhradne sections (dql + parameters ze SKUTECNE spustene query). Knihovna zadny popis filtru od volajiciho neprijima: neoverila by ho a mohl by se s realnym dotazem rozejit - v auditu je pole, ktere muze lhat, horsi nez zadne
  • identifier + rowCount = rychly kontext, recipientEmail odpovida na "kam data odesla" (jina otazka nez kdo export spustil)
  • zaznam se po vytvoreni needituje; dlouhodobou retenci resi mover do auditniho uloziste (viz projektova infrastruktura)

Pripravenost pro SIEM

  • identita udalosti je dvojice (zdroj, id). Zdroj NENI sloupec: mover vi, ze ktere databaze cte, a stampuje ho pri odvozu; dedupikacni klic si odvodi jako zdroj:tabulka:id. Diky tomu je identita v kazde tabulce rodiny logu zdarma - id uz tam vsude je. Vlastni uuid by muselo pribyt do vsech (tedy i do doctrine-authenticator pro auth_log a doctrine-loggable pro change_log), a jednotne jednodussi schema je cennejsi nez robustnejsi schema nasazene jen v jednom logu. Zbytkove riziko: po obnove databaze ze zalohy se autoinkrement preposadi a id se muze vydat znovu - proti tomu je uuid imunni, dvojice ne
  • korelace mezi udalostmi jde pres export_id (id provozniho zaznamu). Tutez hodnotu nesou vsechny akce, takze WHERE export_id = ? ORDER BY id vrati zadani exportu i vsechna jeho stazeni. Zamerne ne id auditniho radku: ten uz mohl byt odvezen moverem
  • action mapuje na ECS event.action; dalsi typ udalosti (napr. smazani souboru retenci) pribude jako nova hodnota enumu, bez migrace
  • created_at je VZDY v UTC - jinak by cas sedel o offset vedle logu ostatnich systemu a pri prechodu na zimni cas by byla jedna hodina v roce nejednoznacna (2:30 nastane dvakrat). Sloupec je bezny DATETIME; UTC zajistuje zapis (format() bere zonu z objektu), zadny vlastni Doctrine typ neni potreba. ExportLog proto ZAMERNE nema getCreatedAt(): pri hydrataci by Doctrine dosadila lokalni zonu a vratila okamzik posunuty o offset. Zaznam je write-only, cte ho mover pres SQL - kdyby cteni z PHP nekdy bylo potreba, musi se soucasne zavest UTC Doctrine typ
  • source_ip a user_agent jsou ploche, protoze na nich stoji detekcni pravidla ("stejny ucet, jina zeme")
  • mapovani na ECS: created_at -> @timestamp, identifier -> event.action, created_by_id/_label -> user.id/user.name, recipient_email -> email.to.address, source_ip -> source.ip
  • POZOR na velikost: sections nese vycet ID, takze u velkeho exportu jde o stovky KB az jednotky MB. Do SIEM patri souhrn (identifier, akter, row_count, fields, prijemce) a odkaz pres id; enumerace ID zustava v archivnim ulozisti - jinak se udalost usekne na limitu ingesce (Splunk ma default TRUNCATE 10 000 B)