mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-05 06:41:42 +08:00
143 lines
8.5 KiB
Markdown
143 lines
8.5 KiB
Markdown
# UI Compatibility — Data Governance Policy Builder
|
|
|
|
The Data Governance Policy Builder edits a **strict subset** of the MDAPI. Anything outside that subset must be maintained via MDAPI/Tooling for the policy's lifetime — admins cannot open it in the builder without silent data loss or a hard crash.
|
|
|
|
> **Default goal**: produce UI-compatible policies. Only fall back to API-only after asking the operator and getting explicit confirmation.
|
|
|
|
---
|
|
|
|
## What the UI Can Render
|
|
|
|
**Categories** : `GOVERNANCE_POLICY_RULE_DEFINITION`, `RECORD_POLICY_RULE_DEFINITION`, `TRANSFORM_POLICY_RULE_DEFINITION`
|
|
**Categories NOT handled** : `ACCESS_POLICY_RULE_DEFINITION` (UI forces to GOVERNANCE on save), `IDENTIFIED_RECORD_POLICY_RULE_DEFINITION`
|
|
|
|
**Effects** : `Permit`, `Forbid` (UI label "Deny"), `Transform`
|
|
|
|
**Actions** : `Read`, `TupleRead` — two real choices for OLS/FLS rules.
|
|
- `Read`: evaluates conditions against the resource itself
|
|
- `TupleRead`: cascade rewriter — expands object-level condition to also cover `resource.object.*` for fields. Use when the intent is "objects matching X AND all their fields"
|
|
- Multiple `<action>` entries: only the first is kept on save
|
|
|
|
**`<ruleConsumer>`** : only `DATACLOUD` (hardcoded on save)
|
|
**`<principalScopeType>`** : only `ANY` (hardcoded on save)
|
|
**`<resourceScopeType>`** : `ANY`, `RECORD`, `FIELD`, `DATASPACE`, `SPAN`
|
|
**`<principalAuthenticationLevel>`** : `INTERNAL`, `AUTHENTICATED`, `UNIDENTIFIED`. `IDENTIFIED` not exposed.
|
|
**`<replicated>`** : read but NOT editable. Once `true`, UI forces read-only mode.
|
|
**`<transformPrecedence>`** : not exposed; non-zero values dropped.
|
|
|
|
---
|
|
|
|
## Conditions — Supported Shapes
|
|
|
|
**Top-level conjunction**: only `<whenPolicyRuleDefinitionClauseConjunction>`. The UI forces `<unlessPolicyRuleDefinitionClauseConjunction>` to `null` on save.
|
|
|
|
**Conjunction expression**: the UI builds `(AND …)` of up to four sections dispatched by path type. A pure flat `(OR 1 2)` at the top level triggers `// ERROR: Unsupported rule!` — rule silently dropped. `OR` only round-trips inside a single section's group.
|
|
|
|
**Path types per UI section:**
|
|
|
|
| UI Section | Recognized path |
|
|
|------------|-----------------|
|
|
| Users (Permission) | `principalPath = ASSIGNED_PERMISSIONS_PATH` |
|
|
| Resource entity-type | `resourcePath = ENTITYTYPE` with `IS` operator |
|
|
| Data space | `resourcePath = DATASPACE` or `contextPath = SESSION_DATASPACE` |
|
|
| Tag / Classification | `resourcePath = TAG` or `CLASSIFICATION` |
|
|
| RLS rule criteria | `resourcePath = RECORDFIELD` or `EXPRESSION` |
|
|
|
|
Any other path type → condition silently ignored when UI rebuilds the rule.
|
|
|
|
**Operators per path**:
|
|
- Tag/Classification: `CONTAINS_ANY`, `CONTAINS_NONE`. (`CONTAINS_ALL` exists in schema but not offered in dropdowns)
|
|
- Permission: `CONTAINS_ANY` (Permit) / `CONTAINS_NONE` (Forbid/Transform)
|
|
- RLS `RECORDFIELD`: `EQUALS`, `NOT_EQUALS`, `LIKE`, `LESS_THAN(_OR_EQUALS)`, `GREATER_THAN(_OR_EQUAL)`, `HIERARCHICALLY_ABOVE` (DLO/DMO only). **Not editable**: `HIERARCHICALLY_BELOW`, `IN`, `CONTAINS_ALL`
|
|
- RLS join (`EXPRESSION`): `EXISTS` only
|
|
|
|
**`<policyRuleValueSet>` reference types** : `CUSTOM_TAG`, `STANDARD_TAG`, `CUSTOM_CLASSIFICATION`, `STANDARD_CLASSIFICATION`, `CUSTOM_PERMISSION`. **Not editable**: `HIERARCHY`, `DATASPACE`.
|
|
|
|
---
|
|
|
|
## Implicit Defaults Injected by the UI on Save
|
|
|
|
When the UI saves, it auto-injects these — if your hand-authored XML omits them or uses different values, the first UI save rewrites them:
|
|
|
|
| Field | UI default |
|
|
|-------|-----------|
|
|
| `principalAuthenticationLevel` | `INTERNAL` |
|
|
| `principalScopeType` | `ANY` |
|
|
| `ruleConsumer` | `DATACLOUD` |
|
|
| `unlessPolicyRuleDefinitionClauseConjunction` | `null` (always) |
|
|
| `actions` | only first action kept |
|
|
| `policyRuleResourceDomains` | only first `<resourceDomain>` kept |
|
|
|
|
---
|
|
|
|
## The Non-Round-Trip Checklist
|
|
|
|
The policy will **not** round-trip through the UI without data loss or silent failure if **any** of the following is true:
|
|
|
|
- [ ] `category` is `ACCESS_POLICY_RULE_DEFINITION` or `IDENTIFIED_RECORD_POLICY_RULE_DEFINITION`
|
|
- [ ] `ruleConsumer` ≠ `DATACLOUD`
|
|
- [ ] More than one `<action>`, or action is not `Read` / `TupleRead`
|
|
- [ ] **No top-level `resourcePath=ENTITYTYPE` discriminator group** for ACCESS/GOVERNANCE rules — this causes a **hard crash** (`Cannot use 'in' operator to search for 'Permit' in undefined`), not a graceful degradation. A `TupleRead` rule without the OR-of-ENTITYTYPE clause still deploys and works at runtime — the crash is UI-only.
|
|
- [ ] **Bare top-level condition index** in `<conjunctionExpression>` (e.g. `3` instead of `(AND 3)`) — causes crash in `buildCriteria` (`param.params.map(...)` = `undefined.map()`)
|
|
- [ ] More than one `<resourceDomain>`
|
|
- [ ] An `<unlessPolicyRuleDefinitionClauseConjunction>` block present
|
|
- [ ] Any `<conditions>` uses `<clause>UNLESS</clause>`
|
|
- [ ] `<conjunctionExpression>` uses top-level `OR` (e.g. `(OR 1 2)`)
|
|
- [ ] `<principalPath>` other than `ASSIGNED_PERMISSIONS_PATH` (user ID etc. may appear only inside SOQL `${User.X}` substitutions)
|
|
- [ ] `<resourcePath>` other than `TAG`, `CLASSIFICATION`, `DATASPACE`, `RECORDFIELD`, `EXPRESSION`, `ENTITYTYPE`
|
|
- [ ] `<valuePrincipalPath>` is not one of `USER_ID`, `USER_ROLE_ID`, `ORGANIZATION_ID`, `SCALAR_ATTRIBUTE`, `PLURAL_ATTRIBUTE` (with `${User.X}` expression)
|
|
- [ ] `<contextPath>` other than `SESSION_DATASPACE`
|
|
- [ ] Operator is `HIERARCHICALLY_BELOW`, `IN`, or `CONTAINS_ALL`
|
|
- [ ] `<valueReferenceType>` is `HIERARCHY` or `DATASPACE`
|
|
- [ ] `<valueString>` is typed JSON other than `{"t":"Text","v":"FIELD"|"RECORD"|"OBJECT"}` or `{"t":"Boolean","v":…}` with a UI-recognized boolean field
|
|
- [ ] `<resourceTransform>` not in the masking-category mapped set
|
|
- [ ] `<resourceExpression>` ARGLIST has multiple arguments
|
|
- [ ] `<replicated>true</replicated>` in an org with replication frozen
|
|
|
|
If **none** of the above is true, the policy is **UI-compatible**.
|
|
|
|
> **Important**: The UI shows NO banner when it drops unsupported pieces on save (`// ERROR: Unsupported rule!` comment, no surfaced message). Operator-confirmation is the only line of defense.
|
|
|
|
---
|
|
|
|
## Operator-Confirmation Protocol
|
|
|
|
Whenever a request would produce a UI-incompatible policy:
|
|
|
|
1. **Try to make it UI-compatible first.** Mechanical rewrites:
|
|
- `unless` → `when` with negated operator:
|
|
| Intent | UI-compatible rewrite |
|
|
|--------|----------------------|
|
|
| `unless principal has permission X` | `WHEN ASSIGNED_PERMISSIONS_PATH CONTAINS_NONE X` |
|
|
| `unless resource has tag X` | `WHEN TAG CONTAINS_NONE X` |
|
|
| `unless record field = value` | `WHEN RECORDFIELD NOT_EQUALS value` |
|
|
- Top-level `OR` between two tags → nest inside a tag/classification section group
|
|
- Multiple `<action>` → split into multiple rules
|
|
|
|
2. **If a UI-compatible rewrite is not possible**, tell the operator:
|
|
- Which traits make it incompatible (cite items from the checklist above)
|
|
- That the Data Governance Policy Builder will silently drop unsupported pieces on save with no warning
|
|
- That maintenance must happen through MDAPI/Tooling for the policy's lifetime
|
|
|
|
3. **Wait for explicit confirmation.** Do not write the file until confirmed.
|
|
|
|
4. **On confirmation**, add an in-source marker to `<description>` (or a comment above the rule): `# API-only: not editable in Data Governance Policy Builder. Reasons: <bullets>`. Also set `<builderCompatible>false</builderCompatible>` on the parent set.
|
|
|
|
---
|
|
|
|
## UI-Compatibility Matrix for Templates
|
|
|
|
| Template | UI-compatible? | Notes |
|
|
|----------|----------------|-------|
|
|
| Bare permit, no conditions | Partial | Single `<action>Read</action>` only; `ruleConsumer=DATACLOUD`. UI maps ACCESS → Governance on load. |
|
|
| Tag / Classification / Permission gate | Yes | `CONTAINS_ANY` / `CONTAINS_NONE` only. Tag refs must resolve in org. |
|
|
| Basic RLS (RECORDFIELD = user attribute) | Yes | `USER_ID` / `ORGANIZATION_ID` / `USER_ROLE_ID` round-trips. |
|
|
| TupleRead with OR-of-ENTITYTYPE | Yes | The canonical UI-compatible OLS/FLS shape. |
|
|
| IDENTIFIED_RECORD | Not MDAPI-authorable | Must be a runtime RuleProvider. |
|
|
| RLS via PROJECTION join | Yes | Single PROJECTION editable. |
|
|
| Scalar/plural SOQL principal | Out of MDAPI contract | `SCALAR_ATTRIBUTE`/`PLURAL_ATTRIBUTE` not in `RulePrincipalPathType`. Fixture-only. |
|
|
| TRANSFORM with permission + classification | Yes | Use masking-category-mapped transforms only. |
|
|
| `(AND 1 2)` multi-condition | Yes (if each section is one of the four UI buckets) | |
|
|
| Top-level `(OR 1 2)` | No | Triggers unsupported-rule path; silently dropped. |
|
|
| Hierarchy traversal | No | `HIERARCHICALLY_BELOW` and `HIERARCHY` not in UI dropdowns. Always API-only. |
|