---
title: "Testa Supabase RLS: 30 av 87 behörigheter gjorde inget"
description: "Policytester för Supabase RLS missar det användarna ser. Ett bevis roll för roll mot en produktionskopia visade att 30 av 87 behörighetsrutor inte gjorde något."
url: "https://madsalt.tech/sv/blog/how-to-test-supabase-rls/"
author: "Denys Zaichenko"
language: "sv"
published: 2026-10-14
---

# Testa Supabase RLS: 30 av 87 behörigheter gjorde inget

Kort sagt

Testa row level security (radnivåsäkerhet) i Supabase så som en medlem möter den: logga in som varje roll, kör den verkliga åtgärden med en behörighet avstängd och sedan påslagen, och kontrollera att resultatet ändras. I en produktionsapp med fem redigerbara roller visade det att 30 av de 87 behörighetsrutor som en administratör kunde spara inte ändrade något i databasen. Ett test per policy ser inte det, eftersom det testar policyn du skrev, inte rutan på skärmen.

Jag heter Denys och är full-stack- och AI-ingenjör. Tidigare i år byggde jag ett projekt- och tidrapporteringssystem för en revisionsbyrå på Supabase, där varje åtkomstregel ligger i databasen. Nedan beskriver jag metoden jag använder för att bevisa de reglerna, vad den hittade och vad den kostar att köra.

## Varför räcker inte policytester för att testa Supabase RLS?

Ett policytest svarar på frågan "gör policyn det jag skrev?". Dina användare ställer en annan fråga: "om jag bockar ur den här rutan för den här rollen, kan de fortfarande göra det?". I en liten app har frågorna samma svar. I en app med en inställningssida där administratörer redigerar en behörighetsmatris glider de isär.

Appen hade 41 tabeller med RLS aktiverat och 402 `CREATE POLICY`-satser fördelade på 255 migreringar. Administratörsskärmen visade en matris med områden (projekt, uppgifter, tidrapporter, filer och så vidare) mot åtgärder (visa, skapa, redigera, arkivera), en kolumn per roll. Varje sparad ruta var ett löfte till administratören. Inget kontrollerade att databasen höll det.

