mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 12:37:39 +00:00
fix(scim): add a metadata config to ignore random password sent during SCIM create (#10296)
<!-- Please inform yourself about the contribution guidelines on submitting a PR here: https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr. Take note of how PR/commit titles should be written and replace the template texts in the sections below. Don't remove any of the sections. It is important that the commit history clearly shows what is changed and why. Important: By submitting a contribution you agree to the terms from our Licensing Policy as described here: https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions. --> # Which Problems Are Solved Okta sends a random password in the request to create a user during SCIM provisioning, irrespective of whether the `Sync Password` option is enabled or disabled on Okta, and this password does not comply with the default password complexity set in Zitadel. This PR adds a workaround to create users without issues in such cases. # How the Problems Are Solved - A new metadata configuration called `urn:zitadel:scim:ignorePasswordOnCreate` is added to the Machine User that is used for provisioning - During SCIM user creation requests, if the `urn:zitadel:scim:ignorePasswordOnCreate` is set to `true` in the Machine User's metadata, the password set in the create request is ignored # Additional Changes # Additional Context The random password is ignored (if set in the metadata) only during customer creation. This change does not affect SCIM password updates. - Closes #10009 --------- Co-authored-by: Marco A. <marco@zitadel.com>
This commit is contained in:

committed by
Tim Möhlmann

parent
213f15164d
commit
c446cd27c9
@@ -426,3 +426,78 @@ func TestCreateUser_scopedExternalID(t *testing.T) {
|
|||||||
assert.Equal(tt, "701984", string(md.Metadata.Value))
|
assert.Equal(tt, "701984", string(md.Metadata.Value))
|
||||||
}, retryDuration, tick)
|
}, retryDuration, tick)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateUser_ignorePasswordOnCreate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ignorePassword string
|
||||||
|
scimErrorType string
|
||||||
|
scimErrorDetail string
|
||||||
|
wantUser *resources.ScimUser
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ignorePasswordOnCreate set to false",
|
||||||
|
ignorePassword: "false",
|
||||||
|
wantErr: true,
|
||||||
|
scimErrorType: "invalidValue",
|
||||||
|
scimErrorDetail: "Password is too short",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignorePasswordOnCreate set to an invalid value",
|
||||||
|
ignorePassword: "random",
|
||||||
|
wantErr: true,
|
||||||
|
scimErrorType: "invalidValue",
|
||||||
|
scimErrorDetail: "Invalid value for metadata key urn:zitadel:scim:ignorePasswordOnCreate: random",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignorePasswordOnCreate set to true",
|
||||||
|
ignorePassword: "true",
|
||||||
|
wantUser: &resources.ScimUser{
|
||||||
|
UserName: "acmeUser1",
|
||||||
|
Name: &resources.ScimUserName{
|
||||||
|
FamilyName: "Ross",
|
||||||
|
GivenName: "Bethany",
|
||||||
|
},
|
||||||
|
Emails: []*resources.ScimEmail{
|
||||||
|
{
|
||||||
|
Value: "user1@example.com",
|
||||||
|
Primary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// create a machine user
|
||||||
|
callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
|
||||||
|
require.NoError(t, err)
|
||||||
|
ctx := integration.WithAuthorizationToken(CTX, callingUserPat)
|
||||||
|
|
||||||
|
// set urn:zitadel:scim:ignorePasswordOnCreate metadata for the machine user
|
||||||
|
setAndEnsureMetadata(t, callingUserId, "urn:zitadel:scim:ignorePasswordOnCreate", tt.ignorePassword)
|
||||||
|
|
||||||
|
// create a user with an invalid password
|
||||||
|
createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, withUsername(invalidPasswordUserJson, "acmeUser1"))
|
||||||
|
require.Equal(t, tt.wantErr, err != nil)
|
||||||
|
if err != nil {
|
||||||
|
scimErr := scim.RequireScimError(t, http.StatusBadRequest, err)
|
||||||
|
assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType)
|
||||||
|
assert.Equal(t, tt.scimErrorDetail, scimErr.Error.Detail)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||||
|
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||||
|
// ensure the user is really stored and not just returned to the caller
|
||||||
|
fetchedUser, err := Instance.Client.SCIM.Users.Get(CTX, Instance.DefaultOrg.Id, createdUser.ID)
|
||||||
|
require.NoError(ttt, err)
|
||||||
|
assert.True(ttt, test.PartiallyDeepEqual(tt.wantUser, fetchedUser))
|
||||||
|
}, retryDuration, tick)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -15,6 +15,7 @@ var scimContextKey scimContextKeyType
|
|||||||
|
|
||||||
type ScimContextData struct {
|
type ScimContextData struct {
|
||||||
ProvisioningDomain string
|
ProvisioningDomain string
|
||||||
|
IgnorePasswordOnCreate bool
|
||||||
ExternalIDScopedMetadataKey ScopedKey
|
ExternalIDScopedMetadataKey ScopedKey
|
||||||
bulkIDMapping map[string]string
|
bulkIDMapping map[string]string
|
||||||
}
|
}
|
||||||
|
@@ -13,8 +13,9 @@ type ScopedKey string
|
|||||||
const (
|
const (
|
||||||
externalIdProvisioningDomainPlaceholder = "{provisioningDomain}"
|
externalIdProvisioningDomainPlaceholder = "{provisioningDomain}"
|
||||||
|
|
||||||
KeyPrefix = "urn:zitadel:scim:"
|
KeyPrefix = "urn:zitadel:scim:"
|
||||||
KeyProvisioningDomain Key = KeyPrefix + "provisioningDomain"
|
KeyProvisioningDomain Key = KeyPrefix + "provisioningDomain"
|
||||||
|
KeyIgnorePasswordOnCreate Key = KeyPrefix + "ignorePasswordOnCreate"
|
||||||
|
|
||||||
KeyExternalId Key = KeyPrefix + "externalId"
|
KeyExternalId Key = KeyPrefix + "externalId"
|
||||||
keyScopedExternalIdTemplate = KeyPrefix + externalIdProvisioningDomainPlaceholder + ":externalId"
|
keyScopedExternalIdTemplate = KeyPrefix + externalIdProvisioningDomainPlaceholder + ":externalId"
|
||||||
|
@@ -3,10 +3,15 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
zhttp "github.com/zitadel/zitadel/internal/api/http/middleware"
|
zhttp "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||||
smetadata "github.com/zitadel/zitadel/internal/api/scim/metadata"
|
smetadata "github.com/zitadel/zitadel/internal/api/scim/metadata"
|
||||||
|
sresources "github.com/zitadel/zitadel/internal/api/scim/resources"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
)
|
)
|
||||||
@@ -29,22 +34,43 @@ func initScimContext(ctx context.Context, q *query.Queries) (context.Context, er
|
|||||||
ctx = smetadata.SetScimContextData(ctx, data)
|
ctx = smetadata.SetScimContextData(ctx, data)
|
||||||
|
|
||||||
userID := authz.GetCtxData(ctx).UserID
|
userID := authz.GetCtxData(ctx).UserID
|
||||||
metadata, err := q.GetUserMetadataByKey(ctx, false, userID, string(smetadata.KeyProvisioningDomain), false)
|
|
||||||
|
// get the provisioningDomain and ignorePasswordOnCreate metadata keys associated with the service user
|
||||||
|
metadataKeys := []smetadata.Key{
|
||||||
|
smetadata.KeyProvisioningDomain,
|
||||||
|
smetadata.KeyIgnorePasswordOnCreate,
|
||||||
|
}
|
||||||
|
queries := sresources.BuildMetadataQueries(ctx, metadataKeys)
|
||||||
|
|
||||||
|
metadataList, err := q.SearchUserMetadata(ctx, false, userID, queries, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if zerrors.IsNotFound(err) {
|
if zerrors.IsNotFound(err) {
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx, err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata == nil {
|
if metadataList == nil || len(metadataList.Metadata) == 0 {
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data.ProvisioningDomain = string(metadata.Value)
|
for _, metadata := range metadataList.Metadata {
|
||||||
if data.ProvisioningDomain != "" {
|
switch metadata.Key {
|
||||||
data.ExternalIDScopedMetadataKey = smetadata.ScopeExternalIdKey(data.ProvisioningDomain)
|
case string(smetadata.KeyProvisioningDomain):
|
||||||
|
data.ProvisioningDomain = string(metadata.Value)
|
||||||
|
if data.ProvisioningDomain != "" {
|
||||||
|
data.ExternalIDScopedMetadataKey = smetadata.ScopeExternalIdKey(data.ProvisioningDomain)
|
||||||
|
}
|
||||||
|
case string(smetadata.KeyIgnorePasswordOnCreate):
|
||||||
|
ignorePasswordOnCreate, parseErr := strconv.ParseBool(strings.TrimSpace(string(metadata.Value)))
|
||||||
|
if parseErr != nil {
|
||||||
|
return ctx,
|
||||||
|
zerrors.ThrowInvalidArgumentf(nil, "SMCM-yvw2rt", "Invalid value for metadata key %s: %s", smetadata.KeyIgnorePasswordOnCreate, metadata.Value)
|
||||||
|
}
|
||||||
|
data.IgnorePasswordOnCreate = ignorePasswordOnCreate
|
||||||
|
default:
|
||||||
|
logging.WithFields("user_metadata_key", metadata.Key).Warn("unexpected metadata key")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return smetadata.SetScimContextData(ctx, data), nil
|
return smetadata.SetScimContextData(ctx, data), nil
|
||||||
}
|
}
|
||||||
|
@@ -45,7 +45,12 @@ func (h *UsersHandler) mapToAddHuman(ctx context.Context, scimUser *ScimUser) (*
|
|||||||
}
|
}
|
||||||
human.Metadata = md
|
human.Metadata = md
|
||||||
|
|
||||||
if scimUser.Password != nil {
|
// Okta sends a random password during SCIM provisioning
|
||||||
|
// irrespective of whether the Sync Password option is enabled or disabled on Okta.
|
||||||
|
// This password does not comply with Zitadel's password complexity, and
|
||||||
|
// the following workaround ignores the random password as it does not add any value.
|
||||||
|
ignorePasswordOnCreate := metadata.GetScimContextData(ctx).IgnorePasswordOnCreate
|
||||||
|
if scimUser.Password != nil && !ignorePasswordOnCreate {
|
||||||
human.Password = scimUser.Password.String()
|
human.Password = scimUser.Password.String()
|
||||||
scimUser.Password = nil
|
scimUser.Password = nil
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []string) (map[string]map[metadata.ScopedKey][]byte, error) {
|
func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []string) (map[string]map[metadata.ScopedKey][]byte, error) {
|
||||||
queries := h.buildMetadataQueries(ctx)
|
queries := BuildMetadataQueries(ctx, metadata.ScimUserRelevantMetadataKeys)
|
||||||
|
|
||||||
md, err := h.query.SearchUserMetadataForUsers(ctx, false, userIds, queries)
|
md, err := h.query.SearchUserMetadataForUsers(ctx, false, userIds, queries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -43,7 +43,7 @@ func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map[metadata.ScopedKey][]byte, error) {
|
func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map[metadata.ScopedKey][]byte, error) {
|
||||||
queries := h.buildMetadataQueries(ctx)
|
queries := BuildMetadataQueries(ctx, metadata.ScimUserRelevantMetadataKeys)
|
||||||
|
|
||||||
md, err := h.query.SearchUserMetadata(ctx, false, id, queries, false)
|
md, err := h.query.SearchUserMetadata(ctx, false, id, queries, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,9 +53,9 @@ func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map
|
|||||||
return metadata.MapListToScopedKeyMap(md.Metadata), nil
|
return metadata.MapListToScopedKeyMap(md.Metadata), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) buildMetadataQueries(ctx context.Context) *query.UserMetadataSearchQueries {
|
func BuildMetadataQueries(ctx context.Context, metadataKeys []metadata.Key) *query.UserMetadataSearchQueries {
|
||||||
keyQueries := make([]query.SearchQuery, len(metadata.ScimUserRelevantMetadataKeys))
|
keyQueries := make([]query.SearchQuery, len(metadataKeys))
|
||||||
for i, key := range metadata.ScimUserRelevantMetadataKeys {
|
for i, key := range metadataKeys {
|
||||||
keyQueries[i] = buildMetadataKeyQuery(ctx, key)
|
keyQueries[i] = buildMetadataKeyQuery(ctx, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user