From adbd78767e16f83d66c0da61b22f41cecdf54dd0 Mon Sep 17 00:00:00 2001 From: Rashad Karanouh <11599358+rashad@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:24:15 +0400 Subject: [PATCH] feat(partners): lock admin-managed + ownership fields on Partner role (#21471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Tightens the **Partner** self-service role's field-level permissions so a partner can edit its own profile but not admin/ops-controlled or ownership fields. All locks are `canUpdateFieldValue: false` on the Partner object. **Admin-managed scalar fields (7):** `slug`, `validationStage`, `reviewed`, `ranking`, `partnerTier`, `applicationNotes`, `lastMatchAt` **Ownership relation FKs (2):** `partnerUser`, `company` ## Why - The 7 scalar fields are admin/ops-controlled (validation, ranking, tiering, internal notes) — a partner must not be able to self-promote or alter ops data. - `partnerUser` is the **RLS pivot**: the row-level predicate scopes a partner to records where `partnerUser IS `. If a partner could clear or repoint it, they'd drop their own record out of scope (an orphan only admins can see). It is already locked on Opportunity; this brings Partner in line. - `company` is read-only at the object level for partners, so its FK link must not be repointable from the Partner side either. The remaining Partner relations (`opportunities`, `persons`, `partnerContents`) need no lock — they are already protected by inverse-side field locks or object-level read-only / no-access rules. ## Scope - One source file: `src/roles/partner.role.ts` (9 field-permission entries). - No schema changes — additive permission tightening; upgrades cleanly via `deploy` + `install`. - Version: patch bump `0.5.0 → 0.5.1`. --- .../internal/twenty-partners/package.json | 2 +- .../twenty-partners/src/roles/partner.role.ts | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/twenty-apps/internal/twenty-partners/package.json b/packages/twenty-apps/internal/twenty-partners/package.json index a083adfdc8..92a921714b 100644 --- a/packages/twenty-apps/internal/twenty-partners/package.json +++ b/packages/twenty-apps/internal/twenty-partners/package.json @@ -1,6 +1,6 @@ { "name": "twenty-partners", - "version": "0.5.0", + "version": "0.5.1", "license": "MIT", "engines": { "node": "^24.5.0", diff --git a/packages/twenty-apps/internal/twenty-partners/src/roles/partner.role.ts b/packages/twenty-apps/internal/twenty-partners/src/roles/partner.role.ts index 96a14267aa..73ede8c3c2 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/roles/partner.role.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/roles/partner.role.ts @@ -15,8 +15,10 @@ import { OPPORTUNITY_SUBSCRIPTION_FREQUENCY_FIELD_ID } from 'src/fields/opportun import { OPPORTUNITY_SUBSCRIPTION_TYPE_FIELD_ID } from 'src/fields/opportunity-subscription-type.field'; import { OPPORTUNITY_TFT_ID_FIELD_ID } from 'src/fields/opportunity-tft-id.field'; import { OPPORTUNITY_USE_CASE_FIELD_ID } from 'src/fields/opportunity-use-case.field'; +import { PARTNER_COMPANY_FIELD_ID } from 'src/fields/partner-company.field'; import { PARTNER_ON_OPPORTUNITY_FIELD_ID } from 'src/fields/partner-on-opportunity.field'; import { PARTNER_USER_ON_OPPORTUNITY_FIELD_ID } from 'src/fields/partner-user-on-opportunity.field'; +import { PARTNER_USER_ON_PARTNER_FIELD_ID } from 'src/fields/partner-user-on-partner.field'; // Shared with configure-partner-rls.ts, which locates the role by this label. export const PARTNER_ROLE_LABEL = 'Partner'; @@ -213,6 +215,56 @@ export default defineRole({ fieldUniversalIdentifier: OPPORTUNITY_DESIGN_DOC_STATUS_FIELD_ID, canUpdateFieldValue: false, }, + // Partner object — lock admin-managed fields so partners can't self-promote or alter ops data. + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: 'a0000002-0000-4000-8000-000000000002', + canUpdateFieldValue: false, + }, + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: '2ca9856f-f54a-4326-9ff3-668fd7da0b50', + canUpdateFieldValue: false, + }, + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: '5af4e57e-7fa7-4c4f-b40f-37549361459a', + canUpdateFieldValue: false, + }, + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: '5412e4ca-cc96-4be8-8652-b73dace7673b', + canUpdateFieldValue: false, + }, + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: 'd4fa6461-37b6-49ee-9181-dd482e74a70b', + canUpdateFieldValue: false, + }, + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: 'a0000011-0000-4000-8000-000000000011', + canUpdateFieldValue: false, + }, + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: 'a0000010-0000-4000-8000-000000000010', + canUpdateFieldValue: false, + }, + // Relation locks — the FK relations the Partner record itself owns. partnerUser is the + // RLS pivot: clearing or repointing it would drop the partner's own record out of scope + // (an orphan only admins can see). company is read-only as an object, so its link must + // not be repointable either. Mirrors the Opportunity partner/partnerUser locks above. + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: PARTNER_USER_ON_PARTNER_FIELD_ID, + canUpdateFieldValue: false, + }, + { + objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, + fieldUniversalIdentifier: PARTNER_COMPANY_FIELD_ID, + canUpdateFieldValue: false, + }, ], objectPermissions: [ {