KDB knowledge

Table rights - GRANT ON TABLE

Give a user rights on one table (GRANT SELECT ON TABLE prices), on some of its columns (GRANT SELECT (id, name) ON staff) or on some of its rows (CREATE POLICY), or take some rights of the role away (REVOKE UPDATE, DELETE ON TABLE ledger).

A user's role gives rights on a whole database. Table rights change that for one table of the current database:

allowed = (the role allows it AND it was not revoked on the table) OR it was granted on the table
RightNeeded for
SELECTreading the table in SELECT, subqueries, JOIN, EXPLAIN, cursors, and RETURNING
INSERTINSERT, and the line protocol (POST /write)
UPDATEUPDATE
DELETEDELETE

They are checked once per statement, before it runs (and before the result cache), never per row.

GRANT ... ON TABLE

Syntax:

GRANT SELECT | INSERT | UPDATE | DELETE | ALL [PRIVILEGES] [, ...] ON TABLE <table> TO <user>
GRANT DEFAULT ON TABLE <table> TO <user>

Adds rights on one table of the current database (USE). DEFAULT removes every table right of the user on that table: the role decides again.

Example: A user who may only read one table (sample data: sample)

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER viewer WITH PASSWORD 'Viewer-password-42';
GRANT SELECT ON TABLE securities TO viewer;
SHOW USERS;

Result:

CREATE USER admin WITH PASSWORD '***' ROLE admin

user 'admin' created with role admin

CREATE USER viewer WITH PASSWORD '***'

user 'viewer' created with role none

GRANT SELECT ON TABLE securities TO viewer

granted SELECT on table 'securities' of database 'demo' to user 'viewer'

SHOW USERS

name (STRING)role (STRING)grants (STRING)created (TIMESTAMP)changed (TIMESTAMP)table_grants (STRING)two_factor (BOOL)statement_timeout (STRING)priority (STRING)
adminadminNULL2026-10-22T05:44:29.129Z2026-10-22T05:44:29.129ZNULLfalseNULLNULL
viewernoneNULL2026-10-22T05:44:29.198Z2026-10-22T05:44:29.241Zdemo.securities: +SELECTfalseNULLNULL

(2 rows)

REVOKE ... ON TABLE

Syntax:

REVOKE SELECT | INSERT | UPDATE | DELETE | ALL [, ...] ON TABLE <table> FROM <user>

Takes rights away on one table, even when the role allows them. For an accounting ledger that must only grow: the app's user has role write, and REVOKE UPDATE, DELETE ON TABLE ledger FROM app.

Example: The ledger only grows (sample data: query-app)

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER app WITH PASSWORD 'App-password-12345';
GRANT write ON DATABASE demo TO app;
REVOKE UPDATE, DELETE ON TABLE ledger FROM app;
SHOW USERS;

Result:

CREATE USER admin WITH PASSWORD '***' ROLE admin

user 'admin' created with role admin

CREATE USER app WITH PASSWORD '***'

user 'app' created with role none

GRANT write ON DATABASE demo TO app

user 'app' has role write on database 'demo'

REVOKE UPDATE, DELETE ON TABLE ledger FROM app

revoked UPDATE, DELETE on table 'ledger' of database 'demo' from user 'app'

SHOW USERS

name (STRING)role (STRING)grants (STRING)created (TIMESTAMP)changed (TIMESTAMP)table_grants (STRING)two_factor (BOOL)statement_timeout (STRING)priority (STRING)
adminadminNULL2026-10-22T05:44:29.156Z2026-10-22T05:44:29.156ZNULLfalseNULLNULL
appnonedemo=write2026-10-22T05:44:29.225Z2026-10-22T05:44:29.273Zdemo.ledger: -UPDATE -DELETEfalseNULLNULL

(2 rows)

Example: A revoked table can not be wrapped in a view (sample data: query-app; shows an error)

Setup:

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER migrate WITH PASSWORD 'Migrations-only-secret-9' ROLE ddl;
REVOKE SELECT ON TABLE ledger FROM migrate;
SET ROLE migrate;
CREATE VIEW all_ledger AS SELECT * FROM ledger;

Result:

statements now use the rights of user 'migrate' (RESET ROLE goes back)

Error: SELECT on table 'ledger' was revoked for user 'migrate' (REVOKE SELECT ON TABLE ledger)

Column rights

Syntax:

GRANT SELECT | INSERT | UPDATE | ALL (<column> [, ...]) [, ...] ON [TABLE] <table> TO <user>
REVOKE SELECT | INSERT | UPDATE | ALL (<column> [, ...]) [, ...] ON [TABLE] <table> FROM <user>

A right can name columns. Then it is about those columns only. Each right has its own list: GRANT SELECT (id, name), UPDATE (name) ON staff TO clerk.

