Skip to content

proxy: row-level security policies (row filtering) #380

Description

@babltiga

Background

Proposed for v1.3. AccessFlow permissions cover schemas, tables, and columns (allowed_schemas, allowed_tables, restricted_columns) but not rows — a user with customers access sees every tenant's rows. Row-level security is standard in data-access governance (Satori, Cyral, Immuta, Privacera). The proxy already walks the parsed AST for the schema allow-list, so the enforcement hook point exists.

Scope

Per-user/group/datasource row predicates that the proxy applies to SELECT (and optionally DML) so only authorised rows are returned/affected.

  • New table row_security_policy (Flyway V56+): id, organization_id, datasource_id, table_name (schema-qualified), predicate_template (e.g. region = :user.region), applies_to (role / group / user), enabled, timestamps.
  • Predicate variables resolve from attributes AccessFlow already holds — requester role, group memberships, and SSO-synced claims (the IdP attributes already mapped at login). No free-form SQL from the end user; the template is admin-authored and parameterised.
  • Enforcement in the proxy module at the AST layer: for each referenced table that has an applicable policy, inject the predicate into the WHERE clause (or wrap as a subquery/CTE) using the dialect-aware parser. Bind values as parameters — never string-concatenate (CLAUDE.md security rule Backend Infrastructure Setup #1). Reject (HTTP 422) queries the engine cannot safely rewrite, rather than running them unfiltered.
  • Must compose with the existing restricted_columns masking and the schema/table allow-list, and behave correctly for joins referencing multiple policied tables.
  • Audit: record on the query that N row-security policies were applied (policy ids), without storing row data.
  • Frontend: a "Row security" card on DatasourceSettingsPage — table, predicate template, scope (role/group/user), test-preview. Reuse existing settings UI patterns.

Acceptance criteria

  • Flyway migration V56+.
  • Predicate injection is parameter-bound and dialect-aware; no string-concatenated SQL.
  • Joins across multiple policied tables filtered correctly; un-rewritable queries rejected with a clear 422, not run unfiltered.
  • Composes with column masking + schema/table allow-list.
  • Applied-policy ids recorded in the audit log (no row data).
  • Unit tests for the rewrite per dialect + the multi-table join case; Testcontainers integration test proving rows are filtered for a scoped user.
  • ApplicationModulesTest / ApiPackageDependencyTest green; coverage ≥ 90%.
  • No hardcoded strings; validation parity on the settings form.
  • Playwright spec: configure a policy → run a SELECT as a scoped user → see filtered rows.
  • docs/03-data-model.md, docs/05-backend.md, docs/06-frontend.md, docs/07-security.md, docs/12-roadmap.md (add v1.3) and website/ updated.

Pointers

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions