mirror of
https://github.com/zitadel/zitadel.git
synced 2025-03-01 14:27:24 +00:00

# Which Problems Are Solved - Adds support for the list users SCIM v2 endpoint # How the Problems Are Solved - Adds support for the list users SCIM v2 endpoints under `GET /scim/v2/{orgID}/Users` and `POST /scim/v2/{orgID}/Users/.search` # Additional Changes - adds a new function `SearchUserMetadataForUsers` to the query layer to query a metadata keyset for given user ids - adds a new function `NewUserMetadataExistsQuery` to the query layer to query a given metadata key value pair exists - adds a new function `CountUsers` to the query layer to count users without reading any rows - handle `ErrorAlreadyExists` as scim errors `uniqueness` - adds `NumberLessOrEqual` and `NumberGreaterOrEqual` query comparison methods - adds `BytesQuery` with `BytesEquals` and `BytesNotEquals` query comparison methods # Additional Context Part of #8140 Supported fields for scim filters: * `meta.created` * `meta.lastModified` * `id` * `username` * `name.familyName` * `name.givenName` * `emails` and `emails.value` * `active` only eq and ne * `externalId` only eq and ne
242 lines
7.2 KiB
Go
242 lines
7.2 KiB
Go
package resources
|
|
|
|
import (
|
|
"context"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
scim_config "github.com/zitadel/zitadel/internal/api/scim/config"
|
|
scim_schemas "github.com/zitadel/zitadel/internal/api/scim/schemas"
|
|
"github.com/zitadel/zitadel/internal/command"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
)
|
|
|
|
type UsersHandler struct {
|
|
command *command.Commands
|
|
query *query.Queries
|
|
userCodeAlg crypto.EncryptionAlgorithm
|
|
config *scim_config.Config
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
type ScimEntitlement struct {
|
|
Value string `json:"value,omitempty"`
|
|
Display string `json:"display,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Primary bool `json:"primary,omitempty"`
|
|
}
|
|
|
|
type ScimRole struct {
|
|
Value string `json:"value,omitempty"`
|
|
Display string `json:"display,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Primary bool `json:"primary,omitempty"`
|
|
}
|
|
|
|
type ScimPhoto struct {
|
|
Value scim_schemas.HttpURL `json:"value"`
|
|
Display string `json:"display,omitempty"`
|
|
Type string `json:"type"`
|
|
Primary bool `json:"primary,omitempty"`
|
|
}
|
|
|
|
type ScimAddress struct {
|
|
Type string `json:"type,omitempty"`
|
|
StreetAddress string `json:"streetAddress,omitempty"`
|
|
Locality string `json:"locality,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
PostalCode string `json:"postalCode,omitempty"`
|
|
Country string `json:"country,omitempty"`
|
|
Formatted string `json:"formatted,omitempty"`
|
|
Primary bool `json:"primary,omitempty"`
|
|
}
|
|
|
|
type ScimIms struct {
|
|
Value string `json:"value"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type ScimEmail struct {
|
|
Value string `json:"value"`
|
|
Primary bool `json:"primary"`
|
|
}
|
|
|
|
type ScimPhoneNumber struct {
|
|
Value string `json:"value"`
|
|
Primary bool `json:"primary"`
|
|
}
|
|
|
|
type ScimUserName struct {
|
|
Formatted string `json:"formatted,omitempty"`
|
|
FamilyName string `json:"familyName,omitempty"`
|
|
GivenName string `json:"givenName,omitempty"`
|
|
MiddleName string `json:"middleName,omitempty"`
|
|
HonorificPrefix string `json:"honorificPrefix,omitempty"`
|
|
HonorificSuffix string `json:"honorificSuffix,omitempty"`
|
|
}
|
|
|
|
func NewUsersHandler(
|
|
command *command.Commands,
|
|
query *query.Queries,
|
|
userCodeAlg crypto.EncryptionAlgorithm,
|
|
config *scim_config.Config) ResourceHandler[*ScimUser] {
|
|
return &UsersHandler{command, query, userCodeAlg, config}
|
|
}
|
|
|
|
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 {
|
|
return u.Resource
|
|
}
|
|
|
|
func (h *UsersHandler) NewResource() *ScimUser {
|
|
return new(ScimUser)
|
|
}
|
|
|
|
func (h *UsersHandler) SchemaType() scim_schemas.ScimSchemaType {
|
|
return scim_schemas.IdUser
|
|
}
|
|
|
|
func (h *UsersHandler) Create(ctx context.Context, user *ScimUser) (*ScimUser, error) {
|
|
orgID := authz.GetCtxData(ctx).OrgID
|
|
addHuman, err := h.mapToAddHuman(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = h.command.AddUserHuman(ctx, orgID, addHuman, true, h.userCodeAlg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
h.mapAddCommandToScimUser(ctx, user, addHuman)
|
|
return user, nil
|
|
}
|
|
|
|
func (h *UsersHandler) Replace(ctx context.Context, id string, user *ScimUser) (*ScimUser, error) {
|
|
user.ID = id
|
|
changeHuman, err := h.mapToChangeHuman(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = h.command.ChangeUserHuman(ctx, changeHuman, h.userCodeAlg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
h.mapChangeCommandToScimUser(ctx, user, changeHuman)
|
|
return user, nil
|
|
}
|
|
|
|
func (h *UsersHandler) Delete(ctx context.Context, id string) error {
|
|
memberships, grants, err := h.queryUserDependencies(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = h.command.RemoveUserV2(ctx, id, memberships, grants...)
|
|
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) List(ctx context.Context, request *ListRequest) (*ListResponse[*ScimUser], error) {
|
|
q, err := h.buildListQuery(ctx, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if request.Count == 0 {
|
|
count, err := h.query.CountUsers(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newListResponse(count, q.SearchRequest, make([]*ScimUser, 0)), nil
|
|
}
|
|
|
|
users, err := h.query.SearchUsers(ctx, q, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metadata, err := h.queryMetadataForUsers(ctx, usersToIDs(users.Users))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scimUsers := h.mapToScimUsers(ctx, users.Users, metadata)
|
|
return newListResponse(users.SearchResponse.Count, q.SearchRequest, scimUsers), nil
|
|
}
|
|
|
|
func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) {
|
|
userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
grants, err := h.query.UserGrants(ctx, &query.UserGrantsQueries{
|
|
Queries: []query.SearchQuery{userGrantUserQuery},
|
|
}, true)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
memberships, err := h.query.Memberships(ctx, &query.MembershipSearchQuery{
|
|
Queries: []query.SearchQuery{membershipsUserQuery},
|
|
}, false)
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), nil
|
|
}
|