Skip to content

feat(rls): implement flexible RLS modes (public-read vs private)#80

Merged
yash-pouranik merged 3 commits intomainfrom
feature/flexible-rls-modes
Apr 4, 2026
Merged

feat(rls): implement flexible RLS modes (public-read vs private)#80
yash-pouranik merged 3 commits intomainfrom
feature/flexible-rls-modes

Conversation

@yash-pouranik
Copy link
Copy Markdown
Owner

@yash-pouranik yash-pouranik commented Apr 4, 2026

  • Analyze review comments
  • Fix getAllData: apply RLS filter after QueryEngine.filter() using .and([baseFilter]) to prevent client bypass via query params
  • Verify getSingleDoc: already uses $and approach (non-bypassable, no key overwrite risk)
  • Fix Dashboard UI: update checkbox label from "Enable RLS for publishable-key writes" to "Enable RLS access rules for publishable-key requests" and add dynamic help text based on rlsMode
  • Update tests in data.controller.read.test.js to match new mock structure and assertions
  • All 50 tests pass, web-dashboard lints clean

Copilot AI review requested due to automatic review settings April 4, 2026 10:53
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ur-backend-web-dashboard Ready Ready Preview, Comment Apr 4, 2026 11:09am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2894698-7674-40d6-a9ae-061958dd6b6b

📥 Commits

Reviewing files that changed from the base of the PR and between cf18567 and efcab14.

📒 Files selected for processing (1)
  • apps/public-api/src/controllers/data.controller.js

📝 Walkthrough

Walkthrough

Adds read-side row-level security: introduces public-read and private modes, normalizes legacy owner-write-only to public-read, enforces read filters via new middleware, applies filters in data controllers, updates schema defaults and dashboard UI, and adds tests and docs reflecting the multi-mode RLS behavior.

Changes

