feat(partners): lock admin-managed + ownership fields on Partner role (#21471)

## 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 <their workspace member>`. 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`.
This commit is contained in:
Rashad Karanouh
2026-06-12 11:24:15 +04:00
committed by GitHub
parent e6d730cd75
commit adbd78767e
2 changed files with 53 additions and 1 deletions
@@ -1,6 +1,6 @@
{
"name": "twenty-partners",
"version": "0.5.0",
"version": "0.5.1",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -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: [
{