Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 69 additions & 22 deletions content/docs/analyzers/FormattingCop/FC0006.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,91 @@ linkTitle = 'FC0006'
ignoreObsolete = true
+++

In the `Permissions` property on codeunits, tables, pages, reports, request pages, xmlports, and queries, the casing of permission values has no runtime effect. `tabledata "Sales Line" = RIMD` and `tabledata "Sales Line" = rimd` behave identically: when the object is in the call stack, the property supplies the object side of the indirect-permission handshake, and the runtime does not distinguish `M` from `m` in that check.
In the `Permissions` property on codeunits, tables, pages, reports, request pages, xmlports, and queries, the casing of a permission letter has no runtime effect. The property does one job: when the object is on the call stack, it supplies the object side of the indirect-permission handshake. Before the object is allowed to read or write a table, the runtime checks two things:

The documented intent is different, which is exactly why uppercase misleads:
1. The user has at least **indirect** permission on the table, from a permission set or from the license.
2. The object declares that operation in its `Permissions` property.

> Specifies direct (R) or indirect (r) read permission.
Neither check looks at whether the letter is `M` or `m`. A user without any permission on the table gets the same runtime error with `RIMD` as with `rimd`. A user with indirect permission succeeds with either. A user with direct permission never needed the property in the first place.

Uppercase invites a wrong conclusion. `tabledata "G/L Entry" = RIMD` on a codeunit reads as "this codeunit grants its callers direct write access to G/L entries". It does not. Direct permission comes from a permission set or the license, and on license-restricted tables such as posted documents and ledger entries the license still wins:

— [Permissions Property](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-permissions-property) on Microsoft Learn
> The `i` means indirect insert. Capital `I` would mean direct — but on license-restricted tables, direct permissions in the `Permissions` property don't actually elevate anything.

