Skip to content

feat(partners): partner role row-level security (RLS) with scoped edits#21386

Open
rashad wants to merge 15 commits into
mainfrom
rk-partner-rls-app
Open

feat(partners): partner role row-level security (RLS) with scoped edits#21386
rashad wants to merge 15 commits into
mainfrom
rk-partner-rls-app

Conversation

@rashad

@rashad rashad commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an external Partner self-service role that sees and edits only its own
records via row-level security (RLS), so a validated partner can sign in and manage
just the deals they're matched on.

What's included

  • partnerUser relation on Partner, Person, Company, Opportunity (+ inverse
    relations on Workspace Member) — the login member a record belongs to.
  • RLS predicates scoping each of those objects to "partnerUser IS the current
    workspace member", plus a self-scope on Workspace Member so member-typed relations
    resolve without exposing the internal team roster. Applied out-of-band via
    yarn rls:configure (the app manifest cannot ship RLS predicates).
  • Assign / unassign cascade (on-opportunity-partner-assigned logic function):
    assigning a Partner to an Opportunity stamps partnerUser onto the Opportunity +
    its Company + People; removing the Partner clears it (and cascades to the
    Company/People when no other deal of that member still uses them).
  • Partner role permissions
    • Partner profile: full read/update.
    • Opportunity: read all; update stage and amount only (every other
      user-facing field locked).
    • Company / Person: read-only.
    • Workspace Member: read-only, RLS-scoped to self.
  • partnerUser column added to the Validated Partners view so the login member
    can be assigned inline.

Install / upgrade note

After install or reinstall, run yarn rls:configure (:prod variant for prod) to
(re)apply the RLS predicates and verify the field-permission locks. Manifest sync
handles object/field permissions; predicates are applied by this script.

Platform gaps found (for the eng team)

  1. Manifest sync doesn't invalidate the roles-permissions Redis cache. Permission
    changes deployed via yarn twenty dev --once persist to the DB but aren't reflected
    in the cached snapshot used for enforcement until
    engine:workspace:metadata:permissions:roles-permissions:<workspaceId>:{data,hash}
    is flushed. Relevant on any real workspace when permissions change.
  2. Locking a server-injected field silently breaks all updates. The *.updateOne
    pre-query hook writes updatedBy into every update, so canUpdateFieldValue:false
    on updatedBy makes the permission check reject every record update with
    PERMISSION_DENIED. Field-permission lock lists must exclude server-managed/injected
    fields (updatedBy; and position, co-written with stage on kanban drag).

Version

Minor bump → 0.5.0 (new role, new fields, new behaviour; backwards-compatible).

Testing

  • Verified locally as a partner user: edits own profile; edits Opportunity stage +
    amount; Company/Person read-only; sees only matched deals; unassigning a partner
    removes the deal (and its company/people) from the partner's view.
  • yarn rls:configure passes (5 predicates upserted; 24 Opportunity fields locked,
    stage + amount editable).
  • Lint clean.

rashad added 13 commits June 10, 2026 08:29
The Partner role locked every Opportunity field except stage/amount with
canUpdateFieldValue:false. But the *.updateOne pre-query hook injects
updatedBy into every update, so the permission check rejected ALL partner
opportunity updates (stage and amount alike) with PERMISSION_DENIED.
updatedBy is overwritten with the real actor on every write, so locking it
protected nothing.

- Remove updatedBy from the Opportunity field-lock list (auto-written on
  every update).
- Remove position (co-written with stage when changing stage via kanban drag).
- Keep createdBy locked (not auto-written on update, still blocks tampering)
  and searchVector (STORED generated column, inert).
- Update OPPORTUNITY_FIELD_LOCK_SKIP in configure-partner-rls.ts to match.
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

Changes were made to package.json, but not to yarn.lock - Perhaps you need to run yarn install?

Generated by 🚫 dangerJS against f2a77f5

@rashad rashad self-assigned this Jun 10, 2026
Cascade (on-opportunity-partner-assigned):
- Paginate the People stamp/clear fully instead of a 200-row cap, so a large
  company's contacts can't be left with a stale partnerUser RLS stamp (notably an
  ex-partner keeping visibility after unassignment).
- Guard assign against clobbering a Company already owned by a different partner
  member (the single partnerUser column models one owner per company); stamp only
  the opportunity and return companyShared in that case.
- Attempt every per-person update via Promise.allSettled (not fail-fast) and throw
  on any failure so the idempotent cascade retries; source the unassign revoke
  target from the event before-image so a retry still works after the opportunity's
  own partnerUser is cleared first.
- Decide "company still in use" on partnerId (source of truth) via the before-image
  rather than the derived partnerUser stamp; fall back to the stamp if absent.

Type-safety:
- Drop all `as any` in the cascade function and its integration test; type the
  handler via DatabaseEventPayload<ObjectRecordUpdateEvent<CoreSchema.Opportunity>>
  and let the generated client / CoreSchema types flow through.

Maintainability:
- Export *_FIELD_ID constants from the custom Opportunity field files and use them
  in partner.role.ts instead of hardcoded UUIDs.
- Share a single PARTNER_ROLE_LABEL constant between partner.role.ts and
  configure-partner-rls.ts so a role-label rename can't desync the script.

Also trims verbose comments across the touched files.
@rashad rashad force-pushed the rk-partner-rls-app branch from e55bc43 to 1fde014 Compare June 10, 2026 06:40
# Conflicts:
#	packages/twenty-apps/internal/twenty-partners/package.json
#	packages/twenty-apps/internal/twenty-partners/src/views/all-partners.view.ts
#	packages/twenty-apps/internal/twenty-partners/src/views/validated-partners.view.ts
@rashad rashad marked this pull request as ready for review June 10, 2026 17:57
@rashad rashad enabled auto-merge June 10, 2026 17:57
@twenty-ci-bot-public

Copy link
Copy Markdown

🔍 Automated Pre-Review

No issues detected - This PR is ready for human review.

Summary

  • 🟡 1 other issue(s)

View details

Automated pre-review — human approval still required.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

No issues found across 24 files

Re-trigger cubic

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant