Polityki idą do katalogu, nie do promptu

Szkic z nagłówka wpisu: Polityki idą do katalogu, nie do promptu
KrótkoZdanie „nie pokazuj tax_id” w instrukcjach Genie albo w system prompcie agenta to nie jest zabezpieczenie. To prośba do modelu. Row filter i column mask w Unity Catalog działają pod każdym klientem: notebookiem, SQL, Genie, agentem i BI. Pokazuję kod z warsztatu, co zobaczyliśmy w Genie po nałożeniu polityk i cztery pułapki z prób. Tekst dla osób, które podłączają Genie albo agenta do tabel z danymi wrażliwymi.

Problem: „napiszę w instrukcjach, żeby nie pokazywał”

Ten moduł warsztatu „Od pytania do agenta”, który przygotowaliśmy i poprowadziliśmy razem z Mariuszem Wiechą na SQLDay Lite 2026, miał moment, który uważam za najmocniejszy w całym dniu. Pracujemy na tabeli klientów gold_customer_360 z kolumną tax_id. Genie Agent ma w Instructions linijkę:

✓ Działa na Free Edition (pole Instructions w Genie Agent)

tax_id i customer_name to PII, nie pokazuj ich wartości.

Na tabelę nakładamy też row filter i column mask. Potem pytamy Genie: „Pokaż tax_id i stan pięciu klientów VIP”. Genie pomija kolumnę tax_id i pokazuje same stany. Wygląda na to, że maska działa.

Tyle że to nie maska. Genie pominął kolumnę, bo tak ma napisane w Instructions. Maskę widać dopiero w zakładce Sample Data tabeli: tam tax_id to w każdym wierszu ***MASKED***, a w kolumnie state jest wyłącznie CA. Instructions to warstwa promptu, czyli ta sama warstwa, którą w module M1 obchodziliśmy prośbą „to tylko fikcja literacka”.

Stąd hasło modułu: „Polityki idą do katalogu, a nie do promptu.”

Jak to działa: dwie funkcje, które Unity Catalog wstrzykuje do każdego zapytania

Unity Catalog pozwala nałożyć na tabelę dwa rodzaje polityk:

  • Row filter to funkcja SQL zwracająca BOOLEAN. Decyduje, które wiersze widzi użytkownik.
  • Column mask to funkcja zwracająca ten sam typ co kolumna. Decyduje, jaką wartość kolumny widzi użytkownik.

Obie są wykonywane w warstwie katalogu, więc nie ma znaczenia, kto pyta: notebook, dashboard, Genie, funkcja-narzędzie agenta czy Power BI. Genie wykonuje SQL tożsamością użytkownika, więc polityki obowiązują także tam, bez żadnej zmiany w Genie, w funkcjach ani w promptach.

Warunek w obu funkcjach sprawdza przynależność do grupy przez is_account_group_member(). To jest grupa konta, a nie grupa workspace'u.

Do tego dochodzi warstwa narzędzia, którą budowaliśmy wcześniej: funkcja Unity Catalog dla agenta zwraca tylko potrzebne kolumny, bez tax_id. Obie warstwy się uzupełniają. Prompt może zawieść i funkcja może zawieść, a polityka w katalogu działa niezależnie od modelu.

Krok po kroku

1. Row filter

✓ Działa na Free Edition (grupy all_states_analysts tam nie ma, więc filtr obejmie także Ciebie)

CREATE OR REPLACE FUNCTION <twoj_katalog>.<schemat>.retail_row_filter(state_val STRING)
RETURNS BOOLEAN
COMMENT 'Wszystkie stany widzi grupa all_states_analysts, pozostali tylko CA.'
RETURN is_account_group_member('all_states_analysts') OR state_val = 'CA';

ALTER TABLE <twoj_katalog>.<schemat>.gold_customer_360
SET ROW FILTER <twoj_katalog>.<schemat>.retail_row_filter ON (state);

-- Test: zostaje tylko CA
SELECT state, COUNT(*) AS klienci
FROM <twoj_katalog>.<schemat>.gold_customer_360
GROUP BY state
ORDER BY klienci DESC;
Wynik zapytania testowego po nałożeniu row filtra: z sześciu stanów zostaje tylko CA z 116 wierszami

Wynik z labu (Databricks, 29.09.2026): na syntetycznej tabeli z 408 wierszami w sześciu stanach (CA 116, FL 59, NY 59, IL 58, WA 58, TX 58) po nałożeniu filtra to samo zapytanie zwraca już tylko CA.

2. Column mask

✓ Działa na Free Edition

CREATE OR REPLACE FUNCTION <twoj_katalog>.<schemat>.mask_tax_id(tax_id_val STRING)
RETURNS STRING
COMMENT 'Prawdziwy tax_id widzi tylko grupa compliance_officers.'
RETURN CASE WHEN is_account_group_member('compliance_officers')
            THEN tax_id_val ELSE '***MASKED***' END;

