Skip to content

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

Description

@os-sam

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:

  1. 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.
  2. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions