Skip to content

Commit db88816

Browse files
authored
feat: add root query endpoint and POST /.search (#199)
Add GET / and POST /.search to query across all resource types. Introduce RootQueryHandler interface, WithRootQueryHandler server option, ValidateFilterForResourceTypes utility, and a raw Filter field on ListRequestParams. Fix filter validation to include common attributes (id, externalId, meta.*) so filters like meta.lastModified work on per-resource endpoints. Fix potential slice mutation in schema attribute appending.
1 parent 2da0e0a commit db88816

7 files changed

Lines changed: 952 additions & 11 deletions

File tree

handlers.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scim
33
import (
44
"encoding/json"
55
"net/http"
6+
"strings"
67

78
"github.com/elimity-com/scim/errors"
89
"github.com/elimity-com/scim/schema"
@@ -334,6 +335,132 @@ func (s Server) resourcesGetHandler(w http.ResponseWriter, r *http.Request, reso
334335
}
335336
}
336337

338+
// rootResourcesGetHandler receives an HTTP GET request to the server root endpoint to query across all resource types.
339+
func (s Server) rootResourcesGetHandler(w http.ResponseWriter, r *http.Request) {
340+
count, startIndex, scimErr := s.parsePaginationParams(r)
341+
if scimErr != nil {
342+
s.errorHandler(w, scimErr)
343+
return
344+
}
345+
346+
params := ListRequestParams{
347+
Count: count,
348+
Filter: strings.TrimSpace(r.URL.Query().Get("filter")),
349+
StartIndex: startIndex,
350+
}
351+
352+
page, getError := s.rootQueryHandler.GetAll(r, params)
353+
if getError != nil {
354+
scimErr := errors.CheckScimError(getError, http.MethodGet)
355+
s.errorHandler(w, &scimErr)
356+
return
357+
}
358+
359+
lr := listResponse{
360+
TotalResults: page.TotalResults,
361+
Resources: page.rawResources(),
362+
StartIndex: params.StartIndex,
363+
ItemsPerPage: params.Count,
364+
}
365+
raw, err := json.Marshal(lr)
366+
if err != nil {
367+
s.errorHandler(w, &errors.ScimErrorInternal)
368+
s.log.Error(
369+
"failed marshaling list response",
370+
"listResponse", lr,
371+
"error", err,
372+
)
373+
return
374+
}
375+
376+
_, err = w.Write(raw)
377+
if err != nil {
378+
s.log.Error(
379+
"failed writing response",
380+
"error", err,
381+
)
382+
}
383+
}
384+
385+
// rootSearchHandler receives an HTTP POST request to /.search to query across all resource types.
386+
// Per RFC 7644 Section 3.4.3, this is an alternative to GET / with query parameters.
387+
func (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)
401+
return
402+
}
403+
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,
429+
}
430+
431+
page, getError := s.rootQueryHandler.GetAll(r, params)
432+
if getError != nil {
433+
scimErr := errors.CheckScimError(getError, http.MethodPost)
434+
s.errorHandler(w, &scimErr)
435+
return
436+
}
437+
438+
lr := listResponse{
439+
TotalResults: page.TotalResults,
440+
Resources: page.rawResources(),
441+
StartIndex: params.StartIndex,
442+
ItemsPerPage: params.Count,
443+
}
444+
raw, err := json.Marshal(lr)
445+
if err != nil {
446+
s.errorHandler(w, &errors.ScimErrorInternal)
447+
s.log.Error(
448+
"failed marshaling list response",
449+
"listResponse", lr,
450+
"error", err,
451+
)
452+
return
453+
}
454+
455+
_, err = w.Write(raw)
456+
if err != nil {
457+
s.log.Error(
458+
"failed writing response",
459+
"error", err,
460+
)
461+
}
462+
}
463+
337464
// schemaHandler receives an HTTP GET to retrieve individual schema definitions which can be returned by appending the
338465
// schema URI to the /Schemas endpoint. For example: "/Schemas/urn:ietf:params:scim:schemas:core:2.0:User".
339466
func (s Server) schemaHandler(w http.ResponseWriter, r *http.Request, id string) {
@@ -440,3 +567,11 @@ func (s Server) serviceProviderConfigHandler(w http.ResponseWriter, r *http.Requ
440567
)
441568
}
442569
}
570+
571+
// searchRequest represents the JSON body of a POST /.search request per RFC 7644 Section 3.4.3.
572+
type searchRequest struct {
573+
Schemas []string `json:"schemas"`
574+
Filter string `json:"filter"`
575+
StartIndex *int `json:"startIndex"`
576+
Count *int `json:"count"`
577+
}

0 commit comments

Comments
 (0)