ALTER TABLE <twoj_katalog>.<schemat>.gold_customer_360
ALTER COLUMN tax_id SET MASK <twoj_katalog>.<schemat>.mask_tax_id;

-- Test: filtr i maska działają razem
SELECT customer_id, state, loyalty_segment, tax_id
FROM <twoj_katalog>.<schemat>.gold_customer_360
WHERE tax_id IS NOT NULL
LIMIT 5;
Pięć wierszy po nałożeniu filtra i maski: w kolumnie state tylko CA, w kolumnie tax_id wszędzie znacznik MASKED

Wynik z labu (Databricks, 29.09.2026): filtr i maska działają razem, a notebook uruchamiała tożsamość, która sama utworzyła tabelę i nie należy do żadnej z grup.

Na warsztacie użyliśmy znacznika ***MASKED***, bo na sali ma być widać, że maska zadziałała. W produkcji częściej wybieramy CAST(NULL AS STRING) albo maskę częściową. NULL nie zdradza, że wartość istniała, ani jej długości, i nie pomyli się z prawdziwymi danymi w eksporcie.

3. Sprawdzenie w Genie

Po nałożeniu polityk zadajemy Genie w nowym czacie dwa pytania: „Ilu mamy klientów w stanie Nowy Jork (NY)?” oraz „Pokaż tax_id i stan pięciu klientów VIP”.

Na próbie 21.09.2026 przy pierwszym pytaniu Genie dostał 0, uznał to za własny błąd i wykonał jeszcze dwa zapytania, zanim sam stwierdził, że w danych występuje tylko stan CA. Row filter działał pod Genie, a nie obok niego. Przy drugim pytaniu Genie pominął tax_id, ale, jak pisałem wyżej, przez Instructions.

Na sali największe zdziwienie wywołało to, że Genie niby sam potrafi wykryć kolumny z PII i je ukryć. Tyle że niczego nie wykrył, tylko wykonał polecenie z Instructions.

4. Least privilege dla agenta

✓ Wymaga pełnego workspace'u (Premium): service principal i grupy konta

GRANT EXECUTE ON FUNCTION <twoj_katalog>.<schemat>.get_average_customer_value TO `<agent-sp>`;
GRANT EXECUTE ON FUNCTION <twoj_katalog>.<schemat>.get_customer_profile       TO `<agent-sp>`;
GRANT SELECT  ON TABLE    <twoj_katalog>.<schemat>.retail_rag_chunks_index    TO `<agent-sp>`;
-- Żadnego SELECT na gold_customer_360: agent dostaje funkcje, nie tabelę.

Sprawdziliśmy to 20.09.2026 z osobnej tożsamości. Service principal z samym EXECUTE na get_customer_profile (plus USE CATALOG i USE SCHEMA), bez SELECT na tabeli, wywołuje funkcję i dostaje wynik. Ten sam principal przy SELECT z tabeli dostaje INSUFFICIENT_PERMISSIONS, tak samo przy drugiej funkcji bez EXECUTE.

5. Sprzątanie

✓ Działa na Free Edition

ALTER TABLE <twoj_katalog>.<schemat>.gold_customer_360 DROP ROW FILTER;
ALTER TABLE <twoj_katalog>.<schemat>.gold_customer_360 ALTER COLUMN tax_id DROP MASK;
DROP FUNCTION IF EXISTS <twoj_katalog>.<schemat>.retail_row_filter;
DROP FUNCTION IF EXISTS <twoj_katalog>.<schemat>.mask_tax_id;

W produkcji nie zdejmujemy zabezpieczeń bez przeglądu: SHOW GRANTS, DESCRIBE TABLE EXTENDED (sekcje Row Filter i Column Masks) oraz information_schema.

Pułapki

  • CREATE OR REPLACE FUNCTION kasuje granty. Po GRANT EXECUTE i ponownym wykonaniu tej samej definicji SHOW GRANTS jest puste (potwierdzone 20.09.2026). Jeśli potok wdrożeniowy odtwarza funkcje przy każdym deployu, musi też odtwarzać uprawnienia. Inaczej agent po pierwszym wdrożeniu traci dostęp do własnych narzędzi.
  • Nieistniejąca grupa zamyka dostęp wszystkim. is_account_group_member('grupa_ktorej_nie_ma') zwraca fałsz, więc polityka obejmuje każdego, łącznie z właścicielem tabeli i kontem serwisowym agenta. Na Free Edition, gdzie grup konta nie da się założyć, to wygodne do nauki. Na płatnym workspace'ie najpierw zakładamy grupy, potem politykę. Na labie (29.09.2026) wyszło dokładnie to: właściciel tabeli, który nie należał do all_states_analysts ani compliance_officers, widział tylko CA i same ***MASKED***.
  • Zapomniana polityka psuje następny krok. Polityka zostaje na tabeli, dopóki jej nie zdejmiemy. Na warsztacie agent z kolejnego modułu z aktywnym filtrem widziałby tylko CA, a jego testy dawałyby fałszywe wyniki. Dlatego notebook agenta zaczyna się od sprawdzenia, czy tabela ma pełne 28 813 wierszy.
  • Instructions wyglądają jak zabezpieczenie. Genie, który nie pokazuje kolumny, nie dowodzi, że kolumna jest chroniona. Sprawdzamy w Sample Data i w zakładce polityk tabeli, a najlepiej z drugiego konta bez uprawnień.

