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:
@@ -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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user