@@ -3,9 +3,9 @@ package scim
33import (
44 "encoding/json"
55 "net/http"
6- "strings"
76
87 "github.com/elimity-com/scim/errors"
8+ "github.com/elimity-com/scim/filter"
99 "github.com/elimity-com/scim/schema"
1010)
1111
@@ -30,6 +30,62 @@ func (s Server) errorHandler(w http.ResponseWriter, scimErr *errors.ScimError) {
3030 }
3131}
3232
33+ // parseSearchRequest reads and parses a search request body, returning a SearchParams.
34+ func (s Server ) parseSearchRequest (r * http.Request ) (searchRequest , SearchParams , * errors.ScimError ) {
35+ data , err := readBody (r )
36+ if err != nil {
37+ return searchRequest {}, SearchParams {}, & errors .ScimErrorInternal
38+ }
39+
40+ var sr searchRequest
41+ if err := json .Unmarshal (data , & sr ); err != nil {
42+ scimErr := errors.ScimError {
43+ Status : 400 ,
44+ Detail : "Invalid search request body." ,
45+ }
46+ return searchRequest {}, SearchParams {}, & scimErr
47+ }
48+
49+ if len (sr .Schemas ) != 1 || sr .Schemas [0 ] != "urn:ietf:params:scim:api:messages:2.0:SearchRequest" {
50+ return searchRequest {}, SearchParams {}, & errors .ScimErrorInvalidValue
51+ }
52+
53+ if sr .SortOrder != "" && sr .SortOrder != "ascending" && sr .SortOrder != "descending" {
54+ return searchRequest {}, SearchParams {}, & errors .ScimErrorInvalidValue
55+ }
56+
57+ defaultCount := s .config .getItemsPerPage ()
58+
59+ count := defaultCount
60+ if sr .Count != nil {
61+ count = * sr .Count
62+ }
63+ if count > defaultCount {
64+ count = defaultCount
65+ }
66+ if count < 0 {
67+ count = 0
68+ }
69+
70+ startIndex := defaultStartIndex
71+ if sr .StartIndex != nil {
72+ startIndex = * sr .StartIndex
73+ }
74+ if startIndex < 1 {
75+ startIndex = defaultStartIndex
76+ }
77+
78+ return sr , SearchParams {
79+ Attributes : sr .Attributes ,
80+ Count : count ,
81+ ExcludedAttributes : sr .ExcludedAttributes ,
82+ Filter : sr .Filter ,
83+ SortBy : sr .SortBy ,
84+ SortOrder : sr .SortOrder ,
85+ StartIndex : startIndex ,
86+ }, nil
87+ }
88+
3389// resourceDeleteHandler receives an HTTP DELETE request to the resource endpoint, e.g., "/Users/{id}" or "/Groups/{id}",
3490// where "{id}" is a resource identifier to delete a known resource.
3591func (s Server ) resourceDeleteHandler (w http.ResponseWriter , r * http.Request , id string , resourceType ResourceType ) {
@@ -215,6 +271,70 @@ func (s Server) resourcePutHandler(w http.ResponseWriter, r *http.Request, id st
215271 }
216272}
217273
274+ // resourceSearchHandler receives an HTTP POST request to a resource endpoint's /.search
275+ // (e.g. /Users/.search) to query resources of that type.
276+ func (s Server ) resourceSearchHandler (w http.ResponseWriter , r * http.Request , resourceType ResourceType ) {
277+ searcher , ok := resourceType .Handler .(ResourceSearcher )
278+ if ! ok {
279+ s .errorHandler (w , & errors.ScimError {
280+ Status : 501 ,
281+ Detail : "Search is not supported for this resource type." ,
282+ })
283+ return
284+ }
285+
286+ _ , params , scimErr := s .parseSearchRequest (r )
287+ if scimErr != nil {
288+ s .errorHandler (w , scimErr )
289+ return
290+ }
291+
292+ if params .Filter != "" {
293+ validator , err := filter .NewValidator (params .Filter , resourceType .Schema , resourceType .getSchemaExtensions ()... )
294+ if err != nil {
295+ s .errorHandler (w , & errors .ScimErrorInvalidFilter )
296+ return
297+ }
298+ if err := validator .Validate (); err != nil {
299+ s .errorHandler (w , & errors .ScimErrorInvalidFilter )
300+ return
301+ }
302+ params .FilterValidator = & validator
303+ }
304+
305+ page , searchErr := searcher .Search (r , params )
306+ if searchErr != nil {
307+ scimErr := errors .CheckScimError (searchErr , http .MethodPost )
308+ s .errorHandler (w , & scimErr )
309+ return
310+ }
311+
312+ lr := listResponse {
313+ TotalResults : page .TotalResults ,
314+ Resources : page .resources (resourceType , s .baseURL ),
315+ StartIndex : params .StartIndex ,
316+ ItemsPerPage : params .Count ,
317+ }
318+ raw , err := json .Marshal (lr )
319+ if err != nil {
320+ s .errorHandler (w , & errors .ScimErrorInternal )
321+ s .log .Error (
322+ "failed marshaling list response" ,
323+ "listResponse" , lr ,
324+ "error" , err ,
325+ )
326+ return
327+ }
328+
329+ _ , err = w .Write (raw )
330+ if err != nil {
331+ s .log .Error (
332+ "failed writing response" ,
333+ "error" , err ,
334+ )
335+ }
336+ }
337+
218338// resourceTypeHandler receives an HTTP GET to retrieve individual resource types which can be returned by appending the
219339// resource types name to the /ResourceTypes endpoint. For example: "/ResourceTypes/User".
220340func (s Server ) resourceTypeHandler (w http.ResponseWriter , r * http.Request , name string ) {
@@ -345,7 +465,6 @@ func (s Server) rootResourcesGetHandler(w http.ResponseWriter, r *http.Request)
345465
346466 params := ListRequestParams {
347467 Count : count ,
348- Filter : strings .TrimSpace (r .URL .Query ().Get ("filter" )),
349468 StartIndex : startIndex ,
350469 }
351470
@@ -384,51 +503,27 @@ func (s Server) rootResourcesGetHandler(w http.ResponseWriter, r *http.Request)
384503
385504// rootSearchHandler receives an HTTP POST request to /.search to query across all resource types.
386505// Per RFC 7644 Section 3.4.3, this is an alternative to GET / with query parameters.
506+ // If the RootQueryHandler also implements ResourceSearcher, the Search method is called
507+ // with full SearchParams. Otherwise, GetAll is called with only Count and StartIndex.
387508func (s Server ) rootSearchHandler (w http.ResponseWriter , r * http.Request ) {
388- data , err := readBody (r )
389- if err != nil {
390- s .errorHandler (w , & errors .ScimErrorInternal )
391- return
392- }
393-
394- var sr searchRequest
395- if err := json .Unmarshal (data , & sr ); err != nil {
396- scimErr := errors.ScimError {
397- Status : http .StatusBadRequest ,
398- Detail : "Invalid search request body." ,
399- }
400- s .errorHandler (w , & scimErr )
509+ _ , params , scimErr := s .parseSearchRequest (r )
510+ if scimErr != nil {
511+ s .errorHandler (w , scimErr )
401512 return
402513 }
403514
404- defaultCount := s .config .getItemsPerPage ()
405-
406- count := defaultCount
407- if sr .Count != nil {
408- count = * sr .Count
409- }
410- if count > defaultCount {
411- count = defaultCount
412- }
413- if count < 0 {
414- count = 0
415- }
416-
417- startIndex := defaultStartIndex
418- if sr .StartIndex != nil {
419- startIndex = * sr .StartIndex
420- }
421- if startIndex < 1 {
422- startIndex = defaultStartIndex
423- }
424-
425- params := ListRequestParams {
426- Count : count ,
427- Filter : sr .Filter ,
428- StartIndex : startIndex ,
515+ var (
516+ page Page
517+ getError error
518+ )
519+ if searcher , ok := s .rootQueryHandler .(ResourceSearcher ); ok {
520+ page , getError = searcher .Search (r , params )
521+ } else {
522+ page , getError = s .rootQueryHandler .GetAll (r , ListRequestParams {
523+ Count : params .Count ,
524+ StartIndex : params .StartIndex ,
525+ })
429526 }
430-
431- page , getError := s .rootQueryHandler .GetAll (r , params )
432527 if getError != nil {
433528 scimErr := errors .CheckScimError (getError , http .MethodPost )
434529 s .errorHandler (w , & scimErr )
@@ -570,8 +665,12 @@ func (s Server) serviceProviderConfigHandler(w http.ResponseWriter, r *http.Requ
570665
571666// searchRequest represents the JSON body of a POST /.search request per RFC 7644 Section 3.4.3.
572667type searchRequest struct {
573- Schemas []string `json:"schemas"`
574- Filter string `json:"filter"`
575- StartIndex * int `json:"startIndex"`
576- Count * int `json:"count"`
668+ Schemas []string `json:"schemas"`
669+ Attributes []string `json:"attributes"`
670+ ExcludedAttributes []string `json:"excludedAttributes"`
671+ Filter string `json:"filter"`
672+ SortBy string `json:"sortBy"`
673+ SortOrder string `json:"sortOrder"`
674+ StartIndex * int `json:"startIndex"`
675+ Count * int `json:"count"`
577676}
0 commit comments