You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Row counts depend on which ATS positions a platform owner holds: seeker/employer RLS composes over viewAllRecords on ats_employer and ats_offer only #30
Observed while browser-testing #4 (the one-app fold) on claude/issue-4-single-app at cc5563f: OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai, memory driver, demo-en seed (791 rows, 0 errored). Signed in as the owner, positions were added and removed through POST / DELETE /api/v1/data/sys_user_position, with a fresh sign-in after every change, and GET /api/v1/data/OBJECT?top=500 was counted as that user.
positions held
ats_employer
ats_job
ats_application
ats_offer
ats_candidate
ats_interview
none (owner standing only)
12
40
200
14
80
40
platform_admin
12
40
200
14
80
40
platform_ops
12
40
200
14
80
40
job_seeker
9 (the verified ones)
40
200
0
80
40
employer_recruiter
0
40
200
0
80
40
employer_admin
0
40
200
0
80
40
platform_admin + job_seeker
9
40
200
0
80
40
platform_admin + employer_recruiter
0
40
200
0
80
40
all three
9
40
200
0
80
40
Two things in that table do not fit one rule:
Binding an extra position removes rows from a platform administrator.ats_platform_admin carries viewAllRecords on ats_employer and ats_offer, and the owner's admin_full_access wildcard does too, yet also holding job_seeker drops employers to the 9 verified ones (seeker_verified_employers: verification_status == 'verified') and offers to 0 (seeker_own_offers: candidate_user == current_user.id), and also holding an employer position drops both to 0 (Employer-side RLS policies fail closed for everyone: IN (current_user.accessible_org_ids) is the deprecated SQL-style spelling and the bridge cannot bind the variable #18's fail-closed accessible_org_ids). The restrictive policy of the narrower set wins over the bypass of the wider one.
It happens on two objects and not on the other four. The same sets carry policies on ats_job (status == 'published', which would leave 22), ats_application (candidate_user == current_user.id, which would leave 0), ats_candidate (user == current_user.id) and ats_interview — and those counts never move. ats_employer and ats_offer are private OWD, but so is ats_application, which is unaffected, so OWD alone does not explain the split.
What it looks like in the app: with all three positions held, the Platform group's Employers Pending queue reads "No matching records"; under platform_admin alone the same queue shows its 2 rows (docs/evidence/issue-4/16-platform-employers-pending-2-rows.png on the #4 branch). No seeded persona holds two audiences, so this does not block #4, but it decides what a platform operator who is also a candidate sees, and it is a measured data point for #18.
Not touched by the #4 PR, whose src/security/ change is limited to the five systemPermissions grants. Whether the composition rule lives upstream (@objectstack/plugin-security 17.3.0) or follows from how these sets are authored is the question to settle before anyone edits a policy.
Observed while browser-testing #4 (the one-app fold) on
claude/issue-4-single-appatcc5563f:OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai, memory driver, demo-en seed (791 rows, 0 errored). Signed in as the owner, positions were added and removed throughPOST/DELETE /api/v1/data/sys_user_position, with a fresh sign-in after every change, andGET /api/v1/data/OBJECT?top=500was counted as that user.platform_adminplatform_opsjob_seekerverifiedones)employer_recruiteremployer_adminplatform_admin+job_seekerplatform_admin+employer_recruiterTwo things in that table do not fit one rule:
ats_platform_admincarriesviewAllRecordsonats_employerandats_offer, and the owner'sadmin_full_accesswildcard does too, yet also holdingjob_seekerdrops employers to the 9verifiedones (seeker_verified_employers:verification_status == 'verified') and offers to 0 (seeker_own_offers:candidate_user == current_user.id), and also holding an employer position drops both to 0 (Employer-side RLS policies fail closed for everyone:IN (current_user.accessible_org_ids)is the deprecated SQL-style spelling and the bridge cannot bind the variable #18's fail-closedaccessible_org_ids). The restrictive policy of the narrower set wins over the bypass of the wider one.ats_job(status == 'published', which would leave 22),ats_application(candidate_user == current_user.id, which would leave 0),ats_candidate(user == current_user.id) andats_interview— and those counts never move.ats_employerandats_offerareprivateOWD, but so isats_application, which is unaffected, so OWD alone does not explain the split.What it looks like in the app: with all three positions held, the Platform group's Employers Pending queue reads "No matching records"; under
platform_adminalone the same queue shows its 2 rows (docs/evidence/issue-4/16-platform-employers-pending-2-rows.pngon the #4 branch). No seeded persona holds two audiences, so this does not block #4, but it decides what a platform operator who is also a candidate sees, and it is a measured data point for #18.Not touched by the #4 PR, whose
src/security/change is limited to the fivesystemPermissionsgrants. Whether the composition rule lives upstream (@objectstack/plugin-security17.3.0) or follows from how these sets are authored is the question to settle before anyone edits a policy.