Moving an Asset Between Companies Developer Guide - edmozley/freeitsm GitHub Wiki
Moving an asset between companies - developer guide
User-facing page: Moving an asset between companies. The asset twin of moving a task between companies; read that first, because the access rule is the same and so is the reason for it.
The move: AssetsService::moveToCompany()
Endpoint api/assets/move_to_company.php is a thin adapter; every rule is in the service.
Access, both ways, exactly as the task move: $ctx->companyScope (what this caller may touch) and analystCanAccessTenant() (what the person may reach). For a session they agree; for an API key issued for one company they do not. "No access" and "no such company" give one message, so probing ids cannot reveal which companies exist.
An asset carries more company-owned things than a task. They split into two kinds on purpose:
| What | Why | |
|---|---|---|
| Cleared, audited, named in the toast | location_id unless visible to the target (its own or shared); asset_type_id / asset_status_id unless in getTenantConfigRows() for the target |
They do not exist in the new company. Keeping them would leave the asset wearing a value its own company cannot see or pick. |
| Refused, naming the problem | a hostname or asset_tag the target already uses; a person holder (users_assets.user_id) whose company differs |
Both identifiers are unique per company, and the inventory agent falls back to the hostname when a machine has no usable serial (reconciliation). An Intune link survives the move (3.1.0). A holder is a decision for a person, not a guess. |
| Moved with it, in the same transaction | its asset_inventory_sources links (the agent's and every REST tool's): tenant_id set to the target (3.15.0) |
That column keeps a tool's device to one asset per company. Left on the old company, a later report there would name this asset - now another company's - in its "already linked" refusal, and the new company could link the same device to a second asset. A TRAP: comment marks it; tests/asset-move-inventory-links.php fails without it |
| Refused, naming the other asset | a link whose (source, source_device_ref) the target already has on a different asset |
One device, one asset per company - the same rule the REST API keeps (device_ref_conflict). Unlinking is the deliberate step; a person decides which. A 1062 on the update (a link made a moment ago) is refused the same way |
The inventory rows themselves (software_inventory_detail, asset_disks, ...) carry no company, so they simply stay on the asset. inventoryLinks() returns nothing before Database Verification has created the table, so an older install moves exactly as before.
Analyst holders move with it: analysts are not company-scoped. The Default company is stored as NULL, and resolved to its real id before comparing, so a Default asset "moved" into Default is a no-op, not a nonsense audit line.
One transaction; one company audit row plus one per cleared field (asset_history, keys shared with the history view's labels).
Company on create
create_asset.php takes an optional tenant_id, checked the same two ways. It also checks the type, status and location belong to that company. The form loads them for the chosen company, so this only fires on a stale form or a crafted request, but without it an asset could be created in one company wearing another's private location.
The form's lists reload through a new ?for_tenant= parameter on get_asset_types.php, get_asset_status_types.php and get_asset_locations.php, resolved by requestedTenantId() in includes/tenancy.php. 🔴 An id the analyst cannot reach is ignored, not honoured: the worst a crafted request gets is the active company's lists. The settings view (?manage=1) never uses it.
The response carries in_active and company_name, so the form opens the new asset when it is in the company on screen, and otherwise says where it went.
⚠️ The asset list is NOT widened for All companies
Tickets → Users and the Tasks board now widen under All companies (see All companies view - Developer Guide). The asset list deliberately does not. Its type, status and location dropdowns are single-company, so a widened list would let you open another company's asset and give it a location from the wrong one. Tickets solved the same problem with per-company lookup maps (getTenantConfigRowsByCompany()); assets would need the same before the list can widen.
Verified
On a real database with throwaway rows (all removed): created into another company from the All companies view; refused a Default-private location for a Gmail asset; moved an asset and saw its private location cleared and audited; moved one on a shared location and saw it kept; refused a hostname clash and an out-of-company holder, naming the person; and a same-company move was a no-op. 3.15.0: tests/asset-move-inventory-links.php (6 checks, through the real endpoint) - both links follow a move and back, the old company can reuse the device number, and a clash is refused with nothing changed. Run against the code before the fix it fails 4 of 6. Then the page was driven headless: the form would not submit without a company, the location list followed the company chosen, and Key info showed the Company field.