Policy Reference
Write row-level and column-level security policies — how rules select readers, the condition language, how policies combine, examples, and the Keboola CLI and API calls that create them.
This page describes the policies behind row-level and column-level security: what a policy contains, how a rule selects its readers, and how to create one.
A policy
Section titled “A policy”A policy protects one table, identified by its Storage table ID (for example in.c-crm.orders). There are two kinds, and a table can have both:
| Kind | Object type | Contains | Controls |
|---|---|---|---|
| Row-level | rls-policy | rules, optional default | Which rows a reader sees |
| Column-level | cls-policy | rules | Which columns a reader sees |
Each rule has a selector, which says whom the rule is for, and a payload:
- In an
rls-policythe payload is acondition, a filter on the table’s rows. - In a
cls-policythe payload isvisible_columns, the list of columns the selected readers can read. Columns that are not listed are left out of the result.
A table has one policy of each kind per owning project. A policy applies to the project that owns it and to the projects it is shared with (see Scope).
Who a rule selects
Section titled “Who a rule selects”Each rule has exactly one selector: subjects or groups.
| Selector | Value | Matches |
|---|---|---|
subjects | A list of ` | ` strings |
groups | A list of group names | A reader whose identity provider delivered one of these groups |
["*"] in either list | The only entry of its list | Every identified reader |
- Administrators. An administrator who queries with a Storage token or an MCP Server session has the subject
keboola|<administrator ID>, for examplekeboola|1234. Administrators get no exemption, so they need a rule like any other reader. - Data App users. A user who signs in to a Data App has the subject
<idp>|<sub>, whereidpis the issuer URL of the identity provider andsubis the user’ssubclaim, for examplehttps://login.example.com|user-1. For GitHub sign-in thesubis the account login. An author cannot read asuboff an identity provider, so in practice group rules and["*"]are the usual selectors for app users. - Groups are the strings the identity provider sends in its groups claim, exactly as delivered. Keboola does not manage group membership. A reader whose groups are unknown never matches a group rule.
- Email is not a selector. New policies cannot select a reader by email, because an identity provider can assert any email. Policies written earlier with
principalorprincipals(schema 1.0.0 and 1.1.0) keep working and are read by their own rules. To change who they select, create a new policy withsubjectsorgroups. "*"must be the only entry of its list. It never matches a reader with no identity, and it cannot widen a stricter policy on the same table.
To find the administrator ID that goes after keboola|, list the tokens of the project and read the admin.id of your own master token:
kbagent --json token list --project my-projectConditions
Section titled “Conditions”A condition is a filter on the rows of the table. It is built from the following shapes. There is no free-text SQL, which keeps the policy safe to author.
{"column": "status", "op": "eq", "value": "active"}Comparison operators are eq, ne, gt, gte, lt and lte, with a string, number or boolean value.
{"column": "region", "op": "in", "values": ["EU", "US"]}Membership operators are in and not_in.
{"column": "deleted_at", "op": "is_null"}is_null and is_not_null are the only way to match an empty value. A null value in a comparison is rejected.
{"and": [ {"column": "region", "op": "eq", "value": "EU"}, {"column": "status", "op": "ne", "value": "draft"}]}and and or combine two or more conditions. There is no not.
{"true": true}{"true": true} applies no filter and {"false": true} matches no row. Use {"true": true} sparingly: it is how you give a reader every row.
Column names are matched exactly, including case.
Values that come from the reader
Section titled “Values that come from the reader”Instead of a fixed value, a condition can use the reader’s own identity:
{"$identity": "email"}as avalueis the reader’s email, lowercased. It exists only when the identity provider verified the email. The column value is compared as stored: store the emails in the column in lowercase and without spaces, or the rows do not match. A rule that uses it cannot be applied to a reader without one: an end user of a Data App gets an empty result for the table, and an administrator is refused (HTTP 403).{"$identity": "groups"}asvaluesis the reader’s list of groups.
{"column": "owner_email", "op": "eq", "value": {"$identity": "email"}}How rules and policies combine
Section titled “How rules and policies combine”- Within one
rls-policy, every rule that selects the reader applies, and their conditions are combined with OR. - If no rule selects the reader, the reader gets the policy’s
defaultcondition. Without adefaultan administrator is refused and an end user of a Data App gets an empty result. Use{"false": true}as thedefaultto return no rows for every reader instead of a refusal. - Within one
cls-policy, the reader sees the union of thevisible_columnsof the rules that select them. A column policy has nodefault: a reader that no rule selects is refused. - Several policies on one table combine with AND (for columns, the intersection). A policy added later can only narrow access for a workspace that has no policy binding (a list of the policies that apply to one workspace or app). A Data App applies only the policies listed in its configuration, and a workspace that is bound to policies only those. A binding can leave a policy out, an organization-scoped one too. The table is then read under fewer policies, so access can be wider than the full set of policies of the project allows. Whoever edits the list of an app or the binding of a workspace decides which policies apply.
- A table needs a policy. A table with no policy is refused for an administrator and empty for a Data App user. A column policy alone opens the table for the readers it names, with all rows and only the columns it allows, and refuses everyone else (see deny by default). When the table has a row policy as well, the rows come from the row policy and the columns from the column policy.
Scope and permissions
Section titled “Scope and permissions”scope | Applies to |
|---|---|
targeted (default) | The project that owns the policy, plus the projects listed in targetProjectIds |
organization | Every project of the organization |
There is no project-only scope; a targeted policy with no targets covers its own project. Prefer targeted: an organization policy is loaded by every project of the organization, including projects on a different backend.
A project administrator (master Storage token) can create, change and delete a targeted policy of their own project. Sharing it with other projects (targetProjectIds) or creating an organization policy needs the organization administrator role.
The dialect field of a policy (snowflake or bigquery) is optional in the API, and Query Service does not read it: it compiles every condition for the backend of the workspace, so one policy works on both backends. The CLI leaves dialect out of a new policy unless you pass --dialect, and it refuses a --dialect that differs from the backend of the project.
Examples
Section titled “Examples”All names and values below are made up.
Rows by region for a group, and own rows for sales representatives
Section titled “Rows by region for a group, and own rows for sales representatives”The group eu-analysts sees the rows of the region EU. A representative in the group sales-reps sees the rows whose owner_email is their own email. Everyone else gets no rows.
{ "rules": [ { "groups": ["eu-analysts"], "condition": {"column": "region", "op": "eq", "value": "EU"} }, { "groups": ["sales-reps"], "condition": {"column": "owner_email", "op": "eq", "value": {"$identity": "email"}} } ], "default": {"false": true}}An administrator who sees every row
Section titled “An administrator who sees every row”Add a rule for the administrator with ID 1234. Rules combine with OR, so this reader sees all rows even when other rules are narrower.
{ "subjects": ["keboola|1234"], "condition": {"true": true}}Columns for a group
Section titled “Columns for a group”Members of analysts see three columns and the administrator sees one more. Nobody else can read the table. The salary column is hidden from both.
{ "rules": [ { "groups": ["analysts"], "visible_columns": ["id", "region", "amount"] }, { "subjects": ["keboola|1234"], "visible_columns": ["id", "region", "amount", "owner_email"] } ]}A table everyone may read
Section titled “A table everyone may read”A reference table that every identified reader may read in full needs a policy that selects every reader: a row policy with the wildcard, or a column policy with the wildcard that lists the columns. Without a policy the table is refused.
{ "rules": [ {"subjects": ["*"], "condition": {"true": true}} ]}Create a policy with the Keboola CLI
Section titled “Create a policy with the Keboola CLI”Both groups have the same commands. --rules accepts JSON, @file or - for standard input. Add --dry-run to any command that writes to see the result without writing it.
| Command | What it does |
|---|---|
rls list, cls list | List the policies of a project |
rls detail, cls detail | Show the rules of one policy (--policy-id) |
rls schema, cls schema | Print the live JSON Schema of the policy from the Metastore |
rls create, cls create | Create the policy of one table (--table-id, --rules; rls create also takes --default) |
rls update, cls update | Change only the options you pass |
rls delete, cls delete | Delete a policy. In a governed workspace a table without any policy is unreadable: an administrator gets HTTP 403, an end user of a Data App an empty result. A policy that a workspace binding lists cannot be deleted (HTTP 409): to delete a policy a Data App uses, remove it from rlsPolicies or clsPolicies, redeploy the app and archive its older deployments first, because each deployment keeps its own binding |
rls setup | Guided setup in a terminal: pick tables, build a condition, preview, write |
Useful options of create and update: --scope targeted|organization, --target-project (repeatable) and --dialect snowflake|bigquery. Without a --default, a reader that no rule selects is refused.
kbagent rls create --project my-project --table-id in.c-crm.orders --rules @rls-rules.json --default '{"false": true}' --dry-runkbagent cls create --project my-project --table-id in.c-crm.orders --rules @cls-rules.json --yesThe CLI refuses a rule that selects by principal or email before anything is written. It also checks the dialect against the project backend and reports every problem it finds.
Create a policy with the API
Section titled “Create a policy with the API”Policies are objects of the Keboola Metastore. Its address is the address of your stack with connection. replaced by metastore., for example https://metastore.<stack>.keboola.com. Authenticate with the project administrator’s Storage token in the X-StorageApi-Token header.
The name of the object is the table ID, and a project can have one policy of each kind per table. The rules and default in data are the ones from the examples above.
curl -sS -X POST "https://metastore.<stack>.keboola.com/api/v1/repository/rls-policy" \ -H "X-StorageApi-Token: $KBC_STORAGE_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "in.c-crm.orders", "branch": "main", "scope": "targeted", "data": { "table": "in.c-crm.orders", "rules": [ { "groups": ["eu-analysts"], "condition": {"column": "region", "op": "eq", "value": "EU"} } ], "default": {"false": true} } }'For a column policy post to /api/v1/repository/cls-policy with "visible_columns" in each rule instead of a condition, and without default. To list the policies visible to the project:
curl -sS "https://metastore.<stack>.keboola.com/api/v1/repository/rls-policy" \ -H "X-StorageApi-Token: $KBC_STORAGE_TOKEN"A rule that selects by email is rejected by the schema validation (HTTP 422), and a second policy of the same kind for the same table is rejected as a duplicate (HTTP 409).
See also
Section titled “See also”- Row-level and column-level security: what is protected, deny by default, limitations
- Keboola CLI
- MCP Server
Was this page helpful?
Thank you for helping improve Keboola's documentation!