3 min readRishi

crossCompany in X++: The Query That Mixes Legal Entities

crossCompany in X++: The Query That Mixes Legal Entities

A select in X++ is scoped to the company the session is in. CustTable in USMF does not return USSI customers, because the kernel adds the current DataAreaId. The crossCompany keyword removes that filter. It does not remove security. Microsoft's select-statement reference is specific: the keyword returns data for all companies the user is authorized to read. A batch account that can open every legal entity will, and the report will look correct while it mixes them.

Constrain the companies in the statement

Pass a container. The query is then limited to that list, and still only to companies the user may read. The container can be a variable or an expression.

CustTable custTable;
container companies = ['USMF', 'USSI'];

while select crossCompany: companies custTable
    order by custTable.DataAreaId
{
    // custTable.DataAreaId tells you which company this row came from
}

An unconstrained while select crossCompany custTable is the version that shows up in incidents. The author tested it in a one-company dev box, where "all companies" and "this company" are the same set. In production the service account can read twenty legal entities, and a customer balance from the wrong one lands on a statement.

Anything you display or export from that loop has to carry DataAreaId. A grid of account numbers with no company column is how someone pays the USSI invoice against the USMF customer of the same number.

Reading across companies does not switch the session

crossCompany changes which rows a select can see. It does not change curext(), and it does not retarget number sequences, default dimensions, or inserts. Read a customer from USSI and then call SalesTable.insert() and the order is created in the company the session started in.

Writes that must land in another company go inside changeCompany, which switches the session for the block:

changeCompany('USSI')
{
    // curext() is USSI here, including number sequences and inserts
}

The same split exists on data entities. When PrimaryCompanyContext is set, X++ reads are filtered to the current company unless the query asks for cross-company data. OData reads are not filtered. The caller has to send dataAreaId itself. An integration that pages an entity with no company filter is a cross-company export whether or not anyone typed the keyword. That is also why a throttled OData client can look "random": it is walking every legal entity. The request limits in OData throttling assume you meant to read that much.

Query objects follow the same rule. allowCrossCompany(true) opens the query, and addCompanyRange puts the container back. A form or a report that sets the flag and never adds a range has the same bug as the unconstrained select.

What to check before it ships

The user on the batch job is the real scope. crossCompany cannot see a company that user cannot open, and it will see every company that user can. A service account granted System user in every legal entity is not a narrow integration account. Pair the keyword with an explicit company list taken from the job parameters, and keep inserts inside changeCompany for the company you mean to post to. Security roles still decide who may open a company at all; they are covered in roles, duties, and privileges. The keyword does not add a second check on top of that.

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

  • No comments yet. Be the first.