Izolacji danych wielu organizacji w PHP nie da się rozwiązać przez dodanie warunku WHERE organization_id = ? na głównym ekranie. Wyciek może mieć źródło w API, eksporcie, cache, załączniku, konsumencie kolejki lub zaplanowanym procesie. Może również wystąpić, gdy uprawniony administrator zmienia organizację, a system zachowuje wcześniejszy kontekst.
Cel architektoniczny powinien być jasny: żadna operacja, która odczytuje, modyfikuje, przetwarza lub dostarcza informacje klienta, nie może działać bez weryfikowalnego zakresu organizacji. Ten zakres musi być jawnie propagowany i walidowany na każdej istotnej granicy aplikacji.
Co powinna izolować aplikacja SaaS

Model transakcyjny to tylko część powierzchni ryzyka. Zinwentaryzuj zasoby mające właściciela organizacyjnego i dla każdego określ, jak identyfikowane, przechowywane, pobierane, usuwane i audytowane jest jego przypisanie.
- Dane transakcyjne: użytkownicy, projekty, zamówienia, faktury, konfiguracje i relacje między encjami.
- Pliki i załączniki: obiekty w zewnętrznym storage, miniatury, wygenerowane dokumenty i ich metadane.
- Cache: wyniki zapytań, wyliczone uprawnienia, sesje, odpowiedzi API i dane konfiguracji.
- Indeksy wyszukiwania: zindeksowane dokumenty, sugestie i uprzednio zagregowane filtry.
- Przetwarzanie asynchroniczne: zadania kolejki, ponowienia, partie importu i powiadomienia.
- Operacje i obserwowalność: logi, ślady, metryki, eksporty wsparcia i narzędzia wewnętrzne.
Nie wszystkie zasoby wymagają tej samej strategii. Publiczny katalog może być współdzielony, podczas gdy faktura, jej PDF oraz logi pobrań muszą zachować jednoznaczne powiązanie z organizacją. Decyzję należy udokumentować, aby nowa encja nie powstała bez reguł własności.
Wybór modelu izolacji danych
Istnieją trzy typowe modele. Żaden nie jest uniwersalnie lepszy: wybór zależy od wymagań regulacyjnych, wolumenu, operacji, modelu biznesowego i zdolności zespołu do utrzymania platformy.
Współdzielona baza danych z kluczem organizacji
Wszystkie organizacje współdzielą tabele, a każdy rekord podlegający izolacji zawiera klucz taki jak organization_id. Jest to najbardziej bezpośrednie podejście do rozwijania produktu i wykonywania globalnych zapytań agregujących. W zamian wymaga skrajnej dyscypliny: każde zapytanie, relacja, indeks, cache i zadanie muszą respektować zakres.
Co najmniej stosuj klucze obce, gdy jest to właściwe, indeksy złożone rozpoczynające się od organization_id oraz również złożone ograniczenia unikalności. Na przykład kod zamówienia unikalny w obrębie organizacji nie powinien być deklarowany jako globalnie unikalny, jeśli nie jest to reguła biznesowa.
Oddzielny schemat dla każdej organizacji
Każdy klient działa w odrębnym schemacie logicznym na tym samym serwerze bazy danych. Zmniejsza to ryzyko pominięcia filtra w rozdzielonych tabelach, lecz komplikuje migracje, połączenia, narzędzia analityczne i zapytania globalne. Jest właściwe tylko wtedy, gdy silnik, framework i codzienna eksploatacja konsekwentnie wspierają ten wzorzec.
Baza danych dla każdej organizacji
Rozdzielenie baz danych zapewnia silniejszą granicę i może ułatwić odtwarzanie lub przenoszenie poszczególnych klientów. Zwiększa także liczbę połączeń, migracji, kopii zapasowych, monitorowania i wdrożeń zmian struktury. Warto szczególnie ocenić, jak będą wykonywane raporty globalne, zmiany masowe i odzyskiwanie po błędach.
Separacja fizyczna ogranicza pewne klasy awarii, ale nie zastępuje autoryzacji, kontroli plików, zarządzania sekretami ani walidacji kontekstu w usługach współdzielonych.
Architektura referencyjna: jawny kontekst na granicach
Kontekst organizacji nie powinien być wyprowadzany z arbitralnych parametrów wysyłanych przez przeglądarkę. Musi być ustalany na podstawie uwierzytelnionego i autoryzowanego źródła: zwalidowanej subdomeny, tokenu z odpowiednią audience, przynależności użytkownika lub poświadczenia integracji powiązanego z jedną organizacją.
W aplikacji PHP warstwa wejściowa może utworzyć niemutowalny obiekt kontekstu z identyfikatorem organizacji, aktorem, jego uprawnieniami i identyfikatorem żądania. Kontrolery, komendy konsolowe i konsumenci kolejek otrzymują ten kontekst albo odtwarzają go na podstawie zweryfikowanych danych. Unikaj mutowalnych zmiennych globalnych, które mogą niewłaściwie utrzymywać się w procesach długo działających.
final class OrganizationContext {
public function __construct(
public readonly string $organizationId,
public readonly string $actorId
) {}
}Repozytoria powinny wymagać kontekstu do odpytywania lub modyfikowania izolowanych encji. Interfejs utrudniający jego pominięcie jest lepszy niż niejawna konwencja zależna od pamięci każdego programisty. Gdy to możliwe, stosuj dodatkowo polityki dostępu w warstwie domenowej: przynależność do organizacji nie upoważnia automatycznie do każdej czynności w jej obrębie.
Unikanie pominiętych filtrów w zapytaniach i relacjach
Izolowane zapytanie powinno filtrować według organizacji przed wyszukiwaniem po identyfikatorach biznesowych. Najpierw pobranie rekordu po id, a następnie sprawdzenie jego właściciela może prowadzić do ujawnienia danych, jeśli wynik zostanie zserializowany, zarejestrowany lub użyty przed jego odrzuceniem.
- Centralizuj zapytania w repozytoriach lub usługach odczytu z metodami przyjmującymi kontekst.
- Zabroń bezpośredniego dostępu do izolowanych modeli z kontrolerów, szablonów i konsumentów zdarzeń.
- Weryfikuj relacje: relacja ładowana leniwie może ominąć filtr zastosowany do encji głównej.
- Stosuj ograniczenia bazy danych, aby uniemożliwiać relacje między wierszami różnych organizacji, gdy model na to pozwala.
- Zdefiniuj konwencje dla migracji, testowych danych inicjalizujących i zapytań analitycznych.
W silnikach oferujących polityki bezpieczeństwa na poziomie wiersza mogą one zapewnić dodatkową ochronę. Ich wdrożenie musi jednak obejmować testy połączeń, zarządzanie rolami i przegląd procesów administracyjnych. Nie należy zakładać, że polityka bazy danych automatycznie chroni pliki, cache lub zewnętrzne indeksy.
Ryzyka poza głównym przepływem webowym
Nieprzezroczyste identyfikatory ograniczają enumerację, ale nie autoryzują dostępu. UUID lub losowy identyfikator nadal musi być rozwiązywany w obrębie aktywnej organizacji. Analogicznie podpisany URL pobierania wymaga obiektu należącego do właściwego zakresu, odpowiedniego wygaśnięcia i zasad unieważniania w razie zmiany uprawnień.
Klucze cache muszą zawierać identyfikator organizacji, a gdy treść zależy od uprawnień, dodatkowy wymiar roli lub wersji autoryzacji. Klucz taki jak dashboard:summary jest niebezpieczny w środowisku wielu organizacji; klucz z jawnym zakresem umożliwia również bardziej precyzyjne unieważnienia.
Eksporty są szczególnie wrażliwe, ponieważ zwykle wykonują się poza pierwotnym żądaniem. Zapisuj, kto je zlecił, dla której organizacji, jakie filtry zatwierdzono i gdzie zostanie dostarczony wynik. Nie wysyłaj załączników ani linków do odbiorców wyliczonych na podstawie niezwalidowanych danych.
Propagowanie kontekstu w API, webhookach i kolejkach
API musi wyprowadzać organizację z poświadczenia albo sprawdzać, czy żądany zasób należy do organizacji powiązanej z tym poświadczeniem. Zezwolenie na nagłówek X-Organization-Id może być prawidłowe dla operatorów z jawną delegacją, ale wymaga konkretnej autoryzacji, audytu i interfejsu, który uwidacznia zmianę zakresu.
Przychodzące webhooki nie powinny ufać identyfikatorowi organizacji zawartemu w body bez weryfikacji podpisu, nadawcy i wcześniejszego powiązania integracji. W przypadku wychodzących webhooków generuj zdarzenia z już ograniczonych danych i unikaj ponownego używania payloadów ze współdzielonej kolejki bez walidacji odbiorcy.
Każde zadanie asynchroniczne musi przenosić identyfikator organizacji wraz z identyfikatorem zasobu oraz odtwarzać kontekst przed wykonaniem zapytania. Konsument musi sprawdzić obie wartości, nawet jeśli zadanie zostało utworzone wewnętrznie. Ponowienia, zadania opóźnione i zadania zaplanowane wymagają tej samej reguły: nie istnieje niejawny kontekst żądania dostępny w bezpieczny sposób.
Testy i weryfikowalne sygnały diagnostyczne
Najważniejszy test nie polega na tym, że organizacja widzi własne dane, lecz na tym, że nie może odczytywać ani modyfikować danych innej organizacji. Utwórz dwie organizacje z celowo podobnymi danymi i wykonaj testy integracyjne dla każdego punktu wejścia: interfejsu webowego, API, komend, eksportów, pobrań i konsumentów kolejek.
- Zażądaj zasobu organizacji B przy użyciu sesji lub poświadczenia organizacji A i oczekuj odpowiedzi, która nie ujawnia informacji.
- Spróbuj aktualizować, usuwać, pobierać i eksportować zasoby między organizacjami, a nie tylko je odpytywać.
- Sprawdź, że klucze cache organizacji A i B generują niezależne wyniki.
- Uruchom zadanie kolejki z zasobem innej organizacji i zweryfikuj, że kończy się ono w kontrolowany sposób niepowodzeniem.
- Testuj odtwarzanie, importy i zadania nocne z danymi więcej niż jednej organizacji.
- Rejestruj wrażliwe działania wraz z aktorem, organizacją, zasobem i wynikiem, bez wprowadzania do logów niepotrzebnych danych osobowych.
Testy oparte na własnościach mogą uzupełniać przypadki ręczne: dla każdego zasobu utworzonego w ramach organizacji żaden aktor bez ważnej przynależności nie powinien móc go obserwować ani modyfikować poprzez udostępnioną ścieżkę. Tę właściwość należy stosować do przyszłych zmian endpointów i repozytoriów.
Plan wdrożenia dla istniejącej aplikacji
Jeśli dane są już wymieszane, nie zaczynaj od przepisania całej aplikacji. Najpierw zinwentaryzuj encje, przepływy, integracje i dostępy administracyjne. Następnie określ własność każdego rekordu i rozwiąż niejednoznaczne przypadki za pomocą możliwych do przeglądu reguł biznesowych.
- Dodaj encję organizacji i klucz przynależności do docelowych tabel.
- Wypełnij ten klucz za pomocą kontrolowanej migracji i zachowaj dowody przypadków bez wiarygodnego przypisania.
- Wprowadź repozytoria z ograniczonym zakresem i testy dostępu między organizacjami w najbardziej wrażliwych ścieżkach.
- Uwzględnij zakres w cache, plikach, wyszukiwaniach i nowych zadaniach.
- Migruj stopniowo stare przepływy i blokuj nowe zapytania bez kontekstu podczas przeglądu kodu.
- Włączaj bardziej rygorystyczne kontrole, gdy metryki i testy wykażą wystarczające pokrycie.
Decyzje, które warto udokumentować przed rozwojem

Przed dołączeniem kolejnej organizacji udokumentuj wybrany model, źródło prawdy dla kontekstu, wyjątki dotyczące dostępu administracyjnego, strategię identyfikatorów, granice cache, własność plików, odzyskiwanie danych, retencję logów oraz procedurę w przypadku podejrzenia dostępu między organizacjami.
Określ również, kto może działać w imieniu innej organizacji, jak taka delegacja jest zatwierdzana i jak jest odwoływana. Izolację danych wielu organizacji w PHP utrzymuje się dzięki jawnym decyzjom, powtarzalnym ograniczeniom technicznym i testom, które przekształcają obietnicę architektoniczną w sprawdzalne zachowanie.