Cohort / File(s) Summary
Schema & Default Mode
packages/common/src/models/Project.js
RLS mode enum expanded to ["public-read","private","owner-write-only"]; default changed to "public-read".
Dashboard API Configuration
apps/dashboard-api/src/controllers/project.controller.js
Default RLS mode now "public-read"; validation allowlist expanded to include "public-read", "private", and legacy "owner-write-only".
Read Authorization Middleware
apps/public-api/src/middlewares/authorizeReadOperation.js
New middleware that sets req.rlsFilter: secret key bypass, 404 if collection missing, public reads when disabled or public-read, owner-scoped filter for private (requires auth), 403 for unsupported modes.
Write Authorization Middleware
apps/public-api/src/middlewares/authorizeWriteOperation.js
Validation updated to accept "public-read", "private", and "owner-write-only"; default normalized to "public-read".
Data Read Operations
apps/public-api/src/controllers/data.controller.js
getAllData and getSingleDoc now build baseFilter from req.rlsFilter and pass it into find() / findOne() to apply RLS constraints.
Routes Configuration
apps/public-api/src/routes/data.js
GET routes now include resolvePublicAuthContext and authorizeReadOperation before controller handlers to enforce read RLS.
Tests — Authorization & Data Read
apps/public-api/src/__tests__/authorizeReadOperation.test.js, apps/public-api/src/__tests__/data.controller.read.test.js
New tests: middleware behavior across modes and auth scenarios; controller tests ensure req.rlsFilter is forwarded to model query calls.
Dashboard UI
apps/web-dashboard/src/pages/Database.jsx
Adds rlsMode state (default "public-read"), normalizes legacy mode to public-read, and exposes an "Access Mode" selector for public-read / private.
Documentation & Examples
README.md, docs/*.md, examples/social-demo/*
Docs and examples updated to describe public-read vs private, normalize owner-write-only as legacy public-read, and update example collection configs and roadmap text.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant PublicAPI as Public API
    participant AuthMW as authorizeReadOperation
    participant DataCtrl as data.controller
    participant DB as Database

    Client->>PublicAPI: GET /:collectionName or /:collectionName/:id
    PublicAPI->>AuthMW: resolvePublicAuthContext -> evaluate RLS
    AuthMW->>AuthMW: if secret key -> set rlsFilter = {}
    alt collection missing
        AuthMW->>Client: 404 { error: 'Collection not found' }
    else RLS disabled or public-read
        AuthMW->>AuthMW: set rlsFilter = {}
        AuthMW->>PublicAPI: next()
    else private + no auth
        AuthMW->>Client: 401 { error: 'Authentication required' }
    else private + auth
        AuthMW->>AuthMW: set rlsFilter = { ownerField: authUser.userId }
        AuthMW->>PublicAPI: next()
    end
    PublicAPI->>DataCtrl: handler called with req.rlsFilter
    DataCtrl->>DataCtrl: build baseFilter from req.rlsFilter
    DataCtrl->>DB: Model.find(baseFilter) / Model.findOne({_id, ...baseFilter})
    DB->>DataCtrl: return documents
    DataCtrl->>Client: res.json(results)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hop through code with gentle cheer,
Public reads for many near,
Private keeps the owners dear,
Legacy hops into new gear. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing flexible RLS modes (public-read vs private), which is the primary objective of this PR across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/flexible-rls-modes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces flexible Row-Level Security (RLS) read modes across the backend, common model schema, and dashboard UI—so collections can be configured as public-read (anyone can read) or private (owner-only reads), while keeping legacy owner-write-only compatible.

Changes:

  • Added RLS mode support (public-read, private, legacy owner-write-only) in the Project collection schema and dashboard API defaults/validation.
  • Enforced read-side RLS via a new authorizeReadOperation middleware and applied req.rlsFilter to public-api read controllers.
  • Updated dashboard UI + docs/examples to reflect the new modes, and added unit tests for read authorization + controller filtering.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Updates RLS docs to mention read scoping by mode.
packages/common/src/models/Project.js Expands rls.mode enum and sets default to public-read.
examples/social-demo/README.md Updates demo config to use public-read.
examples/social-demo/COLLECTIONS_GUIDE.md Updates demo RLS settings to public-read.
docs/rls-implementation-roadmap.md Revises roadmap to include public-read and private behaviors.
docs/introduction.md Documents read behavior and legacy mapping.
docs/database.md Adds RLS read mode explanation to database API docs.
apps/web-dashboard/src/pages/Database.jsx Adds UI selector for RLS access mode and legacy mode normalization.
apps/public-api/src/routes/data.js Adds resolvePublicAuthContext + authorizeReadOperation to GET routes.
apps/public-api/src/middlewares/authorizeWriteOperation.js Updates write middleware to accept new/legacy RLS modes.
apps/public-api/src/middlewares/authorizeReadOperation.js New middleware that sets req.rlsFilter based on RLS mode and auth.
apps/public-api/src/controllers/data.controller.js Applies req.rlsFilter to list/detail reads.
apps/public-api/src/tests/data.controller.read.test.js Tests that controllers pass rlsFilter into queries.
apps/public-api/src/tests/authorizeReadOperation.test.js Tests read middleware behavior across key roles and modes.
apps/dashboard-api/src/controllers/project.controller.js Updates default RLS mode and validates allowed modes on update.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 106 to 107
const features = new QueryEngine(Model.find(baseFilter), req.query)
.filter()
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAllData applies req.rlsFilter by seeding the query with Model.find(baseFilter), but QueryEngine.filter() later calls this.query.find(mongoQuery) (packages/common/src/utils/queryEngine.js:31). In Mongoose, later .find() conditions can override earlier ones for the same field, so a client could pass a query param matching the owner field (e.g. ?userId=other) and bypass the RLS owner constraint in private mode.

To make the RLS constraint non-bypassable, combine RLS and user filters with an $and, or apply/enforce the RLS filter after QueryEngine.filter() in a way that cannot be overridden (and consider stripping the ownerField from user-supplied query params for private collections).

Suggested change
const features = new QueryEngine(Model.find(baseFilter), req.query)
.filter()
const features = new QueryEngine(Model.find(), req.query)
.filter();
if (Object.keys(baseFilter).length > 0) {
features.query = features.query.and([baseFilter]);
}
features

Copilot uses AI. Check for mistakes.

const doc = await Model.findById(id).lean();
const baseFilter = req.rlsFilter && typeof req.rlsFilter === 'object' ? req.rlsFilter : {};
const doc = await Model.findOne({ _id: id, ...baseFilter }).lean();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSingleDoc builds the query as { _id: id, ...baseFilter }. If baseFilter ever contains _id (e.g. when rls.ownerField === '_id' for the users collection in private mode), the spread will overwrite the path parameter id, causing the request to ignore /:id and potentially return the wrong document.

Prefer composing filters without key overwrites (e.g., { ...baseFilter, _id: id } or an explicit $and of the id constraint and baseFilter).

Suggested change
const doc = await Model.findOne({ _id: id, ...baseFilter }).lean();
const doc = await Model.findOne({ ...baseFilter, _id: id }).lean();

Copilot uses AI. Check for mistakes.
Comment on lines 785 to 786
<span>Enable RLS for publishable-key writes</span>
</label>
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkbox label still says “Enable RLS for publishable-key writes”, but this dialog now also configures read access via the new Access Mode selector (e.g. private restricts reads). This wording is now misleading for users and can cause misconfiguration.

Update the label/help text to reflect that enabling RLS may also affect read access depending on the selected mode (or conditionally explain read behavior when rlsMode === 'private').

Suggested change
<span>Enable RLS for publishable-key writes</span>
</label>
<span>Enable RLS access rules for publishable-key requests</span>
</label>
<p className="rls-help-text">
{rlsMode === "private"
? "When enabled, publishable-key access is restricted to the owner for both reads and writes."
: "When enabled, publishable-key writes are restricted to the owner. Reads remain available according to the selected access mode."}
</p>

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements read-side Row-Level Security (RLS) by introducing public-read and private modes, supported by a new authorizeReadOperation middleware and updates to the data controller. The changes also include dashboard UI enhancements and documentation updates. A significant logic error was noted in the getSingleDoc controller where the RLS filter might overwrite the requested document ID if the owner field is _id, potentially leading to incorrect data retrieval; a suggestion was provided to use an $and operator to prevent this.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
apps/public-api/src/controllers/data.controller.js (1)

144-145: Prefer $and when combining _id with req.rlsFilter.

Object spread gives req.rlsFilter precedence over the path id. If any legacy collection still uses _id as its ownerField, GET /:id can return the caller's own document instead of the requested one.

♻️ Suggested refactor
-    const doc = await Model.findOne({ _id: id, ...baseFilter }).lean();
+    const doc = await Model.findOne({ $and: [{ _id: id }, baseFilter] }).lean();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/controllers/data.controller.js` around lines 144 - 145,
Replace the object-spread merge with an $and query to ensure the requested id
cannot be overridden by req.rlsFilter: when building the query for Model.findOne
use a conjunctive $and array containing { _id: id } and the computed baseFilter
(derived from req.rlsFilter) instead of spreading baseFilter into the same
object; update the code that computes baseFilter and the Model.findOne call
(look for baseFilter, req.rlsFilter, Model.findOne, id) to construct and pass {
$and: [{ _id: id }, baseFilter] }.
apps/dashboard-api/src/controllers/project.controller.js (1)

1308-1311: Normalize legacy owner-write-only before saving it.

This still persists the legacy string as-is. Since apps/web-dashboard/src/pages/Database.jsx, Lines 789-801 only exposes public-read and private, normalizing here keeps backward-compatible input support without surfacing a third stored mode back to clients.

♻️ Suggested refactor
-        const validMode = mode || collection?.rls?.mode || 'public-read';
+        const requestedMode = mode || collection?.rls?.mode || 'public-read';
         const allowedModes = new Set(['public-read', 'private', 'owner-write-only']);
-        if (!allowedModes.has(validMode)) {
+        if (!allowedModes.has(requestedMode)) {
             return res.status(400).json({ error: "Unsupported RLS mode. Allowed: public-read, private, owner-write-only (legacy)." });
         }
+        const validMode = requestedMode === 'owner-write-only' ? 'public-read' : requestedMode;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard-api/src/controllers/project.controller.js` around lines 1308 -
1311, The controller accepts the legacy RLS string "owner-write-only" and
persists it unchanged; normalize legacy values before saving so clients only see
"public-read" or "private". Update the logic around validMode (and where
allowedModes is used) to map "owner-write-only" to "private" (or another
canonical value) before validation/response and before saving to the DB,
ensuring the rest of the codepath works with the normalized value instead of the
legacy string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/public-api/src/__tests__/data.controller.read.test.js`:
- Around line 82-91: The controller's getSingleDoc currently merges
req.rlsFilter into the findOne query which can overwrite the route _id when
ownerField === '_id'; change the query construction in getSingleDoc to preserve
both predicates by using an $and clause when base/RLS filter exists (e.g., if
baseFilter non-empty use { $and: [{ _id: id }, baseFilter ] } else { _id: id }),
and update the test to expect mockFindOne to have been called with that
$and-shaped query instead of a merged object; refer to getSingleDoc and any
baseFilter/rlsFilter/ownerField variables to locate and modify the logic and the
assertion.

In `@apps/public-api/src/controllers/data.controller.js`:
- Around line 105-107: The current approach seeds Model.find(baseFilter) but
QueryEngine (packages/common/src/utils/queryEngine.js) re-calls
.find(mongoQuery) using raw req.query which allows req.rlsFilter to be bypassed;
modify QueryEngine (or its usage) so the request-level RLS predicate is merged
into and enforced on the final mongoQuery before any .find is executed:
specifically ensure req.rlsFilter (baseFilter) is combined with parsed req.query
filters (e.g., deep merge or $and wrap) inside QueryEngine (or pass baseFilter
into QueryEngine and have QueryEngine.mergeBaseFilter/constructor merge it) so
that Model.find is only ever called with the combined query and no subsequent
.find replaces it, preserving the owner predicate.

In `@docs/rls-implementation-roadmap.md`:
- Around line 42-44: The roadmap wording is inconsistent: V1 now includes read
behavior for `public-read` and `private` but later sections still call the
change "write-only" and claim "no extra overhead on read routes." Update those
later sections (the parts describing auth-context changes and performance
impact) to reflect that reads are part of V1: remove or rephrase any
"write-only" language, change "no extra overhead on read routes" to accurately
state the overhead/behavior for reads under `public-read` and `private`, and
ensure the auth-context explanation applies to read paths as well so the
document consistently describes current V1 behavior for `public-read` and
`private`.

In `@examples/social-demo/COLLECTIONS_GUIDE.md`:
- Around line 165-169: The RLS settings declare ownerField: userId for the
follows collection but the follows schema only defines followerId and
followingId, so writes will fail ownership checks; update the follows schema to
include a userId field (or change ownerField to followerId) to match the RLS
config, e.g., add a userId property to the follows schema or set ownerField to
"followerId" and ensure any write code uses that field consistently (refer to
the follows schema, ownerField, followerId, and followingId when making the
change).

In `@examples/social-demo/README.md`:
- Around line 140-145: The posts schema in the README is missing the userId
field required by the RLS config's ownerField: userId; update the posts schema
example (the object labelled "posts" or the "posts" schema block) to include a
"userId" property (type String, required true) alongside the existing
authorId/authorUsername/authorDisplayName fields so documents created by clients
will contain the owner field RLS relies on.

In `@README.md`:
- Around line 99-103: The "Read any collection" row in the Key Behavior table
must be split into two rows to match the README steps: create one row for
public-read indicating pk_live reads require no Authorization header (public
access), and a separate row for private indicating pk_live reads require a valid
JWT in the Authorization: Bearer <token> header and that urBackend enforces the
JWT's userId matches the document's owner field (owner field like `userId`);
also ensure the note about `owner-write-only` being treated as `public-read`
remains consistent and update any entrypoint/docs text that still states pk_live
reads are unauthenticated for all modes.

---

Nitpick comments:
In `@apps/dashboard-api/src/controllers/project.controller.js`:
- Around line 1308-1311: The controller accepts the legacy RLS string
"owner-write-only" and persists it unchanged; normalize legacy values before
saving so clients only see "public-read" or "private". Update the logic around
validMode (and where allowedModes is used) to map "owner-write-only" to
"private" (or another canonical value) before validation/response and before
saving to the DB, ensuring the rest of the codepath works with the normalized
value instead of the legacy string.

In `@apps/public-api/src/controllers/data.controller.js`:
- Around line 144-145: Replace the object-spread merge with an $and query to
ensure the requested id cannot be overridden by req.rlsFilter: when building the
query for Model.findOne use a conjunctive $and array containing { _id: id } and
the computed baseFilter (derived from req.rlsFilter) instead of spreading
baseFilter into the same object; update the code that computes baseFilter and
the Model.findOne call (look for baseFilter, req.rlsFilter, Model.findOne, id)
to construct and pass { $and: [{ _id: id }, baseFilter] }.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d7d53f2b-838a-4008-a9e1-8d515c89db0d

📥 Commits

Reviewing files that changed from the base of the PR and between f286c6f and cf18567.

📒 Files selected for processing (15)
  • README.md
  • apps/dashboard-api/src/controllers/project.controller.js
  • apps/public-api/src/__tests__/authorizeReadOperation.test.js
  • apps/public-api/src/__tests__/data.controller.read.test.js
  • apps/public-api/src/controllers/data.controller.js
  • apps/public-api/src/middlewares/authorizeReadOperation.js
  • apps/public-api/src/middlewares/authorizeWriteOperation.js
  • apps/public-api/src/routes/data.js
  • apps/web-dashboard/src/pages/Database.jsx
  • docs/database.md
  • docs/introduction.md
  • docs/rls-implementation-roadmap.md
  • examples/social-demo/COLLECTIONS_GUIDE.md
  • examples/social-demo/README.md
  • packages/common/src/models/Project.js

Comment on lines +82 to +91
test('getSingleDoc applies rlsFilter to findOne()', async () => {
const req = makeReq({ rlsFilter: { userId: 'user_1' } });
const res = makeRes();

await getSingleDoc(req, res);

expect(mockFindOne).toHaveBeenCalledWith({
_id: '507f1f77bcf86cd799439011',
userId: 'user_1',
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve both the route id and the RLS constraint in detail reads.

This test currently locks in the findOne({ _id: id, ...req.rlsFilter }) shape. When ownerField === '_id', that merge rewrites the requested id, so /.../:otherId can return the caller's own document instead of 404. Please fix the controller query to keep both predicates and update this assertion to cover the _id case.

Possible update once the controller preserves both predicates
-        expect(mockFindOne).toHaveBeenCalledWith({
-            _id: '507f1f77bcf86cd799439011',
-            userId: 'user_1',
-        });
+        expect(mockFindOne).toHaveBeenCalledWith({
+            $and: [
+                { _id: '507f1f77bcf86cd799439011' },
+                { userId: 'user_1' },
+            ],
+        });
const query = Object.keys(baseFilter).length
  ? { $and: [{ _id: id }, baseFilter] }
  : { _id: id };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/__tests__/data.controller.read.test.js` around lines 82 -
91, The controller's getSingleDoc currently merges req.rlsFilter into the
findOne query which can overwrite the route _id when ownerField === '_id';
change the query construction in getSingleDoc to preserve both predicates by
using an $and clause when base/RLS filter exists (e.g., if baseFilter non-empty
use { $and: [{ _id: id }, baseFilter ] } else { _id: id }), and update the test
to expect mockFindOne to have been called with that $and-shaped query instead of
a merged object; refer to getSingleDoc and any baseFilter/rlsFilter/ownerField
variables to locate and modify the logic and the assertion.

Comment on lines 42 to +44
Read behavior in V1:
- Keep existing behavior unchanged initially (to reduce risk).
- `public-read` keeps existing behavior for reads.
- `private` filters reads by owner.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the remaining V1 write-only wording.

These bullets now make reads part of V1, but later sections still describe auth-context changes and performance impact as write-only / “no extra overhead on read routes.” Please align those sections so the roadmap matches the implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/rls-implementation-roadmap.md` around lines 42 - 44, The roadmap wording
is inconsistent: V1 now includes read behavior for `public-read` and `private`
but later sections still call the change "write-only" and claim "no extra
overhead on read routes." Update those later sections (the parts describing
auth-context changes and performance impact) to reflect that reads are part of
V1: remove or rephrase any "write-only" language, change "no extra overhead on
read routes" to accurately state the overhead/behavior for reads under
`public-read` and `private`, and ensure the auth-context explanation applies to
read paths as well so the document consistently describes current V1 behavior
for `public-read` and `private`.

Comment on lines 165 to 169
**RLS settings for `follows` (required):**
- enabled: `true`
- mode: `owner-write-only`
- mode: `public-read`
- ownerField: `userId`
- requireAuthForWrite: `true`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

follows still omits the configured owner field.

The guide now sets ownerField: userId, but the follows schema above only defines followerId and followingId. If readers follow this document as written, follow/unfollow writes will fail RLS ownership checks.

📝 Suggested doc fix
 Field Name    | Type    | Required | Index
 --------------|---------|----------|-------
+userId        | String  | Yes      | Yes
 followerId    | String  | Yes      | Yes
 followingId   | String  | Yes      | Yes
 createdAt     | Date    | No       | Date.now
-- [ ] `follows` collection created with 3 fields
+- [ ] `follows` collection created with 4 fields
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/social-demo/COLLECTIONS_GUIDE.md` around lines 165 - 169, The RLS
settings declare ownerField: userId for the follows collection but the follows
schema only defines followerId and followingId, so writes will fail ownership
checks; update the follows schema to include a userId field (or change
ownerField to followerId) to match the RLS config, e.g., add a userId property
to the follows schema or set ownerField to "followerId" and ensure any write
code uses that field consistently (refer to the follows schema, ownerField,
followerId, and followingId when making the change).

Comment on lines 140 to 145
For each writable collection (`posts`, `comments`, `likes`, `follows`, `profiles`):

- `enabled: true`
- `mode: owner-write-only`
- `mode: public-read`
- `ownerField: userId`
- `requireAuthForWrite: true`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add userId to the posts schema example.

This block now tells readers to use ownerField: userId, but the posts schema earlier in this README still omits userId. Following the setup verbatim leaves post documents without the field RLS writes depend on.

📝 Suggested doc fix
 **2. `posts`**
 ```json
 {
+  "userId": { "type": "String", "required": true },
   "authorId": { "type": "String", "required": true },
   "authorUsername": { "type": "String", "required": true },
   "authorDisplayName": { "type": "String" },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/social-demo/README.md` around lines 140 - 145, The posts schema in
the README is missing the userId field required by the RLS config's ownerField:
userId; update the posts schema example (the object labelled "posts" or the
"posts" schema block) to include a "userId" property (type String, required
true) alongside the existing authorId/authorUsername/authorDisplayName fields so
documents created by clients will contain the owner field RLS relies on.

Comment on lines +99 to +103
1. Enable RLS for a collection in the Dashboard and choose a mode.
2. Use `public-read` for content anyone can view, or `private` for owner-only access. (`owner-write-only` is treated as `public-read` for legacy projects.)
3. Choose the **owner field** — the document field that stores the authenticated user's ID (e.g., `userId`).
4. The client must send a valid user JWT in the `Authorization: Bearer <token>` header for `pk_live` writes and for `private` reads.
5. urBackend enforces that the JWT's `userId` matches the document's owner field.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The README still describes all pk_live reads as unauthenticated.

These new steps correctly introduce private read auth, but the “Key Behavior” table above still says Read any collection with pk_live needs no token. Please split that row into public-read and private so the entrypoint docs stay consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 99 - 103, The "Read any collection" row in the Key
Behavior table must be split into two rows to match the README steps: create one
row for public-read indicating pk_live reads require no Authorization header
(public access), and a separate row for private indicating pk_live reads require
a valid JWT in the Authorization: Bearer <token> header and that urBackend
enforces the JWT's userId matches the document's owner field (owner field like
`userId`); also ensure the note about `owner-write-only` being treated as
`public-read` remains consistent and update any entrypoint/docs text that still
states pk_live reads are unauthenticated for all modes.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@yash-pouranik
Copy link
Copy Markdown
Owner Author

@copilot apply changes based on the comments in this thread

…pdate dashboard label

Agent-Logs-Url: https://github.com/yash-pouranik/urBackend/sessions/91669e19-a2de-410f-a997-63b1a2f4f61d

Co-authored-by: yash-pouranik <172860064+yash-pouranik@users.noreply.github.com>
@yash-pouranik yash-pouranik temporarily deployed to feature/flexible-rls-modes - urBackend-frankfrut PR #80 April 4, 2026 11:09 — with Render Destroyed
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 4, 2026

@copilot apply changes based on the comments in this thread

Applied all three review fixes in commit befeb46:

  1. getAllData RLS bypass fix — The query now starts with Model.find() (no filter seed), runs QueryEngine.filter() for user-supplied params, then enforces the RLS constraint non-bypassably via features.query.and([baseFilter]) after user filtering is complete.

  2. getSingleDoc filter composition — Already using { $and: [{ _id: id }, baseFilter] } (verified safe; no key-overwrite risk regardless of ownerField value).

  3. Dashboard checkbox label/help text — Label updated to "Enable RLS access rules for publishable-key requests" with dynamic help text: shows both-read-and-write restriction message when mode is private, and write-only restriction message for public-read.

Tests updated to assert the new .and([baseFilter]) call in getAllData and the $and shape in getSingleDoc. All 50 tests pass, lint clean.

@yash-pouranik
Copy link
Copy Markdown
Owner Author

@coderabbitai Can we erge this?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check PR status, reviews, and checks
echo "=== PR STATUS ==="
gh pr view 80 --repo yash-pouranik/urBackend --json state,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,reviews

echo ""
echo "=== OPEN REVIEW COMMENTS ==="
gh pr view 80 --repo yash-pouranik/urBackend --json reviewThreads --jq '.reviewThreads[] | select(.isResolved == false) | {id: .id, isResolved: .isResolved, firstComment: .comments[0].body[:200]}'

echo ""
echo "=== ALL CHECKS ==="
gh pr checks 80 --repo yash-pouranik/urBackend

Length of output: 18553


@yash-pouranik: Not quite yet — there are a couple of things to address first:


🔴 Blocking: CI check still running

The CodeQL Advanced security analysis (Analyze (javascript-typescript)) is still in progress, causing the merge state to be UNSTABLE. Please wait for it to complete before merging.


🔴 Blocking: Unresolved review comments (6 actionable)

My earlier review flagged several issues that haven't been addressed yet. The most critical one is:

1. ⚠️ Security — QueryEngine can bypass req.rlsFilter

In apps/public-api/src/controllers/data.controller.js (around Line 105–107), getAllData seeds Model.find(baseFilter) but QueryEngine internally re-calls .find(mongoQuery) using the raw req.query — this means the RLS filter can be completely bypassed for private collections if the user crafts a custom query string.

2. _id can be overridden by rlsFilter in getSingleDoc (Line 144–145)

Object spread gives req.rlsFilter precedence over _id. If any collection uses _id as its ownerField, GET /:id could return the wrong document. Suggested fix (also flagged by Gemini):

- const doc = await Model.findOne({ _id: id, ...baseFilter }).lean();
+ const doc = await Model.findOne({ $and: [{ _id: id }, baseFilter] }).lean();

3. follows schema missing userId field (examples/social-demo/COLLECTIONS_GUIDE.md, Line 165–169)

The follows collection declares ownerField: userId in its RLS config, but the schema only has followerId and followingId — writes will fail ownership checks.

4. posts schema missing userId field (examples/social-demo/README.md, Line 140–145)

Same issue — the RLS config expects a userId field for write ownership, but the schema example doesn't include it.

5. README key behavior table needs splitting (README.md, Line 99–103)

The "Read any collection" row should be split into two rows: one for public-read (no auth needed) and one for private (JWT required, results filtered by owner).

6. Roadmap doc inconsistency (docs/rls-implementation-roadmap.md, Line 42–44)

Some sections still say "write-only" and "no extra overhead on read routes", which contradicts the private mode now being part of V1.


🟡 Nitpicks (non-blocking but recommended)

  • project.controller.js (Line 1308–1311): owner-write-only should be normalized to public-read before being persisted to the DB, not just accepted as valid input.

Summary: I'd recommend addressing at minimum issues #1 (security/QueryEngine bypass) and #2 ($and for _id) before merging, as they affect correctness and security of the private RLS mode. The others can follow in a quick patch.

@yash-pouranik yash-pouranik merged commit eefa0b9 into main Apr 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants