W modularnej aplikacji PHP granica to nie tylko podział na katalogi, klasy czy namespaces. Określa ona również, jakie dane moduł może przyjmować, jakie operacje może wykonywać i kto odpowiada za sprawdzenie poszczególnych warunków. Jeśli te decyzje pozostają niejawne, moduły mogą nadmiernie sobie ufać, dublować walidację lub ujawniać dane, nad którymi nie powinny mieć kontroli.
Projektowanie granic zaufania między modułami PHP polega na jawnym określeniu tych odpowiedzialności i zapewnieniu, że można je weryfikować w miarę rozwoju systemu. Nie oznacza traktowania każdego wewnętrznego wywołania jak wrogiego ruchu ani powtarzania wszystkich reguł na każdej warstwie. Chodzi o rozpoznanie miejsc, w których zmienia się zakres uprawnień, zdefiniowanie jasnych kontraktów i walidowanie każdej operacji w module, który ma kompetencje do jej wykonania.
Dlaczego granica między modułami jest również granicą zaufania

Moduł może korzystać z danych innego modułu za pośrednictwem wewnętrznego interfejsu, zdarzenia, API lub współdzielonego zapytania. W każdym przypadku warto ustalić, jakie gwarancje zapewnia nadawca i co musi sprawdzić odbiorca. Sama lokalizacja kodu nie wystarcza, by na to odpowiedzieć: bezpośrednio wywołana metoda PHP może otrzymać dane pochodzące z zewnętrznego żądania, zadania w tle albo innego modułu rozwijanego niezależnie.
Rozróżniaj walidację formatu, niezmienniki biznesowe i autoryzację. Walidacja formatu sprawdza, czy identyfikator lub data mają akceptowalną reprezentację. Niezmiennik zapewnia na przykład, że operacja nie obejmuje ujemnej kwoty. Autoryzacja określa, czy dany aktor może wykonać tę operację na danym zasobie. Są to powiązane, ale niezamienne kontrole.
Granica zaufania ma szczególne znaczenie, gdy zmienia się aktor, właściciel danych lub kanał wejściowy. Na przykład moduł rozliczeń może otrzymać zlecenie z innego modułu, ale nie powinien zakładać, że osoba, która zainicjowała żądanie, ma uprawnienia do modyfikowania dowolnej faktury. Tożsamość i kontekst muszą być przekazane jawnie i w sposób umożliwiający ich weryfikację.
Inwentaryzacja danych i operacji przekraczających poszczególne granice
Zacznij od zmapowania istotnych interakcji, zamiast przeprojektowywać całą aplikację. Dla każdej granicy udokumentuj, kto wywołuje operację, kto jest za nią odpowiedzialny, jakie dane są wymieniane oraz czy wywołanie jest synchroniczne, czy asynchroniczne. Uwzględnij mniej widoczne ścieżki: polecenia konsoli, zaplanowane procesy, konsumentów kolejek i zadania administracyjne.
- Dane: określ przekazywane pola, ich źródło oraz to, czy zawierają informacje osobowe, wrażliwe lub należące do innej domeny.
- Operacje: odnotuj, jakie zmiany są zlecane i który moduł powinien je zastosować.
- Kontekst: ustal, jaki aktor, organizacja, tenant lub cel powinien towarzyszyć żądaniu.
- Wyniki: określ, jakie informacje moduł może zwrócić i jakie błędy może zgłosić.
Taka inwentaryzacja pomaga wykryć sygnały ryzyka: moduł odczytuje cudze tabele, by ominąć interfejs; otrzymuje identyfikator zasobu bez sprawdzania jego przynależności; albo akceptuje mutowalny obiekt domenowy utworzony przez inny moduł. Ujawnia też duplikację: kilku konsumentów implementuje własną interpretację reguły, za którą odpowiada tylko jedna domena.
Definiowanie jawnych kontraktów: format, niezmienniki i zgodność
Kontrakt opisuje, czego odbiorca potrzebuje do wykonania operacji oraz jakie gwarancje zapewnia w odpowiedzi. W PHP można go wyrazić za pomocą niemutowalnych DTO, interfejsów, precyzyjnie określonych typów lub schematów wiadomości, zależnie od architektury. Konkretna forma ma mniejsze znaczenie niż przejrzystość: unikaj przekazywania całych encji, jeśli konsument potrzebuje tylko kilku wartości.
Kontrakt powinien określać wymagane pola, ograniczenia i znaczenie danych. Samo customerId nie mówi na przykład, czy identyfikuje klienta globalnego, klienta należącego do organizacji czy klienta widocznego dla konkretnego użytkownika. Jeśli kontekst warunkuje operację, uwzględnij go w kontrakcie albo określ, w jaki sposób uzyska go odpowiedzialny moduł.
Ustal też rozróżnialne błędy. Niepoprawnie sformatowane dane wejściowe to nie to samo co nieautoryzowana operacja, nieistniejący zasób czy konflikt z bieżącym stanem. Szczegóły przekazywane wywołującemu powinny wystarczyć do obsługi wyniku, ale nie mogą ujawniać danych ułatwiających wyliczanie zasobów ani poznanie informacji o osobach trzecich.
Kontrakty powinny ewoluować w sposób świadomie zachowujący zgodność. Dodanie opcjonalnego pola może być mniej inwazyjne niż zmiana znaczenia istniejącego pola, ale nadal wymaga przeglądu producentów i konsumentów. W przypadku wiadomości asynchronicznych uwzględnij, że starsza wersja może nadal krążyć po wdrożeniu nowego kodu. Nie utożsamiaj wdrożenia kodu z aktywacją nowego zachowania: gdy producenci i konsumenci nie zmieniają się jednocześnie, zastosuj kontrolowane przejście.
Weryfikowanie autoryzacji i własności danych w miejscu wykonywania operacji
Walidacja składniowa może odbywać się blisko punktu wejścia, ale autoryzację należy sprawdzać w kontekście, który zna zasób i kontroluje daną czynność. Na przykład moduł będący właścicielem faktury dysponuje informacjami potrzebnymi do określenia, czy można ją wystawić lub zmodyfikować. Nie powinien delegować tej decyzji do modułu wywołującego, który zna jedynie tożsamość użytkownika.
Przydatna kontrola wiąże aktora, czynność, zasób i kontekst: kto żąda jakiej operacji, na którym zasobie i w ramach jakiej organizacji lub tenanta. Nie zakładaj, że otrzymanie identyfikatora oznacza uzyskanie uprawnień. Sprawdź przynależność w miejscu, w którym wykonywana jest operacja, nawet jeśli żądanie pochodzi z wewnętrznego kodu uznawanego za zaufany. Dzięki temu alternatywna ścieżka lub późniejsza zmiana architektury nie ominie autoryzacji stosowanej wyłącznie w interfejsie webowym.
Unikaj jednak powielania całej polityki we wszystkich modułach. Scentralizuj decyzję w kompetentnym module i udostępnij operację lub usługę z jasnym kontraktem. Moduły konsumenckie mogą przeprowadzać pomocnicze kontrole poprawiające wygodę użytkowania, ale nie powinny stanowić jedynej bariery bezpieczeństwa.
Zapobieganie tworzeniu uprzywilejowanego dostępu przez współdzielone encje
Współdzielenie encji ORM między modułami wydaje się praktyczne, ale może tworzyć ukryte zależności. Jeśli kilka modułów wczytuje i modyfikuje tę samą encję, każdy z nich może ominąć operacje, które powinny chronić niezmienniki. Ponadto zmiany schematu lub zachowania wpływają na konsumentów, którzy nie kontrolują modelu.
Preferuj przekazywanie niezbędnych danych za pomocą poleceń, zapytań lub DTO. Moduł będący właścicielem zachowuje odpowiedzialność za zapis danych i walidację swoich niezmienników; konsument nie potrzebuje ogólnego dostępu do jego repozytoriów ani tabel. Jeśli odczyt jest konieczny, zaprojektuj zapytanie zwracające minimalny zestaw danych i respektujące zdefiniowane uprawnienia oraz zakres.
Współdzielona baza danych nie wymusza współdzielenia uprawnień. Jeszcze przed fizycznym rozdzieleniem danych można ustalić, że tylko jeden moduł zapisuje określone tabele, a pozostałe korzystają z udokumentowanych interfejsów. Taka reguła ogranicza koszt przyszłego rozdzielenia i ułatwia wykrywanie dostępu niezgodnego z kontraktem.
Testowanie, czy izolacja jest zachowana w miarę zmian systemu
Testy powinny obejmować zarówno poprawną ścieżkę, jak i próby przekroczenia granicy bez uprawnień. Testy jednostkowe sprawdzają reguły modułu będącego właścicielem; testy integracyjne weryfikują kontrakty, zapis danych i konfigurację autoryzacji między komponentami. Uwzględnij przypadki, w których prawidłowy aktor próbuje wykonać operację na zasobie innego tenanta, zasób nie istnieje albo kontekst jest niekompletny.
- Sprawdź, czy prawidłowy identyfikator sam w sobie nie przyznaje dostępu.
- Zweryfikuj, czy błędy autoryzacji nie zwracają danych chronionego zasobu.
- Przetestuj, czy konsumenci nie mogą modyfikować niezmienników za pomocą współdzielonych encji ani bezpośredniego dostępu do repozytoriów.
- Uruchamiaj testy kontraktowe, gdy producenci i konsumenci są wdrażani niezależnie.
Testy powinny odzwierciedlać rzeczywistą politykę, a nie tylko sprawdzać, czy rzucono wyjątek. Upewnij się, że zmiana nie została utrwalona, nie opublikowano niewłaściwego zdarzenia, a ponowienie operacji nie powoduje częściowych skutków. W przypadku przepływów obejmujących kilka kroków określ, co dzieje się w razie niepowodzenia jednego z etapów i jak uzgadniany jest stan.
Ponowny przegląd granic przy wydzielaniu usługi lub zmianie własności danych

