[W1][MultiObjects] Allow a multi-value responsibility center security filter
Nobody has claimed this yet.
- Dominant language
- AL
- Stars
- 683
- Forks
- 459
- Avg merge
- 3d 26m
- Merged PRs (30d)
- 633
Description
### Why do you need this change?
Responsibility-center security in the Base Application is capped at exactly one center per user, and that cap cannot be lifted by an extension without re-implementing the feature on every table that uses it.
GetSalesFilter(), GetPurchasesFilter() and GetServiceFilter() return Code[10], and every consumer applies the value with SetRange. Verbatim from Purch. Inv. Header, lines 869-883:
procedure SetSecurityFilterOnRespCenter()
var
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeSetSecurityFilterOnRespCenter(Rec, IsHandled);
if IsHandled then
exit;
if UserSetupMgt.GetPurchasesFilter() <> '' then begin
FilterGroup(2);
SetRange("Responsibility Center", UserSetupMgt.GetPurchasesFilter());
FilterGroup(0);
end;
end;
Because the return type is Code[10] and the call is SetRange, there is no supported way to express "this user may see centers A, B and C".
Every existing extension point in codeunit 5700 is also typed Code[10], so none of them can carry a multi-value filter (line numbers against commit 2f3b868):
| Event | Signature | Why it does not help |
|---|---|---|
OnAfterGetPurchFilter |
(var UserSetup: Record "User Setup"; var UserRespCenter: Code[10]; var UserLocation: Code[10]) |
Writable, but one value of at most 10 characters |
OnAfterGetSalesFilter |
same shape | Same cap |
OnAfterGetServiceFilter |
same shape | Same cap |
OnBeforeGetSalesFilter |
(UserCode: Code[50]; var UserLocation: Code[10]; var UserRespCenter: Code[10]; var IsHandled: Boolean) |
Full override with IsHandled, still Code[10], and no purchase or service equivalent exists |
OnAfterGetSalesFilterProcedure |
(UserCode: Code[50]; Result: Code[10]) |
Result is not var - informational only |
OnAfterGetPurchasesFilter |
(UserCode: Code[50]; Result: Code[10]) |
Same - not var |
And even if a longer value could be passed through, the consumers apply it with SetRange, which matches A|B as one literal code rather than as a filter expression.
Users responsible for several sites are a common requirement. The BC Idea Users with multiple Purchase/Sales responsibility centres has been open since 2019. Today the only options are "exactly one center" or "all centers".
Why the existing per-table OnBeforeSetSecurityFilterOnRespCenter events are not a sufficient answer
I expect the first reaction to be "that event already exists, subscribe to it". Here is why that does not solve the problem, with exact numbers from an exhaustive scan of commit 2f3b868 (rg over src/Layers, full lists in section 3 below):
- 30 tables in the W1 Base Application implement
SetSecurityFilterOnRespCenter, and 29 of them publishOnBeforeSetSecurityFilterOnRespCenter. An extension therefore needs 30 subscribers, each settingIsHandled := trueand repeating the sameFilterGroup(2)/SetFilter/FilterGroup(0)body, to express one security rule. - 188 table implementations and 187 events exist once the 16 localization layers are included (APAC, BE, CH, CZ, DACH, ES, FI, FR, GB, IT, NA, NL, NO, RU, SE). A partner shipping to several countries multiplies the subscriber count accordingly.
- The list is not stable. Every table Microsoft adds later is silently unfiltered until the extension adds another subscriber. For a security feature that is a fail-open outcome, and it fails silently.
- One table cannot be overridden at all.
Service Header Archive(6010) declares the method asinternal procedureand publishes no event, so an extension can neither call it nor override it. That gap is self-contained and does not depend on the change requested here, so it has been split out into #11450. It is listed here only as one more reason why per-table subscribers cannot express the rule completely.
By contrast, 73 W1 pages already call SetSecurityFilterOnRespCenter (full list below). They are the reason a fix at the codeunit + consumer level is so much cheaper than per-table subscribers: all 73 pages, plus any report or API that calls the method, inherit the corrected behaviour with no extension code at all.
Concrete cost of the workaround
In a production multi-center implementation, expressing this one security rule currently takes 9 OnBeforeSetSecurityFilterOnRespCenter overrides on standard document tables, plus 13 OnOpenPageEvent subscribers on master-data and ledger pages (Vendor List, Customer List, General Ledger Entries, G/L Budget Entries, Item List and similar) whose tables carry no responsibility-center security in W1 at all.
With the change requested below, the subscribers that exist solely to widen the responsibility-center filter collapse to 2 - one OnAfterGetSalesFilterText and one OnAfterGetPurchasesFilterText - and every Base Application caller becomes correct automatically.
Repro
Platform/application 27.0, CRONUS demo data, W1.
Goal: user BOB may see two responsibility centers but not a third.
- Set up the data. W1 demo data creates exactly two responsibility centers,
BIRMINGHAMandLONDON(CreateResponsibilityCenter, lines 7-8). Create a third one,YORK, so that a genuine subset exists, and create one purchase invoice in each of the three. - Open User Setup for BOB.
- Attempt A - enter both centers in the standard field. Type
BIRMINGHAM|LONDONinto Purchase Resp. Ctr. Filter.
Expected: BOB sees Birmingham and London only.
Actual: impossible. The field isfield(5701; "Purchase Resp. Ctr. Filter"; Code[10])withTableRelation = "Responsibility Center", so the 17-character value neither fits the length nor passes the table relation. Despite the caption saying "Filter", the field can only hold a single existing center code. - Attempt B - leave the field blank.
Actual: BOB sees all three centers.GetPurchasesFilter, lines 86-100 falls back toCompanyInfo."Responsibility Center", which W1 demo data leaves empty, so the function returns''andSetSecurityFilterOnRespCenterapplies no filter at all. There is no middle ground between "one center" and "all centers". (If Company Information does carry a center, the user is instead pinned to that single one - still never a subset.) - Attempt C - widen the value from an extension. Subscribe to
OnAfterGetPurchFilter, or on the sales side toOnBeforeGetSalesFilterwhich even offersIsHandled, and try to return both centers:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"User Setup Management", 'OnAfterGetPurchFilter', '', false, false)]
local procedure OnAfterGetPurchFilter(var UserSetup: Record "User Setup"; var UserRespCenter: Code[10]; var UserLocation: Code[10])
begin
UserRespCenter := 'BIRMINGHAM|LONDON'; // 17 characters into a Code[10]
end;
Actual: the value cannot survive the parameter type. 'BIRMINGHAM|LONDON' is 17 characters and the parameter is Code[10], so the over-long literal is rejected at build time, and the usual workaround CopyStr(..., 1, MaxStrLen(UserRespCenter)) simply discards everything from the | onwards, leaving BIRMINGHAM. Independently of the length question, the consumer applies the result with SetRange, so even a hypothetically longer value would be matched as one literal code and return no records.
6. The only thing that works today is to subscribe to OnBeforeSetSecurityFilterOnRespCenter on each of the 30 W1 tables (and the localization copies), set IsHandled := true, and repeat the FilterGroup(2) / SetFilter / FilterGroup(0) body in every subscriber. Even then, Service Header Archive cannot be covered, because it is internal and publishes no event.
Step 6 is the situation this request asks you to remove.
Describe the request
An additive change in codeunit 5700 "User Setup Management", plus switching its Base Application consumers from SetRange to SetFilter. No existing signature is changed or removed, so nothing breaks.
The parts are dependent on each other, which is why they are filed as one request per the template guidance: without the consumer change the new events have no observable effect, and without the new events the consumer change is pointless.
1. New text-returning procedures, defaulting to today's behaviour
procedure GetSalesFilterText(): Text
procedure GetSalesFilterText(UserCode: Code[50]): Text
procedure GetPurchasesFilterText(): Text
procedure GetPurchasesFilterText(UserCode: Code[50]): Text
procedure GetServiceFilterText(): Text
procedure GetServiceFilterText(UserCode: Code[50]): Text
The default implementation returns the existing single value, so with no subscriber the resolved filter is identical to today:
procedure GetPurchasesFilterText(UserCode: Code[50]) RespCenterFilter: Text
var
UserSetup: Record "User Setup";
begin
RespCenterFilter := GetPurchasesFilter(UserCode);
if UserSetup.Get(UserCode) then;
OnAfterGetPurchasesFilterText(UserSetup, RespCenterFilter);
end;
2. New OnAfter events
These are "before or after a procedure" events, not IsHandled overrides, per types of events for extensibility. The ...Text suffix avoids a collision with the existing OnAfterGetPurchasesFilter(UserCode; Result: Code[10]). The User Setup record is passed rather than a single short field, per the "pass records rather than a specific field length" guideline.
EventRequest
[W1][Codeunit][5700][User Setup Management]
[GetPurchasesFilterText]
___
Raised after the default single-center filter is assigned, so an extension can replace it with a multi-value filter expression such as BIRMINGHAM|LONDON.
___
[IntegrationEvent(false, false)]
local procedure OnAfterGetPurchasesFilterText(var UserSetup: Record "User Setup"; var RespCenterFilter: Text)
begin
end;
The same pair for sales (OnAfterGetSalesFilterText) and service (OnAfterGetServiceFilterText).
3. Change the consumers to use the text variant with SetFilter
procedure SetSecurityFilterOnRespCenter()
var
RespCenterFilter: Text;
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeSetSecurityFilterOnRespCenter(Rec, IsHandled);
if IsHandled then
exit;
RespCenterFilter := UserSetupMgt.GetPurchasesFilterText();
if RespCenterFilter <> '' then begin
FilterGroup(2);
SetFilter("Responsibility Center", RespCenterFilter);
FilterGroup(0);
end;
end;
The exhaustive list of objects follows. It was produced by scanning commit 2f3b868 for SetSecurityFilterOnRespCenter across src/Layers, so it should match your own tooling exactly.
A. W1 Base Application tables implementing SetSecurityFilterOnRespCenter (30)
| Table ID | Table name | OnBeforeSetSecurityFilterOnRespCenter |
|---|---|---|
| 36 | Sales Header |
yes |
| 38 | Purchase Header |
yes |
| 110 | Sales Shipment Header |
yes |
| 111 | Sales Shipment Line |
yes |
| 112 | Sales Invoice Header |
yes |
| 113 | Sales Invoice Line |
yes |
| 114 | Sales Cr.Memo Header |
yes |
| 115 | Sales Cr.Memo Line |
yes |
| 120 | Purch. Rcpt. Header |
yes |
| 121 | Purch. Rcpt. Line |
yes |
| 122 | Purch. Inv. Header |
yes |
| 123 | Purch. Inv. Line |
yes |
| 124 | Purch. Cr. Memo Hdr. |
yes |
| 125 | Purch. Cr. Memo Line |
yes |
| 5107 | Sales Header Archive |
yes |
| 5109 | Purchase Header Archive |
yes |
| 5900 | Service Header |
yes |
| 5901 | Service Item Line |
yes |
| 5965 | Service Contract Header |
yes |
| 5990 | Service Shipment Header |
yes |
| 5991 | Service Shipment Line |
yes |
| 5992 | Service Invoice Header |
yes |
| 5993 | Service Invoice Line |
yes |
| 5994 | Service Cr.Memo Header |
yes |
| 5995 | Service Cr.Memo Line |
yes |
| 6010 | Service Header Archive |
no event |
| 6650 | Return Shipment Header |
yes |
| 6651 | Return Shipment Line |
yes |
| 6660 | Return Receipt Header |
yes |
| 6661 | Return Receipt Line |
yes |
B. W1 Base Application pages that call SetSecurityFilterOnRespCenter (73)
These need no change and inherit the fix automatically:
41 Sales Quote, 42 Sales Order, 43 Sales Invoice, 44 Sales Credit Memo, 49 Purchase Quote, 50 Purchase Order, 51 Purchase Invoice, 52 Purchase Credit Memo, 130 Posted Sales Shipment, 132 Posted Sales Invoice, 134 Posted Sales Credit Memo, 136 Posted Purchase Receipt, 138 Posted Purchase Invoice, 140 Posted Purchase Credit Memo, 142 Posted Sales Shipments, 143 Posted Sales Invoices, 144 Posted Sales Credit Memos, 145 Posted Purchase Receipts, 146 Posted Purchase Invoices, 147 Posted Purchase Credit Memos, 507 Blanket Sales Order, 509 Blanket Purchase Order, 525 Posted Sales Shipment Lines, 526 Posted Sales Invoice Lines, 527 Posted Sales Credit Memo Lines, 528 Posted Purchase Receipt Lines, 529 Posted Purchase Invoice Lines, 530 Posted Purchase Cr. Memo Lines, 5900 Service Order, 5915 Service Tasks, 5933 Service Invoice, 5935 Service Credit Memo, 5951 Posted Service Invoice Lines, 5952 Posted Service Cr. Memo Lines, 5964 Service Quote, 5970 Posted Service Shipment Lines, 5971 Posted Service Credit Memos, 5972 Posted Service Credit Memo, 5974 Posted Service Shipments, 5975 Posted Service Shipment, 5977 Posted Service Invoices, 5978 Posted Service Invoice, 6000 Dispatch Board, 6050 Service Contract, 6053 Service Contract Quote, 6630 Sales Return Order, 6640 Purchase Return Order, 6650 Posted Return Shipment, 6652 Posted Return Shipments, 6653 Posted Return Shipment Lines, 6660 Posted Return Receipt, 6662 Posted Return Receipts, 6663 Posted Return Receipt Lines, 9300 Sales Quotes, 9301 Sales Invoice List, 9302 Sales Credit Memos, 9303 Blanket Sales Orders, 9304 Sales Return Order List, 9305 Sales Order List, 9306 Purchase Quotes, 9307 Purchase Order List, 9308 Purchase Invoices, 9309 Purchase Credit Memos, 9310 Blanket Purchase Orders, 9311 Purchase Return Order List, 9317 Service Quotes, 9318 Service Orders, 9319 Service Invoices, 9320 Service Credit Memos, 9321 Service Contracts, 9322 Service Contract Quotes, 9347 Purchase Order Archives, 9349 Sales Order Archives
C. Localization layer copies of the same method (158)
| Layer | Count | Tables (IDs) |
|---|---|---|
| APAC | 15 | 36, 38, 112, 113, 114, 115, 120, 122, 123, 124, 125, 5107, 5109, 6650, 6660 |
| BE | 16 | 36, 38, 110, 111, 112, 114, 120, 121, 122, 123, 124, 125, 5107, 5109, 6650, 6660 |
| CH | 7 | 36, 38, 111, 113, 115, 5107, 5109 |
| CZ | 1 | 36 |
| DACH | 4 | 38, 122, 124, 5109 |
| ES | 13 | 36, 38, 110, 111, 112, 113, 114, 115, 120, 122, 123, 124, 125 |
| FI | 10 | 36, 38, 111, 112, 113, 115, 121, 122, 123, 125 |
| FR | 3 | 36, 112, 114 |
| GB | 6 | 36, 38, 113, 115, 123, 125 |
| IT | 20 | 36, 38, 110, 111, 112, 113, 114, 115, 120, 121, 122, 123, 124, 125, 5107, 5109, 5990, 6650, 6660, 6661 |
| NA | 16 | 36, 38, 110, 111, 112, 113, 114, 115, 120, 122, 123, 124, 125, 5107, 5109, 6661 |
| NL | 6 | 36, 38, 112, 114, 122, 124 |
| NO | 11 | 36, 38, 111, 112, 113, 114, 115, 122, 123, 125, 5107 |
| RU | 18 | 36, 38, 110, 111, 112, 113, 114, 115, 120, 121, 122, 123, 124, 125, 5107, 5109, 6650, 6660 |
| SE | 12 | 36, 38, 111, 112, 113, 114, 115, 121, 122, 123, 124, 125 |
Totals across all 17 layers that carry the method: 188 table implementations, 187 events.
Compatibility
No signature is changed or removed. GetSalesFilter(), GetPurchasesFilter() and GetServiceFilter() keep returning Code[10], and the six existing events keep their current parameters. With no subscriber to the new events, SetFilter receives a single center code, which is an equivalent filter expression to SetRange with that code, so behaviour for existing installations is unchanged.
One deliberate behavioural note: SetFilter with a value containing filter characters would be interpreted as an expression rather than a literal. That only becomes reachable when a subscriber deliberately returns such an expression, which is the point of the request. Responsibility center codes themselves cannot contain | or . because they are validated against the Responsibility Center table.
Performance
No additional database access. The new events fire on a lookup that has already been executed and cached in the codeunit's HasGotSalesUserSetup / HasGotPurchUserSetup / HasGotServUserSetup pattern, so the event fires once per session per document type, not per record.
Data sensitivity
No new data is exposed. The parameters are the User Setup record and a responsibility-center filter string, both already available to subscribers of the existing OnAfterGetSalesFilter and OnAfterGetPurchFilter.
Multi-extension interaction
var RespCenterFilter: Text lets a later subscriber read and refine what an earlier one produced, rather than an all-or-nothing IsHandled switch. No IsHandled is requested on the new events, because the default value is always a valid input for any subscriber.
Versioning expectation
The new procedures and events are additive and could ship in main or the latest release. The consumer change from SetRange to SetFilter, and the localization-layer copies, would normally target the next major version. Splitting it that way is fine by us as long as the two land in the same release, since neither is useful alone.
Related requests
- #11450 - make
SetSecurityFilterOnRespCenterpublic on table 6010Service Header Archiveand add its missingOnBeforeSetSecurityFilterOnRespCenter. That item was originally part of this request and has been split out, because it is a self-contained parity fix that stands on its own. If both are accepted, the body of that method should use the text variant andSetFilterlike every other implementation. - #11449 - add
SetSecurityFilterOnRespCentertoSales LineandPurchase Line, which carry"Responsibility Center"today but do not implement the method.
Note for triage: the automated assessment on this issue reported Partial triage - enrichment phase failed with a default NEEDS WORK verdict rather than a content-based one. This revision splits the table 6010 parity item out into #11450 so that this request covers one change, and trims the body accordingly.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in UserSetupManagement.Codeunit.al, reviewing the existing GetSalesFilter, GetPurchasesFilter, and GetServiceFilter procedures and their proposed text-returning counterparts. Then inspect SetSecurityFilterOnRespCenter call sites such as PurchInvHeader.Table.al, along with UserSetup.Table.al. Done means the new events can provide multi-value filters and Base Application consumers apply them with SetFilter while existing signatures remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Domain
- backend, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 64/100