Operator access w domenach wyszukiwania Odoo 19.3
Nowy operator domenowy access wyraża uprawnienia wprost w domenie. Sprawdziliśmy w kodzie ORM, jak działa i gdzie potrafi zaskoczyć.

W domenach Odoo pojawił się operator access. Dotyka miejsca, w którym w wielu modułach siedzi najwięcej kruchego kodu: domen liczonych w Pythonie po to, żeby użytkownik widział tylko rekordy swojej firmy albo te, do których ma prawa. Opisujemy, co operator robi, jak zapisać z nim domenę, gdzie może przyspieszyć zapytania i w którym miejscu potrafi po cichu przestać filtrować.
W której wersji to jest
Na operator natknęliśmy się we wpisie changeloga odoo-zed - to language server dla Odoo pisany w Rust, narzędzie deweloperskie, nie dokumentacja ORM. Changelog podaje dwie różne informacje: że obsługa operatora dotyczy Odoo >= 19.4 i że sam operator doszedł w Odoo 19.3. Pierwsza liczba opisuje zakres wersji wspieranych przez narzędzie, druga - wersję samego Odoo.
Sprawdziliśmy to w źródłach. Operator jest zaimplementowany w odoo/orm/domains.py jako funkcja _operator_access_rule_domain. Jest na gałęziach saas-19.3 i saas-19.4, nie ma go na saas-19.2. Czyli: operator wchodzi w 19.3.
Jest tu druga różnica, ważniejsza niż numer. W saas-19.4 znika plik ir_rule.py, a jego rolę przejmuje ir_access.py z modelem ir.access. Metoda _access_domain, z której operator korzysta, w 19.3 sprawdza uprawnienia przez ir.model.access, a w 19.4 pyta ir.access._get_domain_for. Sama funkcja operatora też się między tymi wersjami różni. Jeżeli piszesz moduł na obie, nie zakładaj, że mechanizm pod spodem jest ten sam.
Jak zapisać warunek
Operator przyjmuje trzy elementy: pole, 'access' i nazwę operacji.
# Odoo >= 19.3 - rekordy, których firmę użytkownik może czytać
domain = [('company_id', 'access', 'read')]
# rekordy tego modelu, do których użytkownik ma prawo zapisu
domain = [('id', 'access', 'write')]
Typowy kod sprzed zmiany wygląda tak:
def domain_for_user(self):
allowed = get_allowed_company_ids(self.env.uid)
return ['|', ('company_id', 'in', allowed), ('company_id', '=', False)]
Ograniczenia wyczytane z implementacji:
- pole musi być
idalbo relacjąmany2one; dla czegokolwiek innego operator rzuca wyjątkiem „The 'access' operator works only for many2one and 'id' fields"; - operacja to jedna z
read,write,create,unlink- inna wartość kończy się błędem „Invalid value for 'access' operator"; - na polach typu property operator nie działa.
Błędy są zgłaszane wprost, nie po cichu. To dobra wiadomość: literówka w nazwie operacji wywali się na teście, zamiast zwrócić zły zestaw rekordów.
Co robi serwer pod spodem
Operator nie jest tłumaczony na SQL jeden do jednego. ORM bierze model powiązany (dla id - model bieżący), odpina uprawnienia administratora przez sudo(False) i pyta go o domenę dostępu dla danej operacji. Dalej są trzy drogi:
- Domena dostępu jest fałszywa, czyli użytkownik nie ma dostępu do żadnego rekordu - całe wyszukiwanie zwraca pusty wynik.
- Domena dostępu jest prawdziwa albo sesja działa jako superużytkownik - warunek zamienia się w zwykłe
pole != False. - W pozostałych przypadkach warunek trafia do SQL jako podzapytanie nad domeną dostępu modelu powiązanego, a przy filtrowaniu w Pythonie korzysta z
has_access.
Punkt drugi to najważniejsza rzecz w tym tekście i wrócimy do niego przy bezpieczeństwie.
Wydajność: skąd zysk i jak go zmierzyć
Domena zwracana przez lambdę albo metodę wykonuje się przed zapytaniem. Python oblicza listę warunków, często z dużą listą identyfikatorów, i dopiero ją przekazuje do bazy. Planista PostgreSQL dostaje gotowy wynik obliczeń i nie wie, skąd się wziął. Operator zostawia tę decyzję ORM-owi, który składa podzapytanie zamiast przyjmować listę kilku tysięcy id.
Tyle mechanizm. Pomiarów nie podaje ani changelog, ani kod, my też ich nie mamy - przyspieszenie traktujemy jako hipotezę do sprawdzenia na własnych danych. Największej różnicy spodziewamy się tam, gdzie domena dziś pobiera listę id i filtruje przez in, w instalacjach multi-company z wieloma firmami oraz w domenach mieszających ograniczenia dostępu z filtrami biznesowymi.
Porównanie ma sens tylko na kopii danych produkcyjnych i na prawdziwych kontach użytkowników. Administrator z dostępem do wszystkiego niczego nie pokaże - teraz już wiadomo dlaczego, patrz punkt drugi wyżej. Sprawdzamy:
- plan zapytania z
EXPLAIN ANALYZEdla SQL generowanego przezsearch(), zwracając uwagę, czy Seq Scan zamienia się w Index Scan; - czas wykonania
search()i liczbę zwróconych rekordów, która musi być identyczna przed i po; - statystyki z
pg_stat_statementsdla najczęstszych zapytań danego modelu; - obciążenie CPU i I/O oraz liczbę odczytów sekwencyjnych po wdrożeniu.
Jeśli liczba rekordów się różni, mamy do czynienia z błędem w uprawnieniach, a nie z optymalizacją.
Migracja istniejących domen
Domena pola zapisana jako lambda
Najprostszy przypadek to pole formularza z domain=lambda self: [...], które liczy listę dozwolonych id. Część dotyczącą dostępu zastępuje się operatorem, a ewentualne filtry biznesowe zostają obok. Pamiętaj o ograniczeniu: po lewej stronie musi stać id albo many2one.
Domena składana w metodzie przed search()
Metoda buduje domenę z kilku kawałków i wywołuje search(domain). Z tej konstrukcji trzeba wyłuskać fragment odpowiedzialny za uprawnienia i zastąpić go operatorem. Przy refaktoryzacji łatwo zgubić warunek po drodze, dlatego sprawdza się, czy access rzeczywiście jest w finalnej domenie przekazanej do search(). Logowanie domeny w testach zajmuje minutę i oszczędza godziny szukania.
Multi-company z długą listą firm
Klasyczny wzorzec to company_id in [...] plus osobny OR dla rekordów bez firmy. Przy grupie kapitałowej z kilkudziesięcioma spółkami taki warunek puchnie. Uwaga na różnicę w zachowaniu: stary warunek z jawnym OR-em przepuszczał rekordy z pustym company_id, a ('company_id', 'access', 'read') wymaga, żeby pole było ustawione. Zanim zniknie stary kod, porównaj wyniki dla użytkownika przypisanego do jednej firmy, do kilku i do wszystkich.
Kod, który musi działać na starszych wersjach
Moduły utrzymywane dla kilku wersji potrzebują warstwy zgodności: dla 19.3 i nowszych domena z operatorem, dla starszych dotychczasowa domena dynamiczna. Do tego jeden zestaw testów uruchamiany na obu gałęziach, który pilnuje, żeby te same konta widziały te same rekordy.
Bezpieczeństwo: gdzie to przestaje filtrować
sudo() kasuje filtr
Gdy kod działa jako superużytkownik albo gdy model nie ma żadnych ograniczeń dostępu, warunek ('company_id', 'access', 'read') zamienia się w ('company_id', '!=', False). Nie w „brak wyniku" i nie w błąd - w filtr, który przepuszcza praktycznie wszystko. Zachowanie jest logiczne, bo superużytkownik ma widzieć całość, ale skutek jest taki, że:
- metoda wołana przez
sudo()i zwracająca rekordy użytkownikowi odda ich więcej, niż powinna, a nic w logu tego nie zasygnalizuje; - testy pisane na koncie administratora przejdą, mimo że w produkcji filtr zadziała inaczej.
To nie jest argument przeciw operatorowi, tylko konkretne miejsce, w którym trzeba uważać. Testy integracyjne piszemy na kontach o realnych uprawnieniach, w kilku kombinacjach grup i firm.
Reguły dostępu zależą od wersji
W 19.3 za domenę dostępu odpowiadają reguły rekordów i ir.model.access, w 19.4 - model ir.access. Po migracji warto sprawdzić, czy dany model nie jest filtrowany dwa razy: raz przez reguły dostępu, drugi raz przez warunek dopisany w widoku. Podwójne filtrowanie zwykle nie łamie bezpieczeństwa, ale potrafi ukryć rekordy, które użytkownik powinien widzieć.
Czego operator nie robi
Operator w domenie ogranicza wyszukiwanie. Nie zastępuje kontroli uprawnień przy create, write i unlink - te odbywają się osobno, niezależnie od tego, co stoi w domenie.
Kiedy zostać przy starym podejściu
Nie każda domena nadaje się do przepisania. Jeśli filtr opiera się na nietypowej logice w Pythonie, na przykład na danych z zewnętrznego systemu albo na regułach, których nie da się zapisać deklaratywnie, lepiej go nie ruszać. To samo dotyczy kodu, który zależy od kolejności działań: najpierw efekt uboczny, potem filtrowanie. Operator ma uprościć typowe przypadki. Przepychanie przez niego wyjątków skończy się kodem trudniejszym do zrozumienia niż ten, który zastąpił.
Źródła
Chcesz sprawdzić, jak Odoo sprawdziłoby się w Twojej firmie?
Skontaktuj się z nami - przeanalizujemy Twoje obecne procesy i pokażemy, od których 2–3 obszarów warto zacząć, żeby możliwie szybko zobaczyć efekt wdrożenia.