Supabase dokumenterar standardverktygen väl: [pgTAP-tester](https://supabase.com/docs/guides/database/extensions/pgtap) och [testhjälparna](https://supabase.com/docs/guides/local-development/testing/pgtap-extended) låter dig skapa användare, byta till dem och kontrollera vad de kan läsa. Använd dem. Metoden nedan ligger ovanpå: den styrs av matrisen som administratören ser, inte av listan med policyer.

## Hur fungerar matrisbeviset?

Varje ruta i matrisen blir en sond. En sond loggar in som medlem i rollen, utför en verklig åtgärd och registrerar utfallet. Det görs två gånger: en gång med rutan avstängd och en gång med den påslagen, allt annat lika. Om utfallet ändras upprätthålls rutan. Om det inte ändras gör rutan ingenting, hur övertygande den än ser ut på skärmen.

1. **En ruta i matrisen**. Exempel: Standardmedlem · Uppgifter · Skapa
2. **Rutan av: agera som medlem i rollen**. Skapa en uppgift via samma väg som appen använder. Registrera utfallet.
3. **Rutan på: upprepa samma åtgärd**. Samma användare, samma data. Bara rutan har ändrats.
4. **Jämför de två utfallen**. Domen beror bara på vad databasen gjorde.
- **Utfallet ändrades**. Upprätthålls. Nekas när den är av, tillåts när den är på.
- **Samma båda gångerna**. Gör ingenting. Skärmen lovar en kontroll som inte finns.
- **Skärm och databas är oense**. En bugg. Sidan visar en knapp som databasen nekar, eller tvärtom.

*Figur 1. Varje dom kommer från ett par verkliga utfall, aldrig från att läsa koden.*

"Utfall" betyder här vad som helst som en konsument av behörigheten gör: en policy returnerar rader eller nekar en skrivning, en databasfunktion kastar ett fel, en trigger blockerar eller en edge function svarar 403. En ruta kan skydda flera av dessa, så de 87 rutorna krävde 279 kontroller per roll. Varje kontroll slutade med en av dessa domar:

| Dom                       | Kontroller per roll | Vad det betyder                                                |
| ------------------------- | ------------------- | -------------------------------------------------------------- |
| Effekt                    | 227                 | Utfallet vände med rutan                                       |
| Död                       | 30                  | Utfallet var detsamma med rutan av och på                      |
| Onåbar                    | 19                  | Åtgärden kan inte nås alls för den här rollen, bevisat separat |
| Edge function             | 1                   | Upprätthålls av en edge function, kontrollerat över HTTP       |
| Bara skärm                | 1                   | Ändrar bara vad sidan visar, täcks av ett frontend-test        |
| Skärm och databas överens | 1                   | Det sidan erbjuder stämmer med det databasen tillåter          |
| Fel                       | 0                   | En kontroll vars utfall inte motsvarade förväntningen          |

## Vad hittade beviset?

Körningen den 15 september 2026 täckte alla fem roller som en administratör kan redigera (Admin, Senior, Standard, Restricted och en klonad anpassad roll). Resultaten var identiska för alla roller.

![87 behörighetsrutor som en administratör kunde spara. Efter vad som faktiskt upprätthöll varje ruta. Lika för alla fem rollerna. Upprätthålls av databasen: 55; Någon annanstans: 1 edge function, 1 skärm: 2; Ändrade ingenting: 30.](https://madsalt.tech/charts/sv/how-to-test-supabase-rls.svg)

*Figur 2. De 87 behörighetsrutor som en administratör kunde spara, efter vad som faktiskt upprätthöll dem. Samma resultat för var och en av de fem rollerna.*

Ingen av de 30 släppte in någon. För 29 av dem läste ingen policy, funktion eller trigger någonsin rutan; den sista slutade spela roll efter att en policy skrivits om. Det är ändå ett verkligt problem: en administratör som bockar ur "Ta bort" för en roll tror att rollen inte kan ta bort. Vi dolde alla 30 i stället för att koppla in dem, eftersom var och en hade krävt ett produktbeslut, och en ruta som inte gör något ska inte synas på skärmen medan man fattar det.

Samma arbete avslöjade fyra defekter till:

| Vad som var fel                                                                                       | Hur det syntes                                                | Åtgärd                                                         |
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------- |
| 30 rutor gjorde ingenting                                                                             | Domen Död, identiskt utfall av och på                         | Dolda från matrisen                                            |
| Tre databasfunktioner släppte igenom en åtgärd när en uppgift saknade ansvarig                        | En sond som borde ha nekats tilläts                           | Funktionerna nekar nu när värdet saknas                        |
| Att återställa en tidrapport till utkast berodde på fel ruta                                          | Skärm och databas var oense för en roll                       | Styrs av behörigheten för återöppning som den hör till         |
| En medlem såg nya behörigheter först efter omladdning                                                 | Databasen hade ändrats, sidan inte                            | Behörigheter uppdateras var 60:e sekund och vid fönsterfokus   |
| Ett kolumnstandardvärde bröt mot sin egen check constraint, så inserts utan den kolumnen misslyckades | Hittades av säkerhetsgranskningen av åtgärden, i samma arbete | Standardvärdet ändrat till ett värde som constrainten tillåter |

Buggen med saknad ansvarig var den som spelade roll. Skydden var skrivna så här i PL/pgSQL:

```sql
if not (has_permission or assignee_id = auth.uid()) then
  raise exception 'forbidden';
end if;
```

När `assignee_id` är null blir `assignee_id = auth.uid()` null, `false or null` blir null, `not null` är fortfarande null, och `if` behandlar null som falskt. Undantaget kastas aldrig. I produktion hade ingen uppgift någon ansvarig satt, så det här var inget kantfall: det gällde varje uppgift som omfattades. Åtgärden lägger villkoret i `coalesce(…, false)` så att ett saknat värde nekas, och den levererades först, för sig själv, före resten av ändringarna.

## Hur kör du det mot en kopia av produktion?

Ett bevis mot ett schema som skiljer sig från produktion bevisar ingenting om produktion. Testmiljön körs på den lokala Supabase-stacken, i en separat databas som är en kopia av produktionsschemat plus migreringarna som granskas. Före varje körning jämförs ett fingeravtryck av båda lokala databaserna: 799 rader som beskriver tabeller, policyer, funktioner och rättigheter. Varje skillnad stoppar körningen.

1. **Produktionsschema**. Kopierat till en betrodd maskin. Det hamnar aldrig på en hostad CI-runner.
2. **Lokal stack: appdatabas**. Betjänas av lokalt API och auth, används av end-to-end-kontrollerna.
3. **Lokal stack: isolerad bevisdatabas**. Samma schema, används bara av matrisbeviset.
4. **Kontroll av fingeravtryck**. 799 rader per databas. Varje skillnad stoppar körningen före första sonden.
5. **Sonder körs som varje roll**. Som verkliga inloggade medlemmar, via samma vägar som appen använder.
6. **Inga spår kvar**. Antalet rader i slutet måste vara lika med antalet i början.

*Figur 3. Beviset räknas bara när databasen det kördes mot stämmer med produktion.*

Två detaljer gjorde testmiljön pålitlig:

- **Bryt den med flit först.** Innan jag litade på en grön körning ersatte jag en insert-policy med en som accepterar allt och körde beviset igen. Det måste bli rött på rutorna som den policyn skyddar. En testmiljö som förblir grön med en trasig policy är dekoration.
- **Kontrollera även det sparade läget.** Första omgången startar varje roll från ett rent utgångsläge. En andra omgång startar från varje rolls nuvarande sparade matris, eftersom en ruta kan bete sig annorlunda bredvid de rutor som en administratör faktiskt har bockat i.

## Hur håller du det grönt efter release?

Ett bevis som körs en gång blir inaktuellt vid nästa migrering. Beviset körs nu före varje push som rör migreringar, auth-koden eller själva matrisen.

1. **git push**. Ändringen rör migreringar, auth eller matrisen.
2. **Pre-push-hook: matrisbevis**. Körs lokalt mot bevisdatabasen. Rött blockerar pushen.
3. **Pushen når main**. Driftsättningen körs som vanligt.
4. **Körning på egen CI-maskin**. Samma bevis igen, på maskinen som har kopian av produktionsschemat.

*Figur 4. Den blockerande grinden är lokal, eftersom kopian av produktionsschemat stannar på en maskin.*

Beviset består av 135 tester i 11 filer, bredvid ungefär 3 100 vanliga enhetstester i samma projekt.

## Vad fungerade inte?

- **Hostad CI kunde inte köra det.** Beviset behöver kopian av produktionsschemat, och den ville jag inte ha på en hostad runner. Därför är den blockerande grinden en pre-push-hook, och CI på en egen maskin är en andra kontroll som driftsättningen inte väntar på.
- **Fasta radantal gick sönder.** Den första kontrollen av "inga spår" jämförde mot fasta siffror. Nästa legitima migrering lade till en behörighetsrad och gjorde den röd. Nu jämförs antalen i slutet med antalen i början.
- **Överhoppat är inte godkänt.** End-to-end-kontrollerna behöver den lokala edge-runtimen. När den ligger nere hoppas de över, och ett överhopp är lätt att läsa som grönt.
- **Det är långsamt på en belastad laptop.** Omgångarna per roll behövde längre timeouts på en hårt belastad maskin.
- **Det bevisar det som sonderna beskriver.** En ruta som ingen skrev en sond för rapporteras som en lucka i täckningen, inte som säker.

## Så upprepar du det här i din Supabase-app

1. Lista varje behörighet som skärmen låter någon spara: roll × område × åtgärd. Den listan är specifikationen, inte dina policyer.
2. Skriv för varje ruta ner den verkliga åtgärd den ska styra och hur ett nej syns (inga rader, ett fel, en 403).
3. Bygg en databas som stämmer med produktion och ta ett fingeravtryck av den, så att avvikelser i schemat stoppar körningen.
4. Kör varje åtgärd som en inloggad medlem, rutan av och sedan på, och jämför.
5. Bryt en policy med flit och bekräfta att beviset blir rött.
6. Dölj eller koppla in varje ruta som inte gör något, och gör sedan beviset till en grind före push.

Om din app redan har RLS men du inte är säker på att den gör det som inställningsskärmen säger, fungerar samma metod som en granskning av en befintlig kodbas, första steget i en [projekträddning](https://madsalt.tech/sv/services/project-rescue/). Jag har skrivit om projektet bakom siffrorna i [caset om tidrapportering för en revisionsbyrå](https://madsalt.tech/sv/work/audit-firm-time-tracking/).

Osäker på om din RLS stämmer med inställningsskärmen?

Jag jämför gärna erfarenheter om hur du testar den. Ingen säljpitch.

[Hör av dig](https://madsalt.tech/sv/#contact)
