mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 00:47:33 +00:00
feat: bulk scim v2 endpoint (#9256)
# Which Problems Are Solved * Adds support for the bulk SCIM v2 endpoint # How the Problems Are Solved * Adds support for the bulk SCIM v2 endpoint under `POST /scim/v2/{orgID}/Bulk` # Additional Context Part of #8140 Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
@@ -1,17 +1,32 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/scim/resources/patch"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/serrors"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
// RawResourceHandlerAdapter adapts the ResourceHandler[T] without any generics
|
||||
type RawResourceHandlerAdapter interface {
|
||||
ResourceNamePlural() schemas.ScimResourceTypePlural
|
||||
|
||||
Create(ctx context.Context, data io.ReadCloser) (ResourceHolder, error)
|
||||
Replace(ctx context.Context, resourceID string, data io.ReadCloser) (ResourceHolder, error)
|
||||
Update(ctx context.Context, resourceID string, data io.ReadCloser) error
|
||||
Delete(ctx context.Context, resourceID string) error
|
||||
}
|
||||
|
||||
type ResourceHandlerAdapter[T ResourceHolder] struct {
|
||||
handler ResourceHandler[T]
|
||||
}
|
||||
@@ -22,38 +37,47 @@ func NewResourceHandlerAdapter[T ResourceHolder](handler ResourceHandler[T]) *Re
|
||||
}
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Create(r *http.Request) (T, error) {
|
||||
entity, err := adapter.readEntityFromBody(r)
|
||||
func (adapter *ResourceHandlerAdapter[T]) ResourceNamePlural() schemas.ScimResourceTypePlural {
|
||||
return adapter.handler.ResourceNamePlural()
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) CreateFromHttp(r *http.Request) (ResourceHolder, error) {
|
||||
return adapter.Create(r.Context(), r.Body)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Create(ctx context.Context, data io.ReadCloser) (ResourceHolder, error) {
|
||||
entity, err := adapter.readEntity(data)
|
||||
if err != nil {
|
||||
return entity, err
|
||||
}
|
||||
|
||||
return adapter.handler.Create(r.Context(), entity)
|
||||
return adapter.handler.Create(ctx, entity)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Replace(r *http.Request) (T, error) {
|
||||
entity, err := adapter.readEntityFromBody(r)
|
||||
func (adapter *ResourceHandlerAdapter[T]) ReplaceFromHttp(r *http.Request) (ResourceHolder, error) {
|
||||
return adapter.Replace(r.Context(), mux.Vars(r)["id"], r.Body)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Replace(ctx context.Context, resourceID string, data io.ReadCloser) (ResourceHolder, error) {
|
||||
entity, err := adapter.readEntity(data)
|
||||
if err != nil {
|
||||
return entity, err
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
return adapter.handler.Replace(r.Context(), id, entity)
|
||||
return adapter.handler.Replace(ctx, resourceID, entity)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Update(r *http.Request) error {
|
||||
func (adapter *ResourceHandlerAdapter[T]) UpdateFromHttp(r *http.Request) error {
|
||||
return adapter.Update(r.Context(), mux.Vars(r)["id"], r.Body)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Update(ctx context.Context, resourceID string, data io.ReadCloser) error {
|
||||
request := new(patch.OperationRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(request)
|
||||
if err != nil {
|
||||
if zerrors.IsZitadelError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgumentf(nil, "SCIM-ucrjson2", "Could not deserialize json: %v", err.Error()))
|
||||
if err := readSchema(data, request, schemas.IdPatchOperation); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = request.Validate()
|
||||
if err != nil {
|
||||
if err := request.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -61,17 +85,19 @@ func (adapter *ResourceHandlerAdapter[T]) Update(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
return adapter.handler.Update(r.Context(), id, request.Operations)
|
||||
return adapter.handler.Update(ctx, resourceID, request.Operations)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Delete(r *http.Request) error {
|
||||
id := mux.Vars(r)["id"]
|
||||
return adapter.handler.Delete(r.Context(), id)
|
||||
func (adapter *ResourceHandlerAdapter[T]) DeleteFromHttp(r *http.Request) error {
|
||||
return adapter.Delete(r.Context(), mux.Vars(r)["id"])
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) List(r *http.Request) (*ListResponse[T], error) {
|
||||
request, err := readListRequest(r)
|
||||
func (adapter *ResourceHandlerAdapter[T]) Delete(ctx context.Context, resourceID string) error {
|
||||
return adapter.handler.Delete(ctx, resourceID)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) ListFromHttp(r *http.Request) (*ListResponse[T], error) {
|
||||
request, err := adapter.readListRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -79,30 +105,40 @@ func (adapter *ResourceHandlerAdapter[T]) List(r *http.Request) (*ListResponse[T
|
||||
return adapter.handler.List(r.Context(), request)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Get(r *http.Request) (T, error) {
|
||||
func (adapter *ResourceHandlerAdapter[T]) GetFromHttp(r *http.Request) (T, error) {
|
||||
id := mux.Vars(r)["id"]
|
||||
return adapter.handler.Get(r.Context(), id)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) readEntityFromBody(r *http.Request) (T, error) {
|
||||
func (adapter *ResourceHandlerAdapter[T]) readEntity(data io.ReadCloser) (T, error) {
|
||||
entity := adapter.handler.NewResource()
|
||||
err := json.NewDecoder(r.Body).Decode(entity)
|
||||
return entity, readSchema(data, entity, adapter.handler.SchemaType())
|
||||
}
|
||||
|
||||
func readSchema(data io.ReadCloser, entity SchemasHolder, schema schemas.ScimSchemaType) error {
|
||||
defer func() {
|
||||
err := data.Close()
|
||||
logging.OnError(err).Warn("Failed to close http request body")
|
||||
}()
|
||||
|
||||
err := json.NewDecoder(data).Decode(&entity)
|
||||
if err != nil {
|
||||
if serrors.IsScimOrZitadelError(err) {
|
||||
return entity, err
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
if errors.As(err, &maxBytesErr) {
|
||||
return serrors.ThrowPayloadTooLarge(zerrors.ThrowInvalidArgumentf(err, "SCIM-hmaxb1", "Request payload too large, max %d bytes allowed.", maxBytesErr.Limit))
|
||||
}
|
||||
|
||||
return entity, serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgumentf(nil, "SCIM-ucrjson", "Could not deserialize json: %v", err.Error()))
|
||||
if serrors.IsScimOrZitadelError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgumentf(err, "SCIM-ucrjson", "Could not deserialize json"))
|
||||
}
|
||||
|
||||
resource := entity.GetResource()
|
||||
if resource == nil {
|
||||
return entity, serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgument(nil, "SCIM-xxrjson", "Could not get resource, is the schema correct?"))
|
||||
providedSchemas := entity.GetSchemas()
|
||||
if !slices.Contains(providedSchemas, schema) {
|
||||
return serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgumentf(nil, "SCIM-xxrschema", "Expected schema %v is not provided", schema))
|
||||
}
|
||||
|
||||
if !slices.Contains(resource.Schemas, adapter.handler.SchemaType()) {
|
||||
return entity, serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgumentf(nil, "SCIM-xxrschema", "Expected schema %v is not provided", adapter.handler.SchemaType()))
|
||||
}
|
||||
|
||||
return entity, nil
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user