column allowed = not revoked on the column AND (the table allows it OR it was granted on the column)
You wantWrite
A user who reads only some columnsGRANT SELECT (id, name) ON staff TO temp (no role on the database)
Hide one column from a roleREVOKE SELECT (salary) ON staff FROM clerk
Let a user change only one columnREVOKE UPDATE ON staff FROM clerk; GRANT UPDATE (name) ON staff TO clerk
Back to the roleGRANT DEFAULT ON TABLE staff TO clerk (removes the table's column rights too)

Every column a statement names is checked: the SELECT list, WHERE, JOIN ... ON, GROUP BY, HAVING, ORDER BY, subqueries, WITH, RETURNING, the columns an INSERT fills and the columns an UPDATE sets. A refused statement gets permission denied for column "salary" of table "staff" (HTTP 403, SQLSTATE 42501).

Example: An auditor who reads some columns of the ledger (sample data: query-app)

Setup:

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER auditor WITH PASSWORD 'Auditor-password-77';
GRANT SELECT (line_id, entry_date, account) ON ledger TO auditor;
SET ROLE auditor;
SELECT line_id, account FROM ledger ORDER BY line_id LIMIT 3;

Result:

SET role = 'auditor'

statements now use the rights of user 'auditor' (RESET ROLE goes back)

SELECT line_id, account FROM ledger ORDER BY line_id LIMIT 3

line_id (INT)account (INT)
11100
23000
31200

(3 rows)

Example: SELECT * needs every column (sample data: query-app; shows an error)

Setup:

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER auditor WITH PASSWORD 'Auditor-password-77';
GRANT SELECT (line_id, entry_date, account) ON ledger TO auditor;
SET ROLE auditor;
SELECT * FROM ledger;

Result:

statements now use the rights of user 'auditor' (RESET ROLE goes back)

Error: permission denied for column "entry_id" of table "ledger": SELECT * (or RETURNING *) reads every column, and user 'auditor' may not SELECT this one; name the columns instead

Example: What the current user may do with each column (sample data: query-app)

Setup:

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER app WITH PASSWORD 'App-password-12345';
GRANT write ON DATABASE demo TO app;
REVOKE SELECT (credit) ON ledger FROM app;
SET ROLE app;
SELECT column_name, privilege_type FROM information_schema.column_privileges
WHERE table_name = 'ledger' AND column_name IN ('debit', 'credit') ORDER BY 1, 2;

Result:

SET role = 'app'

statements now use the rights of user 'app' (RESET ROLE goes back)

SELECT column_name, privilege_type FROM "information_schema.column_privileges" WHERE table_name = 'l

column_name (STRING)privilege_type (STRING)
creditINSERT
creditUPDATE
debitINSERT
debitSELECT
debitUPDATE

(5 rows)

Row rights (row-level security)

Syntax:

CREATE POLICY <name> ON <table> [AS PERMISSIVE] [FOR ALL | SELECT | INSERT | UPDATE | DELETE]
    [TO <user> | PUBLIC [, ...]] [USING (<condition>)] [WITH CHECK (<condition>)]
DROP POLICY [IF EXISTS] <name> ON <table>
ALTER TABLE <table> ENABLE | DISABLE ROW LEVEL SECURITY

A policy decides which rows of a table a user sees and may change. It acts once the table's row-level security is on (ENABLE ROW LEVEL SECURITY). Then, for every user who is not an admin of the database:

StatementWhat the policies do
SELECT (also joins, subqueries, WITH, views, EXPLAIN, CREATE TABLE ... AS)only rows where a USING condition is true are read
UPDATE, DELETEonly those rows are changed; others are left alone, without an error
INSERT, and the new version of an updated rowmust pass a WITH CHECK condition (or the USING of a policy without one), else new row violates row-level security policy for table "t" (HTTP 403, SQLSTATE 42501)

The policies of a table are joined with OR: one that lets a row in is enough. A table with row-level security on and no policy for the user shows them no rows at all.

Example: Each user sees only their own rows (sample data: query-app)

Setup:

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER ann WITH PASSWORD 'Ann-password-12345';
GRANT write ON DATABASE demo TO ann;
CREATE TABLE orders (id INT PRIMARY KEY, owner STRING, amount DOUBLE);
INSERT INTO orders VALUES (1, 'ann', 10), (2, 'bo', 20), (3, 'ann', 30);
CREATE POLICY own ON orders TO ann USING (owner = current_user);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
SET ROLE ann;
SELECT * FROM orders ORDER BY id;

Result:

SET role = 'ann'

statements now use the rights of user 'ann' (RESET ROLE goes back)

SELECT * FROM (SELECT * FROM orders WHERE owner = current_user) AS orders ORDER BY id

id (INT)owner (STRING)amount (DOUBLE)
1ann10
3ann30

(2 rows)

Example: A row for someone else is refused (sample data: query-app; shows an error)

Setup:

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER ann WITH PASSWORD 'Ann-password-12345';
GRANT write ON DATABASE demo TO ann;
CREATE TABLE orders (id INT PRIMARY KEY, owner STRING, amount DOUBLE);
CREATE POLICY own ON orders TO ann USING (owner = current_user);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
SET ROLE ann;
INSERT INTO orders VALUES (4, 'bo', 5);

Result:

statements now use the rights of user 'ann' (RESET ROLE goes back)

Error: new row violates row-level security policy for table "orders"

Example: The policies of a database (sample data: query-app)

Setup:

CREATE USER admin WITH PASSWORD 'Long-Admin-Passw0rd' ROLE admin;
CREATE USER ann WITH PASSWORD 'Ann-password-12345';
CREATE TABLE orders (id INT PRIMARY KEY, owner STRING, amount DOUBLE);
CREATE POLICY own ON orders TO ann USING (owner = current_user);
CREATE POLICY big ON orders FOR SELECT TO PUBLIC USING (amount > 100);
SELECT policyname, cmd, roles, qual FROM pg_policies ORDER BY policyname;

Result:

policyname (STRING)cmd (STRING)roles (STRING)qual (STRING)
bigSELECT{public}amount > 100
ownALL{ann}owner = current_user

(2 rows)

REVOKE role ON DATABASE

Syntax:

REVOKE read | write | admin ON DATABASE <db> FROM <user>
REVOKE ALL ON DATABASE <db> FROM <user>

REVOKE <role> takes away exactly a grant of that role. A stronger grant stays: REVOKE read does not remove a write grant (the message says so). REVOKE ALL removes any grant on the database.