zitadel/internal/api/scim/resources/resource_handler_adapter.go
Lars e15094cdea
feat: add scim v2 service provider configuration endpoints (#9258)
# Which Problems Are Solved
* Adds support for the service provider configuration SCIM v2 endpoints

# How the Problems Are Solved
* Adds support for the service provider configuration SCIM v2 endpoints
  * `GET /scim/v2/{orgId}/ServiceProviderConfig`
  * `GET /scim/v2/{orgId}/ResourceTypes`
  * `GET /scim/v2/{orgId}/ResourceTypes/{name}`
  * `GET /scim/v2/{orgId}/Schemas`
  * `GET /scim/v2/{orgId}/Schemas/{id}`

# Additional Context
Part of #8140

Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
2025-01-29 18:11:12 +00:00

145 lines
4.3 KiB
Go

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 {
Schema() *schemas.ResourceSchema
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]
}
func NewResourceHandlerAdapter[T ResourceHolder](handler ResourceHandler[T]) *ResourceHandlerAdapter[T] {
return &ResourceHandlerAdapter[T]{
handler,
}
}
func (adapter *ResourceHandlerAdapter[T]) Schema() *schemas.ResourceSchema {
return adapter.handler.Schema()
}
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(ctx, entity)
}
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
}
return adapter.handler.Replace(ctx, resourceID, entity)
}
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)
if err := readSchema(data, request, schemas.IdPatchOperation); err != nil {
return err
}
if err := request.Validate(); err != nil {
return err
}
if len(request.Operations) == 0 {
return nil
}
return adapter.handler.Update(ctx, resourceID, request.Operations)
}
func (adapter *ResourceHandlerAdapter[T]) DeleteFromHttp(r *http.Request) error {
return adapter.Delete(r.Context(), mux.Vars(r)["id"])
}
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
}
return adapter.handler.List(r.Context(), request)
}
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]) readEntity(data io.ReadCloser) (T, error) {
entity := adapter.handler.NewResource()
return entity, readSchema(data, entity, adapter.handler.Schema().ID)
}
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 {
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))
}
if serrors.IsScimOrZitadelError(err) {
return err
}
return serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgumentf(err, "SCIM-ucrjson", "Could not deserialize json"))
}
providedSchemas := entity.GetSchemas()
if !slices.Contains(providedSchemas, schema) {
return serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgumentf(nil, "SCIM-xxrschema", "Expected schema %v is not provided", schema))
}
return nil
}