mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:47:33 +00:00
feat: get user scim v2 endpoint (#9161)
# Which Problems Are Solved - Adds support for the get user SCIM v2 endpoint # How the Problems Are Solved - Adds support for the get user SCIM v2 endpoint under `GET /scim/v2/{orgID}/Users/{id}` # Additional Context Part of #8140 Replaces https://github.com/zitadel/zitadel/pull/9154 as requested by the maintainers, discussions see https://github.com/zitadel/zitadel/pull/9154.
This commit is contained in:
@@ -20,6 +20,7 @@ type ResourceHandler[T ResourceHolder] interface {
|
||||
|
||||
Create(ctx context.Context, resource T) (T, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
Get(ctx context.Context, id string) (T, error)
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
|
@@ -52,6 +52,11 @@ func (adapter *ResourceHandlerAdapter[T]) Delete(r *http.Request) error {
|
||||
return adapter.handler.Delete(r.Context(), id)
|
||||
}
|
||||
|
||||
func (adapter *ResourceHandlerAdapter[T]) Get(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) {
|
||||
entity := adapter.handler.NewResource()
|
||||
err := json.NewDecoder(r.Body).Decode(entity)
|
||||
|
@@ -155,6 +155,19 @@ func (h *UsersHandler) Delete(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *UsersHandler) Get(ctx context.Context, id string) (*ScimUser, error) {
|
||||
user, err := h.query.GetUserByID(ctx, false, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadata, err := h.queryMetadataForUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.mapToScimUser(ctx, user, metadata), nil
|
||||
}
|
||||
|
||||
func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
||||
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
||||
if err != nil {
|
||||
|
@@ -2,9 +2,15 @@ package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/zitadel/logging"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/scim/metadata"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
@@ -81,6 +87,112 @@ func (h *UsersHandler) mapPrimaryPhone(scimUser *ScimUser) command.Phone {
|
||||
return command.Phone{}
|
||||
}
|
||||
|
||||
func (h *UsersHandler) mapToScimUser(ctx context.Context, user *query.User, md map[metadata.ScopedKey][]byte) *ScimUser {
|
||||
scimUser := &ScimUser{
|
||||
Resource: h.buildResourceForQuery(ctx, user),
|
||||
ID: user.ID,
|
||||
ExternalID: extractScalarMetadata(ctx, md, metadata.KeyExternalId),
|
||||
UserName: user.Username,
|
||||
ProfileUrl: extractHttpURLMetadata(ctx, md, metadata.KeyProfileUrl),
|
||||
Title: extractScalarMetadata(ctx, md, metadata.KeyTitle),
|
||||
Locale: extractScalarMetadata(ctx, md, metadata.KeyLocale),
|
||||
Timezone: extractScalarMetadata(ctx, md, metadata.KeyTimezone),
|
||||
Active: gu.Ptr(user.State.IsEnabled()),
|
||||
Ims: make([]*ScimIms, 0),
|
||||
Addresses: make([]*ScimAddress, 0),
|
||||
Photos: make([]*ScimPhoto, 0),
|
||||
Entitlements: make([]*ScimEntitlement, 0),
|
||||
Roles: make([]*ScimRole, 0),
|
||||
}
|
||||
|
||||
if scimUser.Locale != "" {
|
||||
_, err := language.Parse(scimUser.Locale)
|
||||
if err != nil {
|
||||
logging.OnError(err).Warn("Failed to load locale of scim user")
|
||||
scimUser.Locale = ""
|
||||
}
|
||||
}
|
||||
|
||||
if scimUser.Timezone != "" {
|
||||
_, err := time.LoadLocation(scimUser.Timezone)
|
||||
if err != nil {
|
||||
logging.OnError(err).Warn("Failed to load timezone of scim user")
|
||||
scimUser.Timezone = ""
|
||||
}
|
||||
}
|
||||
|
||||
if err := extractJsonMetadata(ctx, md, metadata.KeyIms, &scimUser.Ims); err != nil {
|
||||
logging.OnError(err).Warn("Could not deserialize scim ims metadata")
|
||||
}
|
||||
|
||||
if err := extractJsonMetadata(ctx, md, metadata.KeyAddresses, &scimUser.Addresses); err != nil {
|
||||
logging.OnError(err).Warn("Could not deserialize scim addresses metadata")
|
||||
}
|
||||
|
||||
if err := extractJsonMetadata(ctx, md, metadata.KeyPhotos, &scimUser.Photos); err != nil {
|
||||
logging.OnError(err).Warn("Could not deserialize scim photos metadata")
|
||||
}
|
||||
|
||||
if err := extractJsonMetadata(ctx, md, metadata.KeyEntitlements, &scimUser.Entitlements); err != nil {
|
||||
logging.OnError(err).Warn("Could not deserialize scim entitlements metadata")
|
||||
}
|
||||
|
||||
if err := extractJsonMetadata(ctx, md, metadata.KeyRoles, &scimUser.Roles); err != nil {
|
||||
logging.OnError(err).Warn("Could not deserialize scim roles metadata")
|
||||
}
|
||||
|
||||
if user.Human != nil {
|
||||
mapHumanToScimUser(ctx, user.Human, scimUser, md)
|
||||
}
|
||||
|
||||
return scimUser
|
||||
}
|
||||
|
||||
func mapHumanToScimUser(ctx context.Context, human *query.Human, user *ScimUser, md map[metadata.ScopedKey][]byte) {
|
||||
user.DisplayName = human.DisplayName
|
||||
user.NickName = human.NickName
|
||||
user.PreferredLanguage = human.PreferredLanguage
|
||||
user.Name = &ScimUserName{
|
||||
Formatted: human.DisplayName,
|
||||
FamilyName: human.LastName,
|
||||
GivenName: human.FirstName,
|
||||
MiddleName: extractScalarMetadata(ctx, md, metadata.KeyMiddleName),
|
||||
HonorificPrefix: extractScalarMetadata(ctx, md, metadata.KeyHonorificPrefix),
|
||||
HonorificSuffix: extractScalarMetadata(ctx, md, metadata.KeyHonorificSuffix),
|
||||
}
|
||||
|
||||
if string(human.Email) != "" {
|
||||
user.Emails = []*ScimEmail{
|
||||
{
|
||||
Value: string(human.Email),
|
||||
Primary: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if string(human.Phone) != "" {
|
||||
user.PhoneNumbers = []*ScimPhoneNumber{
|
||||
{
|
||||
Value: string(human.Phone),
|
||||
Primary: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UsersHandler) buildResourceForQuery(ctx context.Context, user *query.User) *Resource {
|
||||
return &Resource{
|
||||
Schemas: []schemas.ScimSchemaType{schemas.IdUser},
|
||||
Meta: &ResourceMeta{
|
||||
ResourceType: schemas.UserResourceType,
|
||||
Created: user.CreationDate.UTC(),
|
||||
LastModified: user.ChangeDate.UTC(),
|
||||
Version: strconv.FormatUint(user.Sequence, 10),
|
||||
Location: buildLocation(ctx, h, user.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership {
|
||||
cascades := make([]*command.CascadingMembership, len(memberships))
|
||||
for i, membership := range memberships {
|
||||
|
@@ -12,9 +12,49 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/serrors"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map[metadata.ScopedKey][]byte, error) {
|
||||
queries := h.buildMetadataQueries(ctx)
|
||||
|
||||
md, err := h.query.SearchUserMetadata(ctx, false, id, queries, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadataMap := make(map[metadata.ScopedKey][]byte, len(md.Metadata))
|
||||
for _, entry := range md.Metadata {
|
||||
metadataMap[metadata.ScopedKey(entry.Key)] = entry.Value
|
||||
}
|
||||
|
||||
return metadataMap, nil
|
||||
}
|
||||
|
||||
func (h *UsersHandler) buildMetadataQueries(ctx context.Context) *query.UserMetadataSearchQueries {
|
||||
keyQueries := make([]query.SearchQuery, len(metadata.ScimUserRelevantMetadataKeys))
|
||||
for i, key := range metadata.ScimUserRelevantMetadataKeys {
|
||||
keyQueries[i] = buildMetadataKeyQuery(ctx, key)
|
||||
}
|
||||
|
||||
queries := &query.UserMetadataSearchQueries{
|
||||
SearchRequest: query.SearchRequest{},
|
||||
Queries: []query.SearchQuery{query.Or(keyQueries...)},
|
||||
}
|
||||
return queries
|
||||
}
|
||||
|
||||
func buildMetadataKeyQuery(ctx context.Context, key metadata.Key) query.SearchQuery {
|
||||
scopedKey := metadata.ScopeKey(ctx, key)
|
||||
q, err := query.NewUserMetadataKeySearchQuery(string(scopedKey), query.TextEquals)
|
||||
if err != nil {
|
||||
logging.Panic("Error build user metadata query for key " + key)
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (h *UsersHandler) mapMetadataToCommands(ctx context.Context, user *ScimUser) ([]*command.AddMetadataEntry, error) {
|
||||
md := make([]*command.AddMetadataEntry, 0, len(metadata.ScimUserRelevantMetadataKeys))
|
||||
for _, key := range metadata.ScimUserRelevantMetadataKeys {
|
||||
@@ -51,7 +91,17 @@ func getValueForMetadataKey(user *ScimUser, key metadata.Key) ([]byte, error) {
|
||||
case metadata.KeyAddresses:
|
||||
fallthrough
|
||||
case metadata.KeyRoles:
|
||||
return json.Marshal(value)
|
||||
val, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// null is considered no value
|
||||
if len(val) == 4 && string(val) == "null" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return val, nil
|
||||
|
||||
// http url values
|
||||
case metadata.KeyProfileUrl:
|
||||
@@ -148,3 +198,36 @@ func getRawValueForMetadataKey(user *ScimUser, key metadata.Key) interface{} {
|
||||
logging.Panicf("Unknown or unsupported metadata key %s", key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractScalarMetadata(ctx context.Context, md map[metadata.ScopedKey][]byte, key metadata.Key) string {
|
||||
val, ok := md[metadata.ScopeKey(ctx, key)]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(val)
|
||||
}
|
||||
|
||||
func extractHttpURLMetadata(ctx context.Context, md map[metadata.ScopedKey][]byte, key metadata.Key) *schemas.HttpURL {
|
||||
val, ok := md[metadata.ScopeKey(ctx, key)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
url, err := schemas.ParseHTTPURL(string(val))
|
||||
if err != nil {
|
||||
logging.OnError(err).Warn("Failed to parse scim url metadata for " + key)
|
||||
return nil
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
func extractJsonMetadata(ctx context.Context, md map[metadata.ScopedKey][]byte, key metadata.Key, v interface{}) error {
|
||||
val, ok := md[metadata.ScopeKey(ctx, key)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(val, v)
|
||||
}
|
||||
|
Reference in New Issue
Block a user