Blogs Engineering

Returning Joined Columns Under Restricted Access in pREST

Engelstalig artikel. With restrict=true, pREST dropped every column from a joined table. The merged fix applies the joined table's own field permissions instead.

Goprest/prestPostgreSQLaccess controlREST API

A permission check that only looked at one side of the JOIN, with regression coverage for denied joined tables.

Kort antwoord

With restrict = true, pREST builds the SELECT list from the permitted fields of the requested table only. Any _join target lost all its columns, even when the configuration granted read access to them. The fix resolves the join target, checks its own table and field permissions for the same user and operation, and adds only the permitted columns, qualified by table name. Joined tables without read permission still contribute nothing. It was merged as PR #1032 and closed issue #364.

The problem

pREST serves instant REST APIs over PostgreSQL and MySQL. Issue #364, opened in January 2020, gave a clear reproduction: an employee and a department table, both granted read with explicit fields, and a request:

/test/public/department?_join=inner:employee:department.emp_id:$eq:employee.id

The response contained only department columns. "However if access restricted is false it works as normal."

Root cause

From the PR description: "restrict=true builds the SELECT list from the main table field permissions only, dropping joined table columns".

The fix

A new helper looks up permissions for the joined table:

func (adapter *postgres) joinFieldsPermissions(r *http.Request, database, schema, op, userName string) (fields []string, joined bool) {
    joinSchema, joinTable, ok := joinTableByRequest(r, schema)
    if !ok {
        return
    }
    joined = true
    if !adapter.TablePermissions(database, joinSchema, joinTable, op, userName) {
        return
    }
    allowedFields := adapter.fieldsByPermission(database, joinSchema, joinTable, op, userName)
    if containsAsterisk(allowedFields) {
        return []string{joinTable + ".*"}, true
    }
    return qualifyFields(joinTable, allowedFields), true
}

When a join is present, main-table columns are qualified too, so they can't become ambiguous. _select accepts qualified fields from either side, and sanitizeSelectField gained validated support for table.*.

By the numbers

  • Repository: prest/prest, 4,630 stars (Nov 3, 2026), Go, MIT
  • Files changed: 4
  • Lines: +404 / -6
  • Commits: 4 (an initial fix and three "address review feedback" commits)
  • Opened: Aug 21, 2026. Merged: Sep 16, 2026. Time to merge: 25 days, 7 hours
  • Review: approved and merged by a maintainer; CLA signed

How the review went

The maintainer asked for two things: signing the CLA, and "dont forget to include integration tests for your solution". Both were done. The PR added integration/postgres/controllers/join_acl_test.go, which runs against the auth-enabled server with restricted access and covers an unreadable joined table, a readable one, and a wildcard field grant. CodeRabbit flagged a critical authorization gap in the initial patch: a base-table wildcard could bypass joined-table checks. Follow-up commit 7290488 moved join authorization ahead of wildcard handling, and the final diff adds TestSelectJoin_WildcardFieldsStayOnRequestedTable to cover denied joined-table columns, including an explicit _select. Three follow-up commits addressed review feedback. The maintainer approved with "Thanks @Roshan931 for the contribution".

Takeaways

  • Access control has to follow the query. Once a request can touch two tables, the permission check has to look at both.
  • Test the negative case. One integration test checks that columns from a table the user can't read never appear.

Veelgestelde vragen

Can a join now expose columns a user isn't allowed to read?

The fix checks the joined table's permissions for the same user and operation. If the user lacks read permission, the joined table contributes no columns, and an integration test asserts that.

Where can I read more?

See the pull request and the original issue.

Het doel

Restricted mode should restrict, not break. Applying each table's own grants keeps JOINs working without widening access.

Voer de loop uit

Zet productiesignalen om in gereviewde fixes.

Start een gratis proefperiode en zie hoe Prilog echte incidenten koppelt aan pull requests op codeniveau.