Skip to content

service-analytics: on driver-sql (sqlite) the NativeSQLStrategy answers POST /analytics/dataset/query without the object-level grant or the tenant wall — a user with NO read grant on an object gets its row count (200) where GET /data/OBJECT answers 403; the memory driver's path refuses both #16645

Description

@claude

Measured on @objectstack/cli 17.3.0 (service-analytics / rest / driver-sql / plugin-security 17.3.0) in the objectstack-ai/ats app, demo seed, objectstack dev --fresh on the default sqlite driver and on --database-driver memory. The request is the smallest dataset query there is — an inline { object, measures: [{ name: 'cnt', aggregate: 'count' }] } with selection: { measures: ['cnt'] } — compared with GET /api/v1/data/OBJECT?$top=1&$count=true for the same signed-in user.

persona object sqlite: analytics sqlite: REST memory: analytics memory: REST
job seeker (ats_job_seeker, no grant on this object) ats_employer_member 24 403 PERMISSION_DENIED 403 PERMISSION_DENIED 403 PERMISSION_DENIED
job seeker (grant + RLS verification_status == 'verified') ats_employer 9 0 (tenant wall) 9 9
platform admin (viewAllRecords) ats_employer / ats_employer_member / ats_interview / ats_offer 12 / 30 / 40 / 14 0 / 0 / 0 / 0 (tenant wall, ats#39) 12 / 30 / 40 / 14 12 / 30 / 40 / 14
employer admin ×2 (RLS on employer_org) all seven objects equal to REST equal to REST

So three layers behave differently on the sqlite analytics path:

  • Object-level grant — not applied. The seeker's permission set has no ats_employer_member entry at all; the list API refuses, the dataset query counts 24 rows. (ats_employer_member is tenancy-scoped and private; the 24 is not "all 30", so something narrowed it — a sharing/tenant clause — but the grant that should have made it 403 was never consulted.)
  • Tenant wall (Layer 0) — not applied. Platform staff read 12 / 30 / 40 / 14 through analytics and 0 through /data on sqlite (On the sqlite driver, tenant-scoped objects (ats_employer, ats_interview, ats_offer) return 0 rows to platform admins holding viewAllRecords; the memory driver returns all of them ats#39 is the /data side of that; whichever side is right, the two paths disagree on the same driver for the same user).
  • Row-level policies — applied. Both employer administrators get exactly their own numbers on both paths (1 / 3 / 10 / 2 / 5 / 27 / 70 and 1 / 3 / 6 / 2 / 5 / 31 / 69), and the seeker's ats_employer count is the RLS-scoped 9 — so security.getReadFilter is threaded (ADR-0021 D-C); it is the other two checks that the native path lacks.

On the memory driver the NativeSQLStrategy cannot run (raw SQL unsupported) and the query goes through the ObjectQL engine, where all three layers apply and every cell equals REST — the seeker's ats_employer_member included. That is what points at the native-SQL strategy rather than at the app's metadata: same metadata, same user, same request; the answer depends on which strategy served it.

Why it matters. /analytics/dataset/query is the endpoint every dashboard widget and report calls (objectui DatasetWidget). Any signed-in user can post an inline dataset to it, so on a SQL deployment a user with no grant on an object can learn its row count, and — with dimensions — its grouped counts by any field (GROUP BY over the object's columns), which is a read. The record-level half of this was #4467 (closed); this is the object-level grant and the tenant wall on the native strategy.

Expected. The native strategy consults the same object-level read grant and tenant scope the engine path consults (or is refused for a user the engine path would refuse), so the two strategies give the same answer for the same user; a 403 where /data answers 403.

Evidence files (tables above, plus the widget-level cross-check they came from): docs/evidence/issue-8/22-analytics-vs-rest-by-persona-memory.md and 23-…-sqlite.md on branch claude/issue-8-dashboards of objectstack-ai/ats.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions