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:
Lars
2025-01-21 13:31:54 +01:00
committed by GitHub
parent 926e7169b2
commit 1915d35605
37 changed files with 4173 additions and 417 deletions

View File

@@ -109,6 +109,10 @@ func NewOrQuery(queries ...SearchQuery) (*OrQuery, error) {
return &OrQuery{queries: queries}, nil
}
func (q *OrQuery) Prepend(queries ...SearchQuery) {
q.queries = append(queries, q.queries...)
}
func (q *OrQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query.Where(q.comp())
}
@@ -147,6 +151,10 @@ func (q *AndQuery) comp() sq.Sqlizer {
return and
}
func (q *AndQuery) Prepend(queries ...SearchQuery) {
q.queries = append(queries, q.queries...)
}
type NotQuery struct {
query SearchQuery
}
@@ -406,8 +414,12 @@ func (q *NumberQuery) comp() sq.Sqlizer {
return sq.NotEq{q.Column.identifier(): q.Number}
case NumberLess:
return sq.Lt{q.Column.identifier(): q.Number}
case NumberLessOrEqual:
return sq.LtOrEq{q.Column.identifier(): q.Number}
case NumberGreater:
return sq.Gt{q.Column.identifier(): q.Number}
case NumberGreaterOrEqual:
return sq.GtOrEq{q.Column.identifier(): q.Number}
case NumberListContains:
return &listContains{col: q.Column, args: []interface{}{q.Number}}
case numberCompareMax:
@@ -423,7 +435,9 @@ const (
NumberEquals NumberComparison = iota
NumberNotEquals
NumberLess
NumberLessOrEqual
NumberGreater
NumberGreaterOrEqual
NumberListContains
numberCompareMax
@@ -588,6 +602,57 @@ func (q *BoolQuery) comp() sq.Sqlizer {
return sq.Eq{q.Column.identifier(): q.Value}
}
type BytesComparison int
const (
BytesEquals BytesComparison = iota
BytesNotEquals
bytesCompareMax
)
type BytesQuery struct {
Column Column
Compare BytesComparison
Value []byte
}
func NewBytesQuery(col Column, values []byte, comparison BytesComparison) (*BytesQuery, error) {
if col.isZero() {
return nil, ErrMissingColumn
}
if comparison < 0 || comparison >= bytesCompareMax {
return nil, ErrInvalidCompare
}
return &BytesQuery{
Column: col,
Value: values,
Compare: comparison,
}, nil
}
func (q *BytesQuery) Col() Column {
return q.Column
}
func (q *BytesQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query.Where(q.comp())
}
func (q *BytesQuery) comp() sq.Sqlizer {
switch q.Compare {
case BytesEquals:
return sq.Eq{q.Column.identifier(): q.Value}
case BytesNotEquals:
return sq.NotEq{q.Column.identifier(): q.Value}
case bytesCompareMax:
return nil
}
return nil
}
type TimestampComparison int
const (