77use ArieTimmerman \Laravel \SCIMServer \Helper ;
88use ArieTimmerman \Laravel \SCIMServer \Exceptions \SCIMException ;
99use ArieTimmerman \Laravel \SCIMServer \ResourceType ;
10+ use ArieTimmerman \Laravel \SCIMServer \SCIMConfig ;
1011use Illuminate \Database \Eloquent \Model ;
1112use ArieTimmerman \Laravel \SCIMServer \Events \Delete ;
1213use ArieTimmerman \Laravel \SCIMServer \Events \Get ;
@@ -304,6 +305,24 @@ function (Builder $query) use ($filter, $resourceType) {
304305 );
305306 }
306307
308+ public function crossResourceSearch (Request $ request , PolicyDecisionPoint $ pdp , SCIMConfig $ config )
309+ {
310+ $ input = $ request ->json ()->all ();
311+
312+ if (!is_array ($ input ) || !isset ($ input ['schemas ' ]) || !in_array ('urn:ietf:params:scim:api:messages:2.0:SearchRequest ' , $ input ['schemas ' ])) {
313+ throw (new SCIMException ('Invalid schema. MUST be "urn:ietf:params:scim:api:messages:2.0:SearchRequest" ' ))->setCode (400 );
314+ }
315+
316+ $ request ->replace ($ input );
317+
318+ return $ this ->runCrossResourceQuery ($ request , $ config );
319+ }
320+
321+ public function crossResourceIndex (Request $ request , PolicyDecisionPoint $ pdp , SCIMConfig $ config )
322+ {
323+ return $ this ->runCrossResourceQuery ($ request , $ config );
324+ }
325+
307326 public function search (Request $ request , PolicyDecisionPoint $ pdp , ResourceType $ resourceType ){
308327
309328 $ input = $ request ->json ()->all ();
@@ -319,6 +338,126 @@ public function search(Request $request, PolicyDecisionPoint $pdp, ResourceType
319338 return $ this ->index ($ request , $ pdp , $ resourceType );
320339 }
321340
341+ protected function runCrossResourceQuery (Request $ request , SCIMConfig $ config ): ListResponse
342+ {
343+ if ($ request ->has ('cursor ' )) {
344+ throw (new SCIMException ('Cursor pagination is not supported for cross-resource search ' ))->setCode (400 )->setScimType ('invalidCursor ' );
345+ }
346+
347+ [$ attributes , $ excludedAttributes ] = $ this ->resolveAttributeParameters ($ request );
348+
349+ $ count = min (max (0 , intVal ($ request ->input ('count ' , config ('scim.pagination.defaultPageSize ' )))), config ('scim.pagination.maxPageSize ' ));
350+ $ startIndex = max (1 , intVal ($ request ->input ('startIndex ' , 1 )));
351+
352+ $ resourceTypes = $ this ->resolveResourceTypesForSearch ($ config );
353+
354+ if (empty ($ resourceTypes )) {
355+ return new ListResponse (collect (), $ startIndex , 0 , $ attributes , $ excludedAttributes );
356+ }
357+
358+ if ($ request ->filled ('sortBy ' ) && count ($ resourceTypes ) > 1 ) {
359+ throw (new SCIMException ('sortBy is only supported when a single resourceType is requested ' ))->setCode (400 )->setScimType ('invalidValue ' );
360+ }
361+
362+ $ sortAttribute = null ;
363+ $ sortDirection = $ request ->input ('sortOrder ' ) === 'descending ' ? 'desc ' : 'asc ' ;
364+
365+ if ($ request ->filled ('sortBy ' )) {
366+ $ sortAttribute = $ resourceTypes [0 ]->getMapping ()->getSortAttributeByPath (ParserParser::parse ($ request ->input ('sortBy ' )));
367+ }
368+
369+ $ filter = $ request ->input ('filter ' );
370+
371+ $ resources = collect ();
372+ $ offset = $ startIndex - 1 ;
373+ $ remaining = $ count ;
374+ $ perTypeTotals = [];
375+ $ totalResults = 0 ;
376+
377+ $ applyFilter = function (Builder $ query , ResourceType $ resourceType ) use ($ filter ) {
378+ if ($ filter === null ) {
379+ return ;
380+ }
381+
382+ try {
383+ Helper::scimFilterToLaravelQuery ($ resourceType , $ query , ParserParser::parseFilter ($ filter ));
384+ } catch (ParserFilterException $ e ) {
385+ throw (new SCIMException ($ e ->getMessage ()))->setCode (400 )->setScimType ('invalidFilter ' );
386+ }
387+ };
388+
389+ foreach ($ resourceTypes as $ resourceType ) {
390+ $ countQuery = $ resourceType ->getQuery ();
391+ $ applyFilter ($ countQuery , $ resourceType );
392+
393+ $ typeTotal = $ countQuery ->count ();
394+ $ perTypeTotals [] = [$ resourceType , $ typeTotal ];
395+ $ totalResults += $ typeTotal ;
396+ }
397+
398+ foreach ($ perTypeTotals as [$ resourceType , $ typeTotal ]) {
399+ if ($ offset >= $ typeTotal ) {
400+ $ offset -= $ typeTotal ;
401+ continue ;
402+ }
403+
404+ if ($ remaining === 0 ) {
405+ break ;
406+ }
407+
408+ $ dataQuery = $ resourceType ->getQuery ();
409+ $ applyFilter ($ dataQuery , $ resourceType );
410+
411+ $ dataQuery = $ dataQuery ->with ($ resourceType ->getWithRelations ());
412+
413+ if ($ sortAttribute !== null ) {
414+ $ dataQuery = $ dataQuery ->orderBy ($ sortAttribute , $ sortDirection );
415+ }
416+
417+ if ($ offset > 0 ) {
418+ $ dataQuery = $ dataQuery ->skip ($ offset );
419+ }
420+
421+ if ($ remaining > 0 ) {
422+ $ dataQuery = $ dataQuery ->take ($ remaining );
423+ }
424+
425+ $ items = $ dataQuery ->get ();
426+
427+ $ offset = 0 ;
428+ $ remaining -= $ items ->count ();
429+
430+ foreach ($ items as $ item ) {
431+ $ resources ->push (
432+ Helper::objectToSCIMArray ($ item , $ resourceType , $ attributes , $ excludedAttributes )
433+ );
434+ }
435+
436+ if ($ remaining <= 0 ) {
437+ break ;
438+ }
439+ }
440+
441+ return new ListResponse ($ resources , $ startIndex , $ totalResults , $ attributes , $ excludedAttributes );
442+ }
443+
444+ protected function resolveResourceTypesForSearch (SCIMConfig $ config ): array
445+ {
446+ $ configurations = $ config ->getConfig ();
447+
448+ if (empty ($ configurations )) {
449+ return [];
450+ }
451+
452+ $ resourceTypes = [];
453+
454+ foreach ($ configurations as $ name => $ configuration ) {
455+ $ resourceTypes [] = new ResourceType ($ name , $ configuration );
456+ }
457+
458+ return $ resourceTypes ;
459+ }
460+
322461 protected function respondWithResource (Request $ request , ResourceType $ resourceType , Model $ resourceObject , int $ status = 200 )
323462 {
324463 [$ attributes , $ excludedAttributes ] = $ this ->resolveAttributeParameters ($ request );
0 commit comments