mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:57:32 +00:00
feat: list users scim v2 endpoint (#9187)
# Which Problems Are Solved - Adds support for the list users SCIM v2 endpoint # How the Problems Are Solved - Adds support for the list users SCIM v2 endpoints under `GET /scim/v2/{orgID}/Users` and `POST /scim/v2/{orgID}/Users/.search` # Additional Changes - adds a new function `SearchUserMetadataForUsers` to the query layer to query a metadata keyset for given user ids - adds a new function `NewUserMetadataExistsQuery` to the query layer to query a given metadata key value pair exists - adds a new function `CountUsers` to the query layer to count users without reading any rows - handle `ErrorAlreadyExists` as scim errors `uniqueness` - adds `NumberLessOrEqual` and `NumberGreaterOrEqual` query comparison methods - adds `BytesQuery` with `BytesEquals` and `BytesNotEquals` query comparison methods # Additional Context Part of #8140 Supported fields for scim filters: * `meta.created` * `meta.lastModified` * `id` * `username` * `name.familyName` * `name.givenName` * `emails` and `emails.value` * `active` only eq and ne * `externalId` only eq and ne
This commit is contained in:
148
internal/api/scim/resources/resource_list.go
Normal file
148
internal/api/scim/resources/resource_list.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
zhttp "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/resources/filter"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/serrors"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type ListRequest struct {
|
||||
// Count An integer indicating the desired maximum number of query results per page.
|
||||
Count int64 `json:"count" schema:"count"`
|
||||
|
||||
// StartIndex An integer indicating the 1-based index of the first query result.
|
||||
StartIndex int64 `json:"startIndex" schema:"startIndex"`
|
||||
|
||||
// Filter a scim filter expression to filter the query result.
|
||||
Filter *filter.Filter `json:"filter,omitempty" schema:"filter"`
|
||||
|
||||
// SortBy attribute path to the sort attribute
|
||||
SortBy string `json:"sortBy" schema:"sortBy"`
|
||||
SortOrder ListRequestSortOrder `json:"sortOrder" schema:"sortOrder"`
|
||||
}
|
||||
|
||||
type ListResponse[T ResourceHolder] struct {
|
||||
Schemas []schemas.ScimSchemaType `json:"schemas"`
|
||||
ItemsPerPage uint64 `json:"itemsPerPage"`
|
||||
TotalResults uint64 `json:"totalResults"`
|
||||
StartIndex uint64 `json:"startIndex"`
|
||||
Resources []T `json:"Resources"` // according to the rfc this is the only field in PascalCase...
|
||||
}
|
||||
|
||||
type ListRequestSortOrder string
|
||||
|
||||
const (
|
||||
ListRequestSortOrderAsc ListRequestSortOrder = "ascending"
|
||||
ListRequestSortOrderDsc ListRequestSortOrder = "descending"
|
||||
|
||||
defaultListCount = 100
|
||||
maxListCount = 100
|
||||
)
|
||||
|
||||
var parser = zhttp.NewParser()
|
||||
|
||||
func (o ListRequestSortOrder) isDefined() bool {
|
||||
switch o {
|
||||
case ListRequestSortOrderAsc, ListRequestSortOrderDsc:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (o ListRequestSortOrder) IsAscending() bool {
|
||||
return o == ListRequestSortOrderAsc
|
||||
}
|
||||
|
||||
func newListResponse[T ResourceHolder](totalResultCount uint64, q query.SearchRequest, resources []T) *ListResponse[T] {
|
||||
return &ListResponse[T]{
|
||||
Schemas: []schemas.ScimSchemaType{schemas.IdListResponse},
|
||||
ItemsPerPage: q.Limit,
|
||||
TotalResults: totalResultCount,
|
||||
StartIndex: q.Offset + 1, // start index is 1 based
|
||||
Resources: resources,
|
||||
}
|
||||
}
|
||||
|
||||
func readListRequest(r *http.Request) (*ListRequest, error) {
|
||||
request := &ListRequest{
|
||||
Count: defaultListCount,
|
||||
StartIndex: 1,
|
||||
SortOrder: ListRequestSortOrderAsc,
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if err := parser.Parse(r, request); err != nil {
|
||||
err = parser.UnwrapParserError(err)
|
||||
|
||||
if serrors.IsScimOrZitadelError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "SCIM-ullform", "Could not decode form: "+err.Error())
|
||||
}
|
||||
case http.MethodPost:
|
||||
if err := json.NewDecoder(r.Body).Decode(request); err != nil {
|
||||
if serrors.IsScimOrZitadelError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "SCIM-ulljson", "Could not decode json: "+err.Error())
|
||||
}
|
||||
|
||||
// json deserialization initializes this field if an empty string is provided
|
||||
// to not special case this in the resource implementation,
|
||||
// set it to nil here.
|
||||
if request.Filter.IsZero() {
|
||||
request.Filter = nil
|
||||
}
|
||||
}
|
||||
|
||||
return request, request.validate()
|
||||
}
|
||||
|
||||
func (r *ListRequest) toSearchRequest(defaultSortCol query.Column, fieldPathColumnMapping filter.FieldPathMapping) (query.SearchRequest, error) {
|
||||
sr := query.SearchRequest{
|
||||
Offset: uint64(r.StartIndex - 1), // start index is 1 based
|
||||
Limit: uint64(r.Count),
|
||||
Asc: r.SortOrder.IsAscending(),
|
||||
}
|
||||
|
||||
if r.SortBy == "" {
|
||||
// set a default sort to ensure consistent results
|
||||
sr.SortingColumn = defaultSortCol
|
||||
} else if sortCol, err := fieldPathColumnMapping.Resolve(r.SortBy); err != nil {
|
||||
return sr, serrors.ThrowInvalidValue(zerrors.ThrowInvalidArgument(err, "SCIM-SRT1", "SortBy field is unknown or not supported"))
|
||||
} else {
|
||||
sr.SortingColumn = sortCol.Column
|
||||
}
|
||||
|
||||
return sr, nil
|
||||
}
|
||||
|
||||
func (r *ListRequest) validate() error {
|
||||
// according to the spec values < 1 are treated as 1
|
||||
if r.StartIndex < 1 {
|
||||
r.StartIndex = 1
|
||||
}
|
||||
|
||||
// according to the spec values < 0 are treated as 0
|
||||
if r.Count < 0 {
|
||||
r.Count = 0
|
||||
} else if r.Count > maxListCount {
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "SCIM-ucr", "Limit count exceeded, set a count <= %v", maxListCount)
|
||||
}
|
||||
|
||||
if !r.SortOrder.isDefined() {
|
||||
return zerrors.ThrowInvalidArgument(nil, "SCIM-ucx", "Invalid sort order")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user