Recommended Free Tools
iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
A PostgreSQL row-level security setup check can print “nothing to report” for two reasons that say nothing about whether the data is protected. It can read only the tables where RLS is already switched on, so tables that never had RLS enabled drop out of the output. It can also treat an empty result from information_schema.table_privileges as proof that a role has no access. Catching a missing FORCE ROW LEVEL SECURITY means reading two catalog flags for every table, inspecting the policies and bypass attributes that decide who is affected, and confirming which PostgreSQL role the Symfony connection actually uses.
What the two RLS flags control
Row-level security has two separate switches on each table. ENABLE ROW LEVEL SECURITY turns policy enforcement on. FORCE ROW LEVEL SECURITY decides whether the table owner is also subject to those policies. By default the owner bypasses RLS, so a check that only looks at the owner’s view of a table can look correct when non-owner roles are not protected.
Two situations bypass RLS regardless of the table flags. The PostgreSQL documentation, section 5.9 “Row Security Policies,” states: “Superusers and roles with the BYPASSRLS attribute always bypass the row security system when accessing a table.” The same section also describes the closed default: “If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified.”
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →The catalog exposes the two switches in pg_class as relrowsecurity (enabled) and relforcerowsecurity (forced). The PostgreSQL 18 catalog reference documents both columns. The combinations behave as follows:
#1 Best Overall
| State | relrowsecurity | relforcerowsecurity | Who the table’s policies apply to |
|---|---|---|---|
| RLS disabled | false | Does not switch policies on | No policy applies; ordinary SQL privileges govern access |
| RLS enabled, not forced | true | false | Non-owner roles, except superusers and BYPASSRLS roles. The table owner bypasses policies. |
| RLS enabled and forced | true | true | Non-owner roles and the table owner, except superusers and BYPASSRLS roles |
Policies stored in pg_policy are evaluated only when relrowsecurity is true. A policy that exists on a disabled table protects nothing, which is why the disabled tables need their own line in any report.
The FORCE check, step by step
Run these queries on the database the application uses. Run them as the application’s role or a role with the same attributes. A superuser or BYPASSRLS session will see every row and can make a clean result look better than it is.
Inventory every ordinary and partitioned table
SELECT n.nspname AS schema_name,
c.relname AS table_name,
pg_catalog.pg_get_userbyid(c.relowner) AS owner,
c.relrowsecurity AS rls_enabled,
c.relforcerowsecurity AS force_rls
FROM pg_catalog.pg_class AS c
JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p')
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY n.nspname, c.relname;
rls_enabled tells you whether policies are enforced at all. force_rls tells you whether that enforcement reaches the table owner. Read both columns for every row.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #2
Find enabled tables that are missing FORCE
Add this condition to the inventory query:
AND c.relrowsecurity
AND NOT c.relforcerowsecurity
Any row returned means the owner of that table bypasses its policies. The owner usually also has the most privileges, so this is the case most worth fixing.
List tables where RLS is off
Replace app with your schema name and run:
SELECT n.nspname AS schema_name,
c.relname AS table_name
FROM pg_catalog.pg_class AS c
JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p')
AND NOT c.relrowsecurity
AND n.nspname = 'app'
ORDER BY c.relname;
Every table here should be one you have deliberately left to SQL privileges alone. Any table that holds tenant or user data and appears in this list is unprotected by row policies.
Blind spot 1: a check that only looks at tables with RLS enabled
The checker code for the scenario in the title was not available, so the two blind spots below describe failure patterns that these PostgreSQL behaviours make possible. They are not a reconstruction of any particular script.
Rank #3
Suppose a check filters on relrowsecurity and then looks for missing FORCE. In a schema with app.orders (enabled and forced) and app.invoices (never enabled), the filter sees only orders. The check finds nothing missing FORCE and reports clean. That output is accurate about enabled tables and silent about invoices, which is the one that matters.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteThe fix is structural rather than clever: the report needs the RLS-off list from the previous section as a separate section, compared against the tables the application is meant to protect.
Blind spot 2: treating an empty privilege view as no access
information_schema.table_privileges lists grants to or by roles that are currently enabled. An empty result therefore means no matching grants are visible for those roles. It does not establish that a login lacks effective privileges through role membership or other grant paths, and it says nothing about RLS filtering.
For a specific role and table, use the privilege inquiry functions. These answer SQL-privilege questions:
SELECT has_table_privilege('app_user', 'app.orders', 'SELECT') AS can_select,
has_table_privilege('app_user', 'app.orders', 'INSERT') AS can_insert,
has_column_privilege('app_user', 'app.orders', 'status', 'UPDATE') AS can_update_status;
Those functions do not tell you whether policies filter the rows that role can reach. For that, check the role’s attributes and whether it owns the table:
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsSELECT r.rolname AS role_name,
r.rolsuper,
r.rolbypassrls,
pg_catalog.pg_get_userbyid(c.relowner) = r.rolname AS owns_table
FROM pg_catalog.pg_roles AS r
CROSS JOIN pg_catalog.pg_class AS c
WHERE r.rolname = 'app_user'
AND c.oid = 'app.orders'::regclass;
If rolsuper or rolbypassrls is true, the role bypasses policies. If owns_table is true and force_rls is false, the role bypasses policies on that table.
Confirm the role the Symfony connection uses
In Symfony, the Doctrine integration provides a DoctrineDBALConnection service. The PostgreSQL role is whatever user is in the connection string, usually set through DATABASE_URL in your environment configuration. The Symfony user object, its roles, and firewall decisions do not change the PostgreSQL current_user, its ownership, or its BYPASSRLS attribute. Ask the database.
use DoctrineDBALConnection;
final class RlsSetupCheck
{
public function __construct(private Connection $connection) {}
public function connectedRole(): array|false
{
return $this->connection->fetchAssociative(
'SELECT current_user AS role_name, r.rolsuper, r.rolbypassrls
FROM pg_catalog.pg_roles AS r
WHERE r.rolname = current_user'
);
}
}
This sample is illustrative and has not been tested against a particular Symfony or DBAL release. Confirm the method name against your locked DBAL version. If the application sets a session value that policies read, such as a tenant identifier, the checker must set and query it on the same connection and inside the same transaction lifecycle the application uses. Otherwise the policy evaluates against a different context from the one you are inspecting.
Troubleshooting: when the check reports nothing
- The missing-FORCE query returns no rows, but the RLS-off list contains a protected table. The first query cannot see tables with RLS disabled. Treat the RLS-off list as a finding, then decide whether to enable RLS and write policies.
- Everything is clean, but the connected role has
rolsuperorrolbypassrlsset to true. The check ran as a bypassing role. Rerun it with the role the application uses. - The privilege view is empty, but
has_table_privilegereturns true. Trust the function for the SQL-privilege question. The view only covers grants to or by currently enabled roles. - Policies exist in
pg_policiesfor a table whoserls_enabledis false. Those policies are dormant. Before runningALTER TABLE app.invoices ENABLE ROW LEVEL SECURITY;, confirm that a policy applies to each command and role you need. A table with RLS enabled and no applicable policy is default-deny for non-owner roles. - The owner sees rows, but the application role does not. That is the owner bypass. Set
ALTER TABLE app.orders FORCE ROW LEVEL SECURITY;and retest the owner’s queries.
What a complete setup report should contain
- Schema, table, owner,
relrowsecurity, andrelforcerowsecurityfor every ordinary and partitioned table in the application’s schemas. - The connected PostgreSQL role, its
rolsuperandrolbypassrlsvalues, and whether it owns each table. - Applicable policies from the
pg_policiesview, including command type, roles, theUSINGexpression, and theWITH CHECKexpression. - Effective SQL privileges from
has_table_privilegeandhas_column_privilege, not inferred from an empty privilege view. - A note that referential-integrity checks bypass row security, and that row-security policies do not replace standard SQL privileges.
Trade-offs before setting FORCE
Forcing RLS on a table changes the owner’s view of it. Migrations, seed scripts, and maintenance jobs that run as the owner will now see filtered rows and can fail writes that policies block. Run them against a staging copy of the forced tables before changing production. If a maintenance role needs unrestricted access, grant it deliberately: BYPASSRLS bypasses policies on every table it can reach, so a shared role with that attribute quietly weakens every table, not only the one you intended.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Check the PostgreSQL major version in use before relying on catalog or view details, and check the locked Symfony and Doctrine DBAL versions before relying on the PHP sample.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

