diff --git a/content/docs/analyzers/FormattingCop/FC0006.md b/content/docs/analyzers/FormattingCop/FC0006.md index 806af4e..5aabead 100644 --- a/content/docs/analyzers/FormattingCop/FC0006.md +++ b/content/docs/analyzers/FormattingCop/FC0006.md @@ -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. @@ -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" @@ -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