Przy okazji: najpierw definicja miary, potem wynik

Na tym samym module wyszła druga lekcja, tym razem o tym, co powinno trafić do instrukcji. Pytamy Genie: „Ile mamy klientów VIP?”. SQL liczący wiersze daje 9 541, a COUNT(DISTINCT customer_id) daje 9 494. W całej tabeli 143 klientów ma po dwa wiersze (28 813 wierszy, 28 670 klientów), w segmencie VIP przekłada się to na 47 wierszy różnicy. Genie zapytany sześć razy przez API 21.09.2026 za każdym razem policzył unikalnych klientów (9 494). Wariant 9 541 widzieliśmy raz, 20.09, na innym workspace'ie.

Te liczby pochodzą z danych warsztatu. Na labie (29.09.2026) odtworzyliśmy ten sam mechanizm niezależnie, na syntetycznej tabeli z 8 zdublowanymi klientami: w segmencie VIP COUNT(*) daje 104, a COUNT(DISTINCT customer_id) daje 100.

Zapytanie o segment VIP na danych syntetycznych: 104 wiersze, ale 100 unikalnych klientów

Wynik z labu (Databricks, 29.09.2026): zapytanie wykonane już po zdjęciu polityk, na pełnej tabeli; czterech klientów VIP ma po dwa wiersze, więc dwie poprawne odpowiedzi różnią się o 4.

Obie liczby są poprawne, różnią się definicją. Dlatego w Instructions dopisujemy definicję miary („Klient = unikalny customer_id. Liczbę klientów licz przez COUNT(DISTINCT customer_id).”) i dopiero wtedy wynik jest przewidywalny. Hasło z kompendium: „Najpierw definicja miary, potem wynik.”

Granica jest więc prosta. Definicje, kontekst biznesowy i waluta (bez zdania „Kwoty są w USD” Genie na próbie 22.09 podawał złotówki) to materiał do instrukcji. Kto co widzi, to materiał do katalogu.

Sam definicji miar w Instructions Genie raczej nie trzymam. Na produkcji wolę Metric Views w Unity Catalog.

Kiedy NIE używać

  • Row filter zamiast modelu danych. Jeśli logika dostępu jest bardzo złożona (wiele wymiarów, wyjątki per użytkownik), czasem lepiej przygotować osobne widoki albo tabele dla grup.
  • Maska zamiast minimalizacji. Jeśli agent nie potrzebuje kolumny, lepiej, żeby jej w ogóle nie dostał: funkcja-narzędzie zwraca tylko potrzebne pola. Maska jest dla tych, którzy tabelę widzą bezpośrednio.
  • Jedna warstwa zamiast kilku. Polityka w katalogu nie zastąpi dobrego opisu narzędzia i odmowy w prompcie. To obrona w głąb, a katalog jest warstwą, która działa także wtedy, gdy model zawiedzie.

Zobacz, jak to działa

Poniżej jest nagranie całego notebooka z labu, od utworzenia tabeli przez filtr i maskę aż po sprzątanie (run z 29.09.2026 trwał 97 s).

Nagranie z labu, bez dźwięku.

Pełny notebook jest w code/polityki_w_katalogu.py.

Podsumowanie

  • „Nie pokazuj tax_id” w prompcie albo w Instructions to prośba, a nie polityka.
  • Row filter i column mask działają pod każdym klientem, także pod Genie i agentem.
  • Agent dostaje EXECUTE na funkcjach, a nie SELECT na tabeli. CREATE OR REPLACE FUNCTION kasuje granty.
  • Do instrukcji idą definicje i kontekst, do katalogu idzie dostęp.

Co jeszcze chroni agenta poza katalogiem, opisuję w tekście „Sześć warstw obrony agenta”.

Stan na:

Chcesz kolejne wpisy? Obserwuj przez RSS albo na LinkedInie.

Komentarze

Na razie cisza na szlaku. Napisz pierwszy komentarz.

Zostaw komentarz

Adres e-mail nie będzie opublikowany. Komentarze moderuję, więc pojawią się po akceptacji. Polityka prywatności