That direct/indirect distinction is real in `permissionset` objects and in the `InherentPermissions` property, but it collapses in the object-level `Permissions` property. A user without any permission on the table is blocked whether the value is `M` or `m`; a user with indirect permission succeeds with either. Uppercase cannot elevate access on license-restricted tables such as posted documents or ledger entries. Reading `RIMD` on a codeunit and concluding that its callers get direct write access to the table is a wrong conclusion that uppercase invites.
— [Indirect Permissions in Business Central](https://stefanmaron.com/posts/indirect-permissions-bc-stream/) by Stefan Maroń

Use lowercase values so the declaration reads as what it is: an indirect-permission grant. The rule applies to every permission value in the property, including the execute permission (`x`).
Use lowercase values so the declaration reads as what it is: an indirect-permission grant. The rule applies to every permission letter in the property, including the execute permission (`x`) on codeunit, page, and report entries.

### Example

{{< highlight al "hl_lines=3" >}}
{{< highlight al "hl_lines=3-4" >}}
codeunit 50100 "Ledger Entry Mgt."
{
Permissions = tabledata "G/L Entry" = RIMD; // Permission values should be lowercase [FC0006]
Permissions = tabledata "G/L Entry" = RIMD, // Permission values should be lowercase [FC0006]
codeunit "Gen. Jnl.-Post Line" = X;
}
{{< /highlight >}}

Use lowercase permission values instead:

{{< highlight al "hl_lines=3" >}}
{{< highlight al "hl_lines=3-4" >}}
codeunit 50100 "Ledger Entry Mgt."
{
Permissions = tabledata "G/L Entry" = rimd;
Permissions = tabledata "G/L Entry" = rimd,
codeunit "Gen. Jnl.-Post Line" = x;
}
{{< /highlight >}}

### What the Learn page says, and what the runtime does

The Permissions Property page on Microsoft Learn opens with a values table that assigns a meaning to the casing:

> Specifies direct (R) or indirect (r) read permission.

— [Permissions Property, property values](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-permissions-property#property-values) on Microsoft Learn

That description is the source of most of the confusion around this property. The same page contradicts it twice further down. Its runtime results table has no direct-versus-indirect column on the property side, only *set* or *not set*:

| Permissions granted by permission set | Permission property not set | Permission property set |
|---|---|---|
| None | Runtime error caused by missing permissions. | Runtime error caused by missing permissions. |
| Indirect permission | Runtime error caused by missing permissions. | Success |
| Direct permission | Success | Success |

— [Permissions Property](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-permissions-property#example---indirect-permission) on Microsoft Learn

And the page's own worked example declares `TableData "Cust. Ledger Entry" = rm` in lowercase. The runtime results table describes the actual behavior; the values table at the top does not.

The base application is no better guide. Codeunit 80 "Sales-Post" declares everything in lowercase, while codeunit 408 "Dimension Management" mixes uppercase and lowercase across its entries. The mix is left over from the C/SIDE-to-AL conversion and years of edits by different teams. It does not encode a difference in behavior.

### Permissions on pages

The property works the same way on a page as on a codeunit: it lets a user with indirect permission on a table reach that table through the page.

> A user who has Indirect Read permission cannot open a page that displays data from a table unless the page has been given permission to read data from the table for the user.

— [Permissions Property](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-permissions-property#remarks) on Microsoft Learn

Declaring `Permissions = tabledata "G/L Entry" = rimd` on a page therefore hands every user who can open the page, and who holds at least indirect permission on G/L entries, a user-interface path to insert, modify, and delete those entries. That is a consequence of declaring the property on a page at all. Writing `RIMD` instead of `rimd` neither widens nor narrows it. When a page should only expose data the user can already access directly, leave the property off the page; use `AccessByPermission` when the goal is to hide a field or action from users who lack a permission.

### Verifying the behavior

A Docker container running with a developer license does not enforce license-level indirect restrictions the way a cloud sandbox does. Code that passes locally can fail at a customer whose license only grants indirect access to posted documents and ledger entries. Verify permission scenarios in a cloud sandbox, or in an AL test codeunit with `TestPermissions = Restrictive`.

When a local test appears to show that one casing works and the other fails, something else changed between the two runs: the permission sets assigned to the test user, the license, or the object that was actually on the call stack. The runtime does not distinguish `M` from `m` in the `Permissions` property.

### When the diagnostic is reported

- The `Permissions` property is declared on a `codeunit`, `table`, `page`, `report`, `requestpage`, `xmlport`, or `query` object. Extension objects do not support the property.
- Any entry in the property contains at least one uppercase letter. A single `RIMD` among lowercase entries, or a mixed value such as `Rimd`, is enough.
- One diagnostic is reported per `Permissions` property, not per entry.

### Code fix

The **ALCops: Convert permission values to lowercase** code fix converts every permission value in the `Permissions` property to lowercase, preserving entry order and formatting.

The [AC0031](../../applicationcop/ac0031/) code fix, which adds missing `tabledata` entries to the property, already emits lowercase values. Tools that generate the property in uppercase, such as the *Add permissions to all tables used by this object* command in AZ AL Dev Tools, produce a `Permissions` property that this rule flags; running the code fix once normalizes it.

### Exception

The rule does not apply to `permissionset` and `permissionsetextension` objects. There the casing is semantic: uppercase grants direct permission, lowercase grants indirect permission, and changing one to the other changes what users can do.
Expand All @@ -54,12 +109,9 @@ permissionset 50100 "Sales Documents"
}
{{< /highlight >}}

The `InherentPermissions` property is also excluded for the same reason.
The `InherentPermissions` property and the `[InherentPermissions]` attribute are also excluded for the same reason: there uppercase and lowercase declare direct and indirect permission respectively.

The `AccessByPermission` property is not covered either. Although it uses the same syntax as
`Permissions`, it is a UI-visibility mask: it hides a table field, page field, page part, action
or object from users who lack the listed permission. It grants nothing, so it has no direct/indirect
distinction, and Microsoft Learn documents the uppercase form as its canonical spelling.
The `AccessByPermission` property is not covered either. Although it uses the same syntax as `Permissions`, it is a UI-visibility mask: it hides a table field, page field, page part, action, or object from users who lack the listed permission. It grants nothing, so it has no direct/indirect distinction, and Microsoft Learn documents the uppercase form as its canonical spelling.

{{< highlight al "hl_lines=9" >}}
page 50100 "Sales Order Extras"
Expand All @@ -77,12 +129,7 @@ page 50100 "Sales Order Extras"
}
{{< /highlight >}}

### Code fix

The **ALCops: Convert permission values to lowercase** code fix converts every permission value in the `Permissions` property to lowercase, preserving entry order and formatting.

### See also

- [Permissions Property — Microsoft Learn](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-permissions-property)
- [Indirect Permissions in Business Central — What They Are and Why They Matter — Stefan Maroń](https://stefanmaron.com/posts/indirect-permissions-bc-stream/)
- [What's the story with indirect permissions in Business Central — Erik Hougaard](https://www.hougaard.com/whats-the-story-with-indirect-permissions-in-business-central/)
- [What's the story with indirect permissions in Business Central](https://www.hougaard.com/whats-the-story-with-indirect-permissions-in-business-central/) by Erik Hougaard
- [The `Permissions` property on codeunit objects should use lowercase `r` `i` `m` `d` only](https://github.com/ALCops/Analyzers/discussions/383), the discussion that led to this rule, with a link to the original Viva Engage thread
Loading