Merge branch 'main' into dev-container-2

This commit is contained in:
Elio Bischof
2025-07-23 12:26:58 +02:00
committed by GitHub
6 changed files with 122 additions and 14 deletions

View File

@@ -432,3 +432,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)
})
}
}

View File

@@ -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
} }

View File

@@ -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"

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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, nil) md, err := h.query.SearchUserMetadata(ctx, false, id, queries, nil)
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)
} }