mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 05:07:31 +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:
183
internal/api/scim/schemas/schema_builder.go
Normal file
183
internal/api/scim/schemas/schema_builder.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package schemas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type SchemaBuilderArgs struct {
|
||||
ID ScimSchemaType
|
||||
Name ScimResourceTypeSingular
|
||||
EndpointName ScimResourceTypePlural
|
||||
Description string
|
||||
Resource any
|
||||
}
|
||||
|
||||
type fieldSchemaInfo struct {
|
||||
Ignore bool
|
||||
Required bool
|
||||
CaseExact bool
|
||||
Unique bool
|
||||
}
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeOf(time.Time{})
|
||||
languageTagType = reflect.TypeOf(language.Tag{})
|
||||
httpURLType = reflect.TypeOf(HttpURL{})
|
||||
writeOnlyStringType = reflect.TypeOf(WriteOnlyString(""))
|
||||
)
|
||||
|
||||
func BuildSchema(args SchemaBuilderArgs) *ResourceSchema {
|
||||
return &ResourceSchema{
|
||||
Resource: &Resource{
|
||||
Schemas: []ScimSchemaType{IdSchema},
|
||||
ID: string(args.ID),
|
||||
Meta: &ResourceMeta{
|
||||
ResourceType: SchemaResourceType,
|
||||
},
|
||||
},
|
||||
ID: args.ID,
|
||||
Name: args.Name,
|
||||
PluralName: args.EndpointName,
|
||||
Description: args.Description,
|
||||
Attributes: buildSchemaAttributes(reflect.TypeOf(args.Resource)),
|
||||
}
|
||||
}
|
||||
|
||||
func buildSchemaAttributes(fieldType reflect.Type) []*SchemaAttribute {
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
|
||||
if fieldType.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
attributes := make([]*SchemaAttribute, 0, fieldType.NumField())
|
||||
for i := 0; i < fieldType.NumField(); i++ {
|
||||
field := fieldType.Field(i)
|
||||
attribute := buildAttribute(field)
|
||||
|
||||
if attribute != nil {
|
||||
attributes = append(attributes, attribute)
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
func buildAttribute(field reflect.StructField) *SchemaAttribute {
|
||||
info := getFieldSchemaInfo(field)
|
||||
if info.Ignore {
|
||||
return nil
|
||||
}
|
||||
|
||||
fieldType := getFieldType(field)
|
||||
attribute := &SchemaAttribute{
|
||||
Name: getFieldJsonName(field),
|
||||
Description: "For details see RFC7643",
|
||||
Type: getFieldAttributeType(fieldType),
|
||||
MultiValued: isFieldMultiValued(field),
|
||||
Required: info.Required,
|
||||
CaseExact: info.CaseExact,
|
||||
Mutability: SchemaAttributeMutabilityReadWrite,
|
||||
Returned: SchemaAttributeReturnedAlways,
|
||||
Uniqueness: SchemaAttributeUniquenessNone,
|
||||
}
|
||||
|
||||
if attribute.Type == SchemaAttributeTypeComplex {
|
||||
attribute.SubAttributes = buildSchemaAttributes(fieldType)
|
||||
}
|
||||
|
||||
if fieldType == writeOnlyStringType {
|
||||
attribute.Returned = SchemaAttributeReturnedNever
|
||||
attribute.Mutability = SchemaAttributeMutabilityWriteOnly
|
||||
}
|
||||
|
||||
if info.Unique {
|
||||
attribute.Uniqueness = SchemaAttributeUniquenessServer
|
||||
}
|
||||
|
||||
return attribute
|
||||
}
|
||||
|
||||
func isFieldMultiValued(field reflect.StructField) bool {
|
||||
if field.Type.Kind() != reflect.Ptr {
|
||||
return field.Type.Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
return field.Type.Elem().Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
func getFieldAttributeType(fieldType reflect.Type) SchemaAttributeType {
|
||||
switch fieldType.Kind() { //nolint:exhaustive
|
||||
case reflect.String:
|
||||
return SchemaAttributeTypeString
|
||||
case reflect.Bool:
|
||||
return SchemaAttributeTypeBoolean
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return SchemaAttributeTypeInteger
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return SchemaAttributeTypeDecimal
|
||||
case reflect.Struct:
|
||||
switch fieldType {
|
||||
case timeType:
|
||||
return SchemaAttributeTypeDateTime
|
||||
case writeOnlyStringType, languageTagType, httpURLType:
|
||||
return SchemaAttributeTypeString
|
||||
default:
|
||||
return SchemaAttributeTypeComplex
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported field type: %v", fieldType.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
func getFieldType(field reflect.StructField) reflect.Type {
|
||||
fieldType := field.Type
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
|
||||
if fieldType.Kind() == reflect.Slice || fieldType.Kind() == reflect.Array {
|
||||
fieldType = fieldType.Elem()
|
||||
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
}
|
||||
return fieldType
|
||||
}
|
||||
|
||||
func getFieldSchemaInfo(field reflect.StructField) *fieldSchemaInfo {
|
||||
tag := field.Tag.Get("scim")
|
||||
tagOptions := strings.Split(tag, ",")
|
||||
return &fieldSchemaInfo{
|
||||
Ignore: slices.Contains(tagOptions, "ignoreInSchema"),
|
||||
Required: slices.Contains(tagOptions, "required"),
|
||||
CaseExact: !slices.Contains(tagOptions, "caseInsensitive"),
|
||||
Unique: slices.Contains(tagOptions, "unique"),
|
||||
}
|
||||
}
|
||||
|
||||
func getFieldJsonName(field reflect.StructField) string {
|
||||
jsonTag := field.Tag.Get("json")
|
||||
|
||||
// Skip fields explicitly excluded
|
||||
if jsonTag == "-" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// use field name as default
|
||||
if jsonTag == "" {
|
||||
return field.Name
|
||||
}
|
||||
|
||||
// strip other options such as omitempty
|
||||
return strings.Split(jsonTag, ",")[0]
|
||||
}
|
@@ -1,5 +1,14 @@
|
||||
package schemas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
)
|
||||
|
||||
type ScimSchemaType string
|
||||
type ScimResourceTypeSingular string
|
||||
type ScimResourceTypePlural string
|
||||
@@ -9,17 +18,147 @@ const (
|
||||
idPrefixCore = "urn:ietf:params:scim:schemas:core:2.0:"
|
||||
idPrefixZitadelMessages = "urn:ietf:params:scim:api:zitadel:messages:2.0:"
|
||||
|
||||
IdUser ScimSchemaType = idPrefixCore + "User"
|
||||
IdListResponse ScimSchemaType = idPrefixMessages + "ListResponse"
|
||||
IdPatchOperation ScimSchemaType = idPrefixMessages + "PatchOp"
|
||||
IdSearchRequest ScimSchemaType = idPrefixMessages + "SearchRequest"
|
||||
IdBulkRequest ScimSchemaType = idPrefixMessages + "BulkRequest"
|
||||
IdBulkResponse ScimSchemaType = idPrefixMessages + "BulkResponse"
|
||||
IdError ScimSchemaType = idPrefixMessages + "Error"
|
||||
IdZitadelErrorDetail ScimSchemaType = idPrefixZitadelMessages + "ErrorDetail"
|
||||
IdUser ScimSchemaType = idPrefixCore + "User"
|
||||
IdServiceProviderConfig ScimSchemaType = idPrefixCore + "ServiceProviderConfig"
|
||||
IdResourceType ScimSchemaType = idPrefixCore + "ResourceType"
|
||||
IdSchema ScimSchemaType = idPrefixCore + "Schema"
|
||||
IdListResponse ScimSchemaType = idPrefixMessages + "ListResponse"
|
||||
IdPatchOperation ScimSchemaType = idPrefixMessages + "PatchOp"
|
||||
IdSearchRequest ScimSchemaType = idPrefixMessages + "SearchRequest"
|
||||
IdBulkRequest ScimSchemaType = idPrefixMessages + "BulkRequest"
|
||||
IdBulkResponse ScimSchemaType = idPrefixMessages + "BulkResponse"
|
||||
IdError ScimSchemaType = idPrefixMessages + "Error"
|
||||
IdZitadelErrorDetail ScimSchemaType = idPrefixZitadelMessages + "ErrorDetail"
|
||||
|
||||
UserResourceType ScimResourceTypeSingular = "User"
|
||||
UsersResourceType ScimResourceTypePlural = "Users"
|
||||
|
||||
ServiceProviderConfigResourceType ScimResourceTypeSingular = "ServiceProviderConfig"
|
||||
ServiceProviderConfigsResourceType ScimResourceTypePlural = "ServiceProviderConfig"
|
||||
|
||||
SchemaResourceType ScimResourceTypeSingular = "Schema"
|
||||
SchemasResourceType ScimResourceTypePlural = "Schemas"
|
||||
|
||||
ResourceTypesResourceType ScimResourceTypePlural = "ResourceTypes"
|
||||
|
||||
HandlerPrefix = "/scim/v2"
|
||||
)
|
||||
|
||||
type Resource struct {
|
||||
ID string `json:"-"`
|
||||
Schemas []ScimSchemaType `json:"schemas"`
|
||||
Meta *ResourceMeta `json:"meta"`
|
||||
}
|
||||
|
||||
type ResourceMeta struct {
|
||||
ResourceType ScimResourceTypeSingular `json:"resourceType"`
|
||||
Created *time.Time `json:"created,omitempty"`
|
||||
LastModified *time.Time `json:"lastModified,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceType struct {
|
||||
*Resource
|
||||
ID ScimResourceTypeSingular `json:"id"`
|
||||
Name ScimResourceTypeSingular `json:"name"`
|
||||
Endpoint ScimResourceTypePlural `json:"endpoint"`
|
||||
Schema ScimSchemaType `json:"schema"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ResourceSchema struct {
|
||||
*Resource
|
||||
ID ScimSchemaType `json:"id"`
|
||||
Name ScimResourceTypeSingular `json:"name"`
|
||||
PluralName ScimResourceTypePlural `json:"-"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Attributes []*SchemaAttribute `json:"attributes"`
|
||||
}
|
||||
|
||||
type SchemaAttribute struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type SchemaAttributeType `json:"type"`
|
||||
SubAttributes []*SchemaAttribute `json:"subAttributes,omitempty"`
|
||||
MultiValued bool `json:"multiValued"`
|
||||
Required bool `json:"required"`
|
||||
CaseExact bool `json:"caseExact"`
|
||||
Mutability SchemaAttributeMutability `json:"mutability"`
|
||||
Returned SchemaAttributeReturned `json:"returned"`
|
||||
Uniqueness SchemaAttributeUniqueness `json:"uniqueness"`
|
||||
}
|
||||
|
||||
type SchemaAttributeType string
|
||||
|
||||
const (
|
||||
SchemaAttributeTypeString SchemaAttributeType = "string"
|
||||
SchemaAttributeTypeBoolean SchemaAttributeType = "boolean"
|
||||
SchemaAttributeTypeDecimal SchemaAttributeType = "decimal"
|
||||
SchemaAttributeTypeInteger SchemaAttributeType = "integer"
|
||||
SchemaAttributeTypeDateTime SchemaAttributeType = "dateTime"
|
||||
SchemaAttributeTypeComplex SchemaAttributeType = "complex"
|
||||
)
|
||||
|
||||
type SchemaAttributeMutability string
|
||||
|
||||
const (
|
||||
SchemaAttributeMutabilityReadWrite SchemaAttributeMutability = "readWrite"
|
||||
SchemaAttributeMutabilityWriteOnly SchemaAttributeMutability = "writeOnly"
|
||||
)
|
||||
|
||||
type SchemaAttributeReturned string
|
||||
|
||||
const (
|
||||
SchemaAttributeReturnedAlways SchemaAttributeReturned = "always"
|
||||
SchemaAttributeReturnedNever SchemaAttributeReturned = "never"
|
||||
)
|
||||
|
||||
type SchemaAttributeUniqueness string
|
||||
|
||||
const (
|
||||
SchemaAttributeUniquenessNone SchemaAttributeUniqueness = "none"
|
||||
SchemaAttributeUniquenessServer SchemaAttributeUniqueness = "server"
|
||||
)
|
||||
|
||||
func (s *ResourceType) GetSchemas() []ScimSchemaType {
|
||||
return s.Resource.Schemas
|
||||
}
|
||||
|
||||
func (s *ResourceType) GetResource() *Resource {
|
||||
return s.Resource
|
||||
}
|
||||
|
||||
func (s *ResourceSchema) GetSchemas() []ScimSchemaType {
|
||||
return s.Resource.Schemas
|
||||
}
|
||||
|
||||
func (s *ResourceSchema) GetResource() *Resource {
|
||||
return s.Resource
|
||||
}
|
||||
|
||||
func (s *ResourceSchema) ToResourceType(ctx context.Context, orgID string) *ResourceType {
|
||||
return &ResourceType{
|
||||
Resource: &Resource{
|
||||
Schemas: []ScimSchemaType{IdResourceType},
|
||||
ID: string(s.Name),
|
||||
Meta: &ResourceMeta{
|
||||
ResourceType: s.Name,
|
||||
Location: BuildLocationWithOrg(ctx, orgID, ResourceTypesResourceType, string(s.Name)),
|
||||
},
|
||||
},
|
||||
ID: s.Name,
|
||||
Name: s.Name,
|
||||
Endpoint: s.PluralName,
|
||||
Schema: s.ID,
|
||||
Description: s.Description,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildLocationForResource(ctx context.Context, resourceName ScimResourceTypePlural, id string) string {
|
||||
return BuildLocationWithOrg(ctx, authz.GetCtxData(ctx).OrgID, resourceName, id)
|
||||
}
|
||||
|
||||
func BuildLocationWithOrg(ctx context.Context, orgID string, resourceName ScimResourceTypePlural, id string) string {
|
||||
return http.DomainContext(ctx).Origin() + path.Join(HandlerPrefix, orgID, string(resourceName), id)
|
||||
}
|
||||
|
@@ -7,11 +7,6 @@ import "encoding/json"
|
||||
// This increases security to really ensure this is never sent to a client.
|
||||
type WriteOnlyString string
|
||||
|
||||
func NewWriteOnlyString(s string) *WriteOnlyString {
|
||||
wos := WriteOnlyString(s)
|
||||
return &wos
|
||||
}
|
||||
|
||||
func (s *WriteOnlyString) MarshalJSON() ([]byte, error) {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user