zitadel/internal/api/scim/service_provider.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

183 lines
6.6 KiB
Go

package scim
import (
"context"
"net/http"
"github.com/gorilla/mux"
zhttp "github.com/zitadel/zitadel/internal/api/http"
scim_config "github.com/zitadel/zitadel/internal/api/scim/config"
sresources "github.com/zitadel/zitadel/internal/api/scim/resources"
sschemas "github.com/zitadel/zitadel/internal/api/scim/schemas"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
type serviceProviderHandler struct {
config *scim_config.Config
schemas []*sschemas.ResourceSchema
schemasByID map[sschemas.ScimSchemaType]*sschemas.ResourceSchema
schemasByResourceName map[sschemas.ScimResourceTypeSingular]*sschemas.ResourceSchema
}
type serviceProviderConfig struct {
*sschemas.Resource
DocumentationUri string `json:"documentationUri"`
Patch serviceProviderConfigSupported `json:"patch"`
Bulk serviceProviderConfigBulk `json:"bulk"`
Filter serviceProviderFilterSupported `json:"filter"`
ChangePassword serviceProviderConfigSupported `json:"changePassword"`
Sort serviceProviderConfigSupported `json:"sort"`
ETag serviceProviderConfigSupported `json:"etag"`
AuthenticationSchemes []*scim_config.ServiceProviderConfigAuthenticationScheme `json:"authenticationSchemes,omitempty"`
}
type serviceProviderConfigSupported struct {
Supported bool `json:"supported"`
}
type serviceProviderFilterSupported struct {
Supported bool `json:"supported"`
MaxResults int `json:"maxResults"`
}
type serviceProviderConfigBulk struct {
Supported bool `json:"supported"`
MaxOperations int `json:"maxOperations"`
MaxPayloadSize int64 `json:"maxPayloadSize"`
}
var (
defaultConfigSearchRequest = query.SearchRequest{
Offset: 0,
Limit: 100,
}
)
func newServiceProviderHandler(cfg *scim_config.Config, handlers ...sresources.RawResourceHandlerAdapter) *serviceProviderHandler {
schemas := make([]*sschemas.ResourceSchema, len(handlers))
schemasByID := make(map[sschemas.ScimSchemaType]*sschemas.ResourceSchema, len(handlers))
schemasByResourceName := make(map[sschemas.ScimResourceTypeSingular]*sschemas.ResourceSchema, len(handlers))
for i, handler := range handlers {
schema := handler.Schema()
schemas[i] = schema
schemasByID[schema.ID] = schema
schemasByResourceName[schema.Name] = schema
}
return &serviceProviderHandler{
config: cfg,
schemas: schemas,
schemasByID: schemasByID,
schemasByResourceName: schemasByResourceName,
}
}
func (h *serviceProviderHandler) GetConfig(r *http.Request) (*serviceProviderConfig, error) {
// the request is unauthenticated, read the orgID from the url instead of the context
orgID := mux.Vars(r)[zhttp.OrgIdInPathVariableName]
return &serviceProviderConfig{
Resource: &sschemas.Resource{
Schemas: []sschemas.ScimSchemaType{sschemas.IdServiceProviderConfig},
Meta: &sschemas.ResourceMeta{
ResourceType: sschemas.ServiceProviderConfigResourceType,
Location: sschemas.BuildLocationWithOrg(r.Context(), orgID, sschemas.ServiceProviderConfigsResourceType, ""),
},
},
DocumentationUri: h.config.DocumentationUrl,
Patch: serviceProviderConfigSupported{
Supported: true,
},
Bulk: serviceProviderConfigBulk{
Supported: true,
MaxOperations: h.config.Bulk.MaxOperationsCount,
MaxPayloadSize: h.config.MaxRequestBodySize,
},
Filter: serviceProviderFilterSupported{
Supported: true,
MaxResults: sresources.MaxListCount,
},
ChangePassword: serviceProviderConfigSupported{
Supported: true,
},
Sort: serviceProviderConfigSupported{
Supported: true,
},
ETag: serviceProviderConfigSupported{
Supported: false,
},
AuthenticationSchemes: h.config.AuthenticationSchemes,
}, nil
}
func (h *serviceProviderHandler) ListResourceTypes(r *http.Request) (*sresources.ListResponse[*sschemas.ResourceType], error) {
// the request is unauthenticated, read the orgID from the url instead of the context
ctx := r.Context()
orgID := mux.Vars(r)[zhttp.OrgIdInPathVariableName]
resourceTypes := make([]*sschemas.ResourceType, len(h.schemas))
for i, schema := range h.schemas {
resourceTypes[i] = schema.ToResourceType(ctx, orgID)
}
return sresources.NewListResponse(uint64(len(resourceTypes)), defaultConfigSearchRequest, resourceTypes), nil
}
func (h *serviceProviderHandler) GetResourceType(r *http.Request) (*sschemas.ResourceType, error) {
// the request is unauthenticated, read the orgID from the url instead of the context
ctx := r.Context()
vars := mux.Vars(r)
orgID := vars[zhttp.OrgIdInPathVariableName]
name := sschemas.ScimResourceTypeSingular(vars["name"])
schema, ok := h.schemasByResourceName[name]
if !ok {
return nil, zerrors.ThrowNotFoundf(nil, "SCIMSP-148z", "Scim resource type %s not found", name)
}
return schema.ToResourceType(ctx, orgID), nil
}
func (h *serviceProviderHandler) ListSchemas(r *http.Request) (*sresources.ListResponse[*sschemas.ResourceSchema], error) {
// the request is unauthenticated, read the orgID from the url instead of the context
ctx := r.Context()
orgID := mux.Vars(r)[zhttp.OrgIdInPathVariableName]
schemas := make([]*sschemas.ResourceSchema, len(h.schemas))
for i, schema := range h.schemas {
schemas[i] = buildSchema(ctx, orgID, schema)
}
return sresources.NewListResponse(uint64(len(h.schemas)), defaultConfigSearchRequest, schemas), nil
}
func (h *serviceProviderHandler) GetSchema(r *http.Request) (*sschemas.ResourceSchema, error) {
// the request is unauthenticated, read the orgID from the url instead of the context
ctx := r.Context()
vars := mux.Vars(r)
orgID := vars[zhttp.OrgIdInPathVariableName]
id := sschemas.ScimSchemaType(vars["id"])
schema, ok := h.schemasByID[id]
if !ok {
return nil, zerrors.ThrowNotFoundf(nil, "SCIMSP-148y", "Scim schema %s not found", id)
}
return buildSchema(ctx, orgID, schema), nil
}
// buildSchema shallow copies the provided schema and sets the correct location based on the provided context information.
func buildSchema(ctx context.Context, orgID string, schema *sschemas.ResourceSchema) *sschemas.ResourceSchema {
newSchema := *schema
newSchema.Resource = &sschemas.Resource{
ID: schema.Resource.ID,
Schemas: schema.Resource.Schemas,
Meta: &sschemas.ResourceMeta{
ResourceType: schema.Resource.Meta.ResourceType,
Location: sschemas.BuildLocationWithOrg(ctx, orgID, sschemas.SchemasResourceType, string(schema.ID)),
},
}
return &newSchema
}