@@ -187,16 +187,23 @@ public function transformAsset(Asset $asset)
187187 $ array ['components ' ] = [];
188188
189189 foreach ($ asset ->components as $ component ) {
190- $ array ['components ' ][] = [
190+ // Info-disclosure guard: if the caller is denied view
191+ // on this specific component, omit it from the response
192+ // entirely - not even id / pivot_id are exposed, so a
193+ // caller with an explicit components.view deny can't
194+ // enumerate what's on the asset.
195+ if (Gate::denies ('view ' , $ component )) {
196+ continue ;
197+ }
191198
199+ $ array ['components ' ][] = [
192200 'id ' => $ component ->id ,
193201 'pivot_id ' => $ component ->pivot ->id ,
194202 'name ' => e ($ component ->name ),
195203 'qty ' => $ component ->pivot ->assigned_qty ,
196204 'purchase_cost ' => $ component ->purchase_cost ,
197205 'purchase_total ' => $ component ->calculated_purchase_cost ,
198206 'checkout_date ' => Helper::getFormattedDateObject ($ component ->pivot ->created_at , 'datetime ' ),
199-
200207 ];
201208 }
202209 }
@@ -230,7 +237,7 @@ public function transformAssignedTo($asset)
230237 // stripped is PII (username, email, employee_num, jobtitle,
231238 // first/last name split). Instance check so FMCS scoping
232239 // applies too.
233- if (! Gate::allows ('view ' , $ asset ->assigned )) {
240+ if (Gate::denies ('view ' , $ asset ->assigned )) {
234241 return [
235242 'id ' => (int ) $ asset ->assigned ->id ,
236243 'type ' => 'user ' ,
@@ -430,44 +437,49 @@ public function transformLicenseCheckedToAsset(LicenseSeat $licenseseat)
430437 public function transformCheckedoutComponents (Collection $ components_assets , $ total )
431438 {
432439 $ array = [];
440+ $ suppressed = 0 ;
433441 foreach ($ components_assets as $ component_checkout ) {
434442 $ component = $ component_checkout ->component ;
435443
436444 // Info-disclosure guard: GET /api/v1/hardware/{asset}/assigned/components
437445 // is gated only on assets.view, so a caller with assets.view but
438- // not components.view used to read the component's name / qty /
439- // note straight off this response. Fall back to `{id, type}`
440- // when the caller can't view the specific component. Instance
441- // check so FMCS scoping applies too.
442- $ canViewComponent = $ component && Gate::allows ('view ' , $ component );
446+ // an explicit deny on components.view used to read the component's
447+ // name / qty / note straight off this response. When denied, skip
448+ // the row entirely so nothing about the component (not even id
449+ // or existence) is exposed. Instance check so FMCS scoping
450+ // applies too.
451+ //
452+ // The controller-supplied $total still reflects the true row
453+ // count and would leak "there are N components you can't see",
454+ // so decrement it by the number of rows suppressed here.
455+ if (! $ component || Gate::denies ('view ' , $ component )) {
456+ $ suppressed ++;
457+
458+ continue ;
459+ }
443460
444461 $ array [] = [
445462 'assigned_pivot_id ' => $ component_checkout ->id ,
446- 'name ' => $ canViewComponent
447- ? [
448- 'id ' => $ component ->id ,
449- 'name ' => e ($ component ->display_name ),
450- 'type ' => 'component ' ,
451- 'deleted_at ' => $ component ->deleted_at ,
452- ]
453- : [
454- 'id ' => $ component ?->id,
455- 'type ' => 'component ' ,
456- ],
457- 'assigned_qty ' => $ canViewComponent ? $ component_checkout ->assigned_qty : null ,
458- 'note ' => $ canViewComponent && $ component_checkout ->note ? e ($ component_checkout ->note ) : null ,
463+ 'name ' => [
464+ 'id ' => $ component ->id ,
465+ 'name ' => e ($ component ->display_name ),
466+ 'type ' => 'component ' ,
467+ 'deleted_at ' => $ component ->deleted_at ,
468+ ],
469+ 'assigned_qty ' => $ component_checkout ->assigned_qty ,
470+ 'note ' => ($ component_checkout ->note ) ? e ($ component_checkout ->note ) : null ,
459471 'created_at ' => Helper::getFormattedDateObject ($ component_checkout ->created_at , 'datetime ' ),
460472 'created_by ' => $ component_checkout ->adminuser ? [
461473 'id ' => (int ) $ component_checkout ->adminuser ->id ,
462474 'name ' => e ($ component_checkout ->adminuser ->display_name ),
463475 ] : null ,
464476 'available_actions ' => [
465- 'checkin ' => (($ component? ->deleted_at == '' ) && Gate::allows ('checkin ' , Component::class)),
466- 'view ' => (($ component? ->deleted_at == '' ) && Gate::allows ('view ' , Component::class)),
477+ 'checkin ' => (($ component ->deleted_at == '' ) && Gate::allows ('checkin ' , Component::class)),
478+ 'view ' => (($ component ->deleted_at == '' ) && Gate::allows ('view ' , Component::class)),
467479 ],
468480 ];
469481 }
470482
471- return (new DatatablesTransformer )->transformDatatables ($ array , $ total );
483+ return (new DatatablesTransformer )->transformDatatables ($ array , max ( 0 , $ total - $ suppressed ) );
472484 }
473485}
0 commit comments