mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:47:33 +00:00
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>
This commit is contained in:
@@ -60,7 +60,7 @@ func NewBulkHandler(
|
||||
) *BulkHandler {
|
||||
handlersByPluralResourceName := make(map[schemas.ScimResourceTypePlural]RawResourceHandlerAdapter, len(handlers))
|
||||
for _, handler := range handlers {
|
||||
handlersByPluralResourceName[handler.ResourceNamePlural()] = handler
|
||||
handlersByPluralResourceName[handler.Schema().PluralName] = handler
|
||||
}
|
||||
|
||||
return &BulkHandler{
|
||||
@@ -135,7 +135,7 @@ func (h *BulkHandler) processOperation(ctx context.Context, op *BulkRequestOpera
|
||||
}
|
||||
|
||||
if resourceNamePlural != "" && resourceID != "" {
|
||||
opResp.Location = buildLocation(ctx, resourceNamePlural, resourceID)
|
||||
opResp.Location = schemas.BuildLocationForResource(ctx, resourceNamePlural, resourceID)
|
||||
}
|
||||
|
||||
opResp.Status = strconv.Itoa(statusCode)
|
||||
|
@@ -2,21 +2,17 @@ package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/scim/resources/patch"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
type ResourceHandler[T ResourceHolder] interface {
|
||||
SchemaType() schemas.ScimSchemaType
|
||||
ResourceNameSingular() schemas.ScimResourceTypeSingular
|
||||
ResourceNamePlural() schemas.ScimResourceTypePlural
|
||||
Schema() *schemas.ResourceSchema
|
||||
NewResource() T
|
||||
|
||||
Create(ctx context.Context, resource T) (T, error)
|
||||
@@ -27,48 +23,31 @@ type ResourceHandler[T ResourceHolder] interface {
|
||||
List(ctx context.Context, request *ListRequest) (*ListResponse[T], error)
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
ID string `json:"-"`
|
||||
Schemas []schemas.ScimSchemaType `json:"schemas"`
|
||||
Meta *ResourceMeta `json:"meta"`
|
||||
}
|
||||
|
||||
type ResourceMeta struct {
|
||||
ResourceType schemas.ScimResourceTypeSingular `json:"resourceType"`
|
||||
Created time.Time `json:"created"`
|
||||
LastModified time.Time `json:"lastModified"`
|
||||
Version string `json:"version"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
type ResourceHolder interface {
|
||||
SchemasHolder
|
||||
GetResource() *Resource
|
||||
GetResource() *schemas.Resource
|
||||
}
|
||||
|
||||
type SchemasHolder interface {
|
||||
GetSchemas() []schemas.ScimSchemaType
|
||||
}
|
||||
|
||||
func buildResource[T ResourceHolder](ctx context.Context, handler ResourceHandler[T], details *domain.ObjectDetails) *Resource {
|
||||
func buildResource[T ResourceHolder](ctx context.Context, handler ResourceHandler[T], details *domain.ObjectDetails) *schemas.Resource {
|
||||
created := details.CreationDate.UTC()
|
||||
if created.IsZero() {
|
||||
created = details.EventDate.UTC()
|
||||
}
|
||||
|
||||
return &Resource{
|
||||
schema := handler.Schema()
|
||||
return &schemas.Resource{
|
||||
ID: details.ID,
|
||||
Schemas: []schemas.ScimSchemaType{handler.SchemaType()},
|
||||
Meta: &ResourceMeta{
|
||||
ResourceType: handler.ResourceNameSingular(),
|
||||
Created: created,
|
||||
LastModified: details.EventDate.UTC(),
|
||||
Schemas: []schemas.ScimSchemaType{schema.ID},
|
||||
Meta: &schemas.ResourceMeta{
|
||||
ResourceType: schema.Name,
|
||||
Created: &created,
|
||||
LastModified: gu.Ptr(details.EventDate.UTC()),
|
||||
Version: strconv.FormatUint(details.Sequence, 10),
|
||||
Location: buildLocation(ctx, handler.ResourceNamePlural(), details.ID),
|
||||
Location: schemas.BuildLocationForResource(ctx, schema.PluralName, details.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildLocation(ctx context.Context, resourceName schemas.ScimResourceTypePlural, id string) string {
|
||||
return http.DomainContext(ctx).Origin() + path.Join(schemas.HandlerPrefix, authz.GetCtxData(ctx).OrgID, string(resourceName), id)
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
// RawResourceHandlerAdapter adapts the ResourceHandler[T] without any generics
|
||||
type RawResourceHandlerAdapter interface {
|
||||
ResourceNamePlural() schemas.ScimResourceTypePlural
|
||||
Schema() *schemas.ResourceSchema
|
||||
|
||||
Create(ctx context.Context, data io.ReadCloser) (ResourceHolder, error)
|
||||
Replace(ctx context.Context, resourceID string, data io.ReadCloser) (ResourceHolder, error)
|
||||
@@ -37,8 +37,8 @@ func NewResourceHandlerAdapter[T ResourceHolder](handler ResourceHandler[T]) *Re
|
||||
}
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) ResourceNamePlural() schemas.ScimResourceTypePlural {
|
||||
return adapter.handler.ResourceNamePlural()
|
||||
func (adapter *ResourceHandlerAdapter[T]) Schema() *schemas.ResourceSchema {
|
||||
return adapter.handler.Schema()
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) CreateFromHttp(r *http.Request) (ResourceHolder, error) {
|
||||
@@ -112,7 +112,7 @@ func (adapter *ResourceHandlerAdapter[T]) GetFromHttp(r *http.Request) (T, error
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) readEntity(data io.ReadCloser) (T, error) {
|
||||
entity := adapter.handler.NewResource()
|
||||
return entity, readSchema(data, entity, adapter.handler.SchemaType())
|
||||
return entity, readSchema(data, entity, adapter.handler.Schema().ID)
|
||||
}
|
||||
|
||||
func readSchema(data io.ReadCloser, entity SchemasHolder, schema schemas.ScimSchemaType) error {
|
||||
|
@@ -28,7 +28,7 @@ type ListRequest struct {
|
||||
SortOrder ListRequestSortOrder `json:"sortOrder" schema:"sortOrder"`
|
||||
}
|
||||
|
||||
type ListResponse[T ResourceHolder] struct {
|
||||
type ListResponse[T any] struct {
|
||||
Schemas []schemas.ScimSchemaType `json:"schemas"`
|
||||
ItemsPerPage uint64 `json:"itemsPerPage"`
|
||||
TotalResults uint64 `json:"totalResults"`
|
||||
@@ -43,7 +43,7 @@ const (
|
||||
ListRequestSortOrderDsc ListRequestSortOrder = "descending"
|
||||
|
||||
defaultListCount = 100
|
||||
maxListCount = 100
|
||||
MaxListCount = 100
|
||||
)
|
||||
|
||||
var parser = zhttp.NewParser()
|
||||
@@ -65,7 +65,7 @@ func (o ListRequestSortOrder) IsAscending() bool {
|
||||
return o == ListRequestSortOrderAsc
|
||||
}
|
||||
|
||||
func newListResponse[T ResourceHolder](totalResultCount uint64, q query.SearchRequest, resources []T) *ListResponse[T] {
|
||||
func NewListResponse[T any](totalResultCount uint64, q query.SearchRequest, resources []T) *ListResponse[T] {
|
||||
return &ListResponse[T]{
|
||||
Schemas: []schemas.ScimSchemaType{schemas.IdListResponse},
|
||||
ItemsPerPage: q.Limit,
|
||||
@@ -137,8 +137,8 @@ func (r *ListRequest) validate() error {
|
||||
// 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)
|
||||
} else if r.Count > MaxListCount {
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "SCIM-ucr", "Limit count exceeded, set a count <= %v", MaxListCount)
|
||||
}
|
||||
|
||||
if !r.SortOrder.isDefined() {
|
||||
|
@@ -23,30 +23,31 @@ type UsersHandler struct {
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
config *scim_config.Config
|
||||
filterEvaluator *filter.Evaluator
|
||||
schema *scim_schemas.ResourceSchema
|
||||
}
|
||||
|
||||
type ScimUser struct {
|
||||
*Resource
|
||||
ID string `json:"id"`
|
||||
ExternalID string `json:"externalId,omitempty"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
Name *ScimUserName `json:"name,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
ProfileUrl *scim_schemas.HttpURL `json:"profileUrl,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
Emails []*ScimEmail `json:"emails,omitempty"`
|
||||
PhoneNumbers []*ScimPhoneNumber `json:"phoneNumbers,omitempty"`
|
||||
Password *scim_schemas.WriteOnlyString `json:"password,omitempty"`
|
||||
Ims []*ScimIms `json:"ims,omitempty"`
|
||||
Addresses []*ScimAddress `json:"addresses,omitempty"`
|
||||
Photos []*ScimPhoto `json:"photos,omitempty"`
|
||||
Entitlements []*ScimEntitlement `json:"entitlements,omitempty"`
|
||||
Roles []*ScimRole `json:"roles,omitempty"`
|
||||
*scim_schemas.Resource `scim:"ignoreInSchema"`
|
||||
ID string `json:"id" scim:"ignoreInSchema"`
|
||||
ExternalID string `json:"externalId,omitempty"`
|
||||
UserName string `json:"userName,omitempty" scim:"required,unique,caseInsensitive"`
|
||||
Name *ScimUserName `json:"name,omitempty" scim:"required"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
ProfileUrl *scim_schemas.HttpURL `json:"profileUrl,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
Emails []*ScimEmail `json:"emails,omitempty" scim:"required"`
|
||||
PhoneNumbers []*ScimPhoneNumber `json:"phoneNumbers,omitempty"`
|
||||
Password *scim_schemas.WriteOnlyString `json:"password,omitempty"`
|
||||
Ims []*ScimIms `json:"ims,omitempty"`
|
||||
Addresses []*ScimAddress `json:"addresses,omitempty"`
|
||||
Photos []*ScimPhoto `json:"photos,omitempty"`
|
||||
Entitlements []*ScimEntitlement `json:"entitlements,omitempty"`
|
||||
Roles []*ScimRole `json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
type ScimEntitlement struct {
|
||||
@@ -87,7 +88,7 @@ type ScimIms struct {
|
||||
}
|
||||
|
||||
type ScimEmail struct {
|
||||
Value string `json:"value"`
|
||||
Value string `json:"value" scim:"required"`
|
||||
Primary bool `json:"primary"`
|
||||
}
|
||||
|
||||
@@ -98,8 +99,8 @@ type ScimPhoneNumber struct {
|
||||
|
||||
type ScimUserName struct {
|
||||
Formatted string `json:"formatted,omitempty"`
|
||||
FamilyName string `json:"familyName,omitempty"`
|
||||
GivenName string `json:"givenName,omitempty"`
|
||||
FamilyName string `json:"familyName,omitempty" scim:"required"`
|
||||
GivenName string `json:"givenName,omitempty" scim:"required"`
|
||||
MiddleName string `json:"middleName,omitempty"`
|
||||
HonorificPrefix string `json:"honorificPrefix,omitempty"`
|
||||
HonorificSuffix string `json:"honorificSuffix,omitempty"`
|
||||
@@ -110,20 +111,26 @@ func NewUsersHandler(
|
||||
query *query.Queries,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
config *scim_config.Config) ResourceHandler[*ScimUser] {
|
||||
return &UsersHandler{command, query, userCodeAlg, config, filter.NewEvaluator(scim_schemas.IdUser)}
|
||||
return &UsersHandler{
|
||||
command,
|
||||
query,
|
||||
userCodeAlg,
|
||||
config,
|
||||
filter.NewEvaluator(scim_schemas.IdUser),
|
||||
scim_schemas.BuildSchema(scim_schemas.SchemaBuilderArgs{
|
||||
ID: scim_schemas.IdUser,
|
||||
Name: scim_schemas.UserResourceType,
|
||||
EndpointName: scim_schemas.UsersResourceType,
|
||||
Description: "User Account",
|
||||
Resource: new(ScimUser),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UsersHandler) ResourceNameSingular() scim_schemas.ScimResourceTypeSingular {
|
||||
return scim_schemas.UserResourceType
|
||||
}
|
||||
|
||||
func (h *UsersHandler) ResourceNamePlural() scim_schemas.ScimResourceTypePlural {
|
||||
return scim_schemas.UsersResourceType
|
||||
}
|
||||
|
||||
func (u *ScimUser) GetResource() *Resource {
|
||||
func (u *ScimUser) GetResource() *scim_schemas.Resource {
|
||||
return u.Resource
|
||||
}
|
||||
|
||||
func (u *ScimUser) GetSchemas() []scim_schemas.ScimSchemaType {
|
||||
if u.Resource == nil {
|
||||
return nil
|
||||
@@ -132,12 +139,12 @@ func (u *ScimUser) GetSchemas() []scim_schemas.ScimSchemaType {
|
||||
return u.Resource.Schemas
|
||||
}
|
||||
|
||||
func (h *UsersHandler) NewResource() *ScimUser {
|
||||
return new(ScimUser)
|
||||
func (h *UsersHandler) Schema() *scim_schemas.ResourceSchema {
|
||||
return h.schema
|
||||
}
|
||||
|
||||
func (h *UsersHandler) SchemaType() scim_schemas.ScimSchemaType {
|
||||
return scim_schemas.IdUser
|
||||
func (h *UsersHandler) NewResource() *ScimUser {
|
||||
return new(ScimUser)
|
||||
}
|
||||
|
||||
func (h *UsersHandler) Create(ctx context.Context, user *ScimUser) (*ScimUser, error) {
|
||||
@@ -226,7 +233,7 @@ func (h *UsersHandler) List(ctx context.Context, request *ListRequest) (*ListRes
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newListResponse(count, q.SearchRequest, make([]*ScimUser, 0)), nil
|
||||
return NewListResponse(count, q.SearchRequest, make([]*ScimUser, 0)), nil
|
||||
}
|
||||
|
||||
users, err := h.query.SearchUsers(ctx, q, nil)
|
||||
@@ -240,7 +247,7 @@ func (h *UsersHandler) List(ctx context.Context, request *ListRequest) (*ListRes
|
||||
}
|
||||
|
||||
scimUsers := h.mapToScimUsers(ctx, users.Users, metadata)
|
||||
return newListResponse(users.SearchResponse.Count, q.SearchRequest, scimUsers), nil
|
||||
return NewListResponse(users.SearchResponse.Count, q.SearchRequest, scimUsers), nil
|
||||
}
|
||||
|
||||
func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
||||
|
@@ -382,29 +382,29 @@ func (h *UsersHandler) mapAndValidateMetadata(ctx context.Context, user *ScimUse
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UsersHandler) buildResourceForQuery(ctx context.Context, user *query.User) *Resource {
|
||||
return &Resource{
|
||||
func (h *UsersHandler) buildResourceForQuery(ctx context.Context, user *query.User) *schemas.Resource {
|
||||
return &schemas.Resource{
|
||||
ID: user.ID,
|
||||
Schemas: []schemas.ScimSchemaType{schemas.IdUser},
|
||||
Meta: &ResourceMeta{
|
||||
Meta: &schemas.ResourceMeta{
|
||||
ResourceType: schemas.UserResourceType,
|
||||
Created: user.CreationDate.UTC(),
|
||||
LastModified: user.ChangeDate.UTC(),
|
||||
Created: gu.Ptr(user.CreationDate.UTC()),
|
||||
LastModified: gu.Ptr(user.ChangeDate.UTC()),
|
||||
Version: strconv.FormatUint(user.Sequence, 10),
|
||||
Location: buildLocation(ctx, h.ResourceNamePlural(), user.ID),
|
||||
Location: schemas.BuildLocationForResource(ctx, h.schema.PluralName, user.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UsersHandler) buildResourceForWriteModel(ctx context.Context, user *command.UserV2WriteModel) *Resource {
|
||||
return &Resource{
|
||||
func (h *UsersHandler) buildResourceForWriteModel(ctx context.Context, user *command.UserV2WriteModel) *schemas.Resource {
|
||||
return &schemas.Resource{
|
||||
Schemas: []schemas.ScimSchemaType{schemas.IdUser},
|
||||
Meta: &ResourceMeta{
|
||||
Meta: &schemas.ResourceMeta{
|
||||
ResourceType: schemas.UserResourceType,
|
||||
Created: user.CreationDate.UTC(),
|
||||
LastModified: user.ChangeDate.UTC(),
|
||||
Created: gu.Ptr(user.CreationDate.UTC()),
|
||||
LastModified: gu.Ptr(user.ChangeDate.UTC()),
|
||||
Version: strconv.FormatUint(user.ProcessedSequence, 10),
|
||||
Location: buildLocation(ctx, h.ResourceNamePlural(), user.AggregateID),
|
||||
Location: schemas.BuildLocationForResource(ctx, h.schema.PluralName, user.AggregateID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -88,7 +88,7 @@ func (h *UsersHandler) buildListQuery(ctx context.Context, request *ListRequest)
|
||||
return q, nil
|
||||
}
|
||||
|
||||
filterQuery, err := request.Filter.BuildQuery(ctx, h.SchemaType(), fieldPathColumnMapping)
|
||||
filterQuery, err := request.Filter.BuildQuery(ctx, h.schema.ID, fieldPathColumnMapping)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
Reference in New Issue
Block a user