Wydzielenie osobnej usługi ujawnia założenia, które wcześniej pozostawały ukryte w pamięci procesu: tożsamość, uprawnienia, opóźnienia, ponowienia, duplikaty i dostępność. Kontrakt musi przenosić wystarczający kontekst i uwzględniać awarie sieci. Nie przekształcaj zdalnego wywołania w kopię lokalnego bez ponownego przeanalizowania limitów czasu, idempotencji i zachowania w przypadku błędów.
Ponownie przeanalizuj granice również wtedy, gdy zmienia się odpowiedzialność za dane, pojawia się tenant, dochodzi nowy kanał lub przybywa konsumentów. Ustal, kto może zapisywać i odczytywać dane, który moduł decyduje o autoryzacji oraz jakie gwarancje są zachowane podczas migracji. Zapisz te ustalenia w dokumentacji kontraktu i powiązanych testach.
Przydatna granica zaufania nie zależy od tego, czy wszystkie moduły działają w tym samym procesie, ani od tego, czy zespół pamięta o nieformalnych konwencjach. Można ją rozpoznać po przypisanej odpowiedzialności, ograniczonym zakresie przekazywanych danych, weryfikacji uprawnień w miejscu wykonywania operacji i testach wykrywających nieautoryzowany dostęp. W ten sposób można chronić izolację, nie zamieniając każdego modułu w kopię reguł pozostałych.



