microsoft / microsoft/BCApps

[W1][MultiObjects] Allow a multi-value responsibility center security filter

Open
#11,448 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Approved Team: SCM
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 publish OnBeforeSetSecurityFilterOnRespCenter. An extension therefore needs 30 subscribers, each setting IsHandled := true and repeating the same FilterGroup(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 as internal procedure and 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.

  1. Set up the data. W1 demo data creates exactly two responsibility centers, BIRMINGHAM and LONDON (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.
  2. Open User Setup for BOB.
  3. Attempt A - enter both centers in the standard field. Type BIRMINGHAM|LONDON into Purchase Resp. Ctr. Filter.
    Expected: BOB sees Birmingham and London only.
    Actual: impossible. The field is field(5701; "Purchase Resp. Ctr. Filter"; Code[10]) with TableRelation = "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.
  4. Attempt B - leave the field blank.
    Actual: BOB sees all three centers. GetPurchasesFilter, lines 86-100 falls back to CompanyInfo."Responsibility Center", which W1 demo data leaves empty, so the function returns '' and SetSecurityFilterOnRespCenter applies 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.)
  5. Attempt C - widen the value from an extension. Subscribe to OnAfterGetPurchFilter, or on the sales side to OnBeforeGetSalesFilter which even offers IsHandled, 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 SetSecurityFilterOnRespCenter public on table 6010 Service Header Archive and add its missing OnBeforeSetSecurityFilterOnRespCenter. 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 and SetFilter like every other implementation.
  • #11449 - add SetSecurityFilterOnRespCenter to Sales Line and Purchase 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.