mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:47:32 +00:00
fix: scim v2 endpoints enforce user resource owner (#9273)
# Which Problems Are Solved - If a SCIM endpoint is called with an orgID in the URL that is not the resource owner, no error is returned, and the action is executed. # How the Problems Are Solved - The orgID provided in the SCIM URL path must match the resource owner of the target user. Otherwise, an error will be returned. # Additional Context Part of https://github.com/zitadel/zitadel/issues/8140
This commit is contained in:
@@ -275,7 +275,7 @@ func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
details, err := s.command.RemoveUserV2(ctx, req.UserId, memberships, grants...)
|
details, err := s.command.RemoveUserV2(ctx, req.UserId, "", memberships, grants...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -278,7 +278,7 @@ func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
details, err := s.command.RemoveUserV2(ctx, req.UserId, memberships, grants...)
|
details, err := s.command.RemoveUserV2(ctx, req.UserId, "", memberships, grants...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -9,12 +9,16 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Instance *integration.Instance
|
Instance *integration.Instance
|
||||||
CTX context.Context
|
SecondaryOrganization *org.AddOrganizationResponse
|
||||||
|
CTX context.Context
|
||||||
|
|
||||||
// remove comments in the json, as the default golang json unmarshaler cannot handle them
|
// remove comments in the json, as the default golang json unmarshaler cannot handle them
|
||||||
// some test files (e.g. bulk, patch) are much easier to maintain with comments
|
// some test files (e.g. bulk, patch) are much easier to maintain with comments
|
||||||
@@ -29,6 +33,10 @@ func TestMain(m *testing.M) {
|
|||||||
Instance = integration.NewInstance(ctx)
|
Instance = integration.NewInstance(ctx)
|
||||||
|
|
||||||
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
|
CTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)
|
||||||
|
|
||||||
|
iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||||
|
SecondaryOrganization = Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email())
|
||||||
|
|
||||||
return m.Run()
|
return m.Run()
|
||||||
}())
|
}())
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/brianvoe/gofakeit/v6"
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -164,6 +163,7 @@ func TestCreateUser(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
body []byte
|
body []byte
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
orgID string
|
||||||
want *resources.ScimUser
|
want *resources.ScimUser
|
||||||
wantErr bool
|
wantErr bool
|
||||||
scimErrorType string
|
scimErrorType string
|
||||||
@@ -275,6 +275,13 @@ func TestCreateUser(t *testing.T) {
|
|||||||
wantErr: true,
|
wantErr: true,
|
||||||
errorStatus: http.StatusNotFound,
|
errorStatus: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "another org",
|
||||||
|
body: minimalUserJson,
|
||||||
|
orgID: SecondaryOrganization.OrganizationId,
|
||||||
|
wantErr: true,
|
||||||
|
errorStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -283,7 +290,12 @@ func TestCreateUser(t *testing.T) {
|
|||||||
ctx = CTX
|
ctx = CTX
|
||||||
}
|
}
|
||||||
|
|
||||||
createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, tt.body)
|
orgID := tt.orgID
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = Instance.DefaultOrg.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
createdUser, err := Instance.Client.SCIM.Users.Create(ctx, orgID, tt.body)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("CreateUser() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("CreateUser() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
@@ -311,7 +323,7 @@ func TestCreateUser(t *testing.T) {
|
|||||||
|
|
||||||
assert.EqualValues(t, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, createdUser.Resource.Schemas)
|
assert.EqualValues(t, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, createdUser.Resource.Schemas)
|
||||||
assert.Equal(t, schemas.ScimResourceTypeSingular("User"), createdUser.Resource.Meta.ResourceType)
|
assert.Equal(t, schemas.ScimResourceTypeSingular("User"), createdUser.Resource.Meta.ResourceType)
|
||||||
assert.Equal(t, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, Instance.DefaultOrg.Id, "Users", createdUser.ID), createdUser.Resource.Meta.Location)
|
assert.Equal(t, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", createdUser.ID), createdUser.Resource.Meta.Location)
|
||||||
assert.Nil(t, createdUser.Password)
|
assert.Nil(t, createdUser.Password)
|
||||||
|
|
||||||
if tt.want != nil {
|
if tt.want != nil {
|
||||||
@@ -423,9 +435,3 @@ 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_anotherOrg(t *testing.T) {
|
|
||||||
org := Instance.CreateOrganization(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), gofakeit.Name(), gofakeit.Email())
|
|
||||||
_, err := Instance.Client.SCIM.Users.Create(CTX, org.OrganizationId, fullUserJson)
|
|
||||||
scim.RequireScimError(t, http.StatusNotFound, err)
|
|
||||||
}
|
|
||||||
|
@@ -8,7 +8,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/brianvoe/gofakeit/v6"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -22,6 +21,7 @@ func TestDeleteUser_errors(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
orgID string
|
||||||
errorStatus int
|
errorStatus int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -38,6 +38,17 @@ func TestDeleteUser_errors(t *testing.T) {
|
|||||||
name: "unknown user id",
|
name: "unknown user id",
|
||||||
errorStatus: http.StatusNotFound,
|
errorStatus: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "another org",
|
||||||
|
orgID: SecondaryOrganization.OrganizationId,
|
||||||
|
errorStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "another org with permissions",
|
||||||
|
orgID: SecondaryOrganization.OrganizationId,
|
||||||
|
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
|
||||||
|
errorStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -46,7 +57,11 @@ func TestDeleteUser_errors(t *testing.T) {
|
|||||||
ctx = CTX
|
ctx = CTX
|
||||||
}
|
}
|
||||||
|
|
||||||
err := Instance.Client.SCIM.Users.Delete(ctx, Instance.DefaultOrg.Id, "1")
|
orgID := tt.orgID
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = Instance.DefaultOrg.Id
|
||||||
|
}
|
||||||
|
err := Instance.Client.SCIM.Users.Delete(ctx, orgID, "1")
|
||||||
|
|
||||||
statusCode := tt.errorStatus
|
statusCode := tt.errorStatus
|
||||||
if statusCode == 0 {
|
if statusCode == 0 {
|
||||||
@@ -81,10 +96,3 @@ func TestDeleteUser_ensureReallyDeleted(t *testing.T) {
|
|||||||
integration.AssertGrpcStatus(tt, codes.NotFound, err)
|
integration.AssertGrpcStatus(tt, codes.NotFound, err)
|
||||||
}, retryDuration, tick)
|
}, retryDuration, tick)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteUser_anotherOrg(t *testing.T) {
|
|
||||||
createUserResp := Instance.CreateHumanUser(CTX)
|
|
||||||
org := Instance.CreateOrganization(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), gofakeit.Name(), gofakeit.Email())
|
|
||||||
err := Instance.Client.SCIM.Users.Delete(CTX, org.OrganizationId, createUserResp.UserId)
|
|
||||||
scim.RequireScimError(t, http.StatusNotFound, err)
|
|
||||||
}
|
|
||||||
|
@@ -9,7 +9,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/brianvoe/gofakeit/v6"
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -27,6 +26,7 @@ import (
|
|||||||
func TestGetUser(t *testing.T) {
|
func TestGetUser(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
orgID string
|
||||||
buildUserID func() string
|
buildUserID func() string
|
||||||
cleanup func(userID string)
|
cleanup func(userID string)
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -46,6 +46,19 @@ func TestGetUser(t *testing.T) {
|
|||||||
errorStatus: http.StatusNotFound,
|
errorStatus: http.StatusNotFound,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "another org",
|
||||||
|
orgID: SecondaryOrganization.OrganizationId,
|
||||||
|
errorStatus: http.StatusNotFound,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "another org with permissions",
|
||||||
|
orgID: SecondaryOrganization.OrganizationId,
|
||||||
|
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
|
||||||
|
errorStatus: http.StatusNotFound,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "unknown user id",
|
name: "unknown user id",
|
||||||
buildUserID: func() string {
|
buildUserID: func() string {
|
||||||
@@ -237,11 +250,16 @@ func TestGetUser(t *testing.T) {
|
|||||||
userID = createUserResp.UserId
|
userID = createUserResp.UserId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orgID := tt.orgID
|
||||||
|
if orgID == "" {
|
||||||
|
orgID = Instance.DefaultOrg.Id
|
||||||
|
}
|
||||||
|
|
||||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||||
var fetchedUser *resources.ScimUser
|
var fetchedUser *resources.ScimUser
|
||||||
var err error
|
var err error
|
||||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||||
fetchedUser, err = Instance.Client.SCIM.Users.Get(ctx, Instance.DefaultOrg.Id, userID)
|
fetchedUser, err = Instance.Client.SCIM.Users.Get(ctx, orgID, userID)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
statusCode := tt.errorStatus
|
statusCode := tt.errorStatus
|
||||||
if statusCode == 0 {
|
if statusCode == 0 {
|
||||||
@@ -255,7 +273,7 @@ func TestGetUser(t *testing.T) {
|
|||||||
assert.Equal(ttt, userID, fetchedUser.ID)
|
assert.Equal(ttt, userID, fetchedUser.ID)
|
||||||
assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas)
|
assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas)
|
||||||
assert.Equal(ttt, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType)
|
assert.Equal(ttt, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType)
|
||||||
assert.Equal(ttt, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, Instance.DefaultOrg.Id, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location)
|
assert.Equal(ttt, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location)
|
||||||
assert.Nil(ttt, fetchedUser.Password)
|
assert.Nil(ttt, fetchedUser.Password)
|
||||||
if !test.PartiallyDeepEqual(tt.want, fetchedUser) {
|
if !test.PartiallyDeepEqual(tt.want, fetchedUser) {
|
||||||
ttt.Errorf("GetUser() got = %#v, want %#v", fetchedUser, tt.want)
|
ttt.Errorf("GetUser() got = %#v, want %#v", fetchedUser, tt.want)
|
||||||
@@ -268,10 +286,3 @@ func TestGetUser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUser_anotherOrg(t *testing.T) {
|
|
||||||
createUserResp := Instance.CreateHumanUser(CTX)
|
|
||||||
org := Instance.CreateOrganization(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), gofakeit.Name(), gofakeit.Email())
|
|
||||||
_, err := Instance.Client.SCIM.Users.Get(CTX, org.OrganizationId, createUserResp.UserId)
|
|
||||||
scim.RequireScimError(t, http.StatusNotFound, err)
|
|
||||||
}
|
|
||||||
|
@@ -37,14 +37,16 @@ var (
|
|||||||
|
|
||||||
func TestReplaceUser(t *testing.T) {
|
func TestReplaceUser(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
body []byte
|
body []byte
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
want *resources.ScimUser
|
createUserOrgID string
|
||||||
wantErr bool
|
replaceUserOrgID string
|
||||||
scimErrorType string
|
want *resources.ScimUser
|
||||||
errorStatus int
|
wantErr bool
|
||||||
zitadelErrID string
|
scimErrorType string
|
||||||
|
errorStatus int
|
||||||
|
zitadelErrID string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "minimal user",
|
name: "minimal user",
|
||||||
@@ -207,10 +209,26 @@ func TestReplaceUser(t *testing.T) {
|
|||||||
wantErr: true,
|
wantErr: true,
|
||||||
errorStatus: http.StatusNotFound,
|
errorStatus: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "another org",
|
||||||
|
body: minimalUserJson,
|
||||||
|
replaceUserOrgID: SecondaryOrganization.OrganizationId,
|
||||||
|
wantErr: true,
|
||||||
|
errorStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "another org with permissions",
|
||||||
|
body: minimalUserJson,
|
||||||
|
replaceUserOrgID: SecondaryOrganization.OrganizationId,
|
||||||
|
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
|
||||||
|
wantErr: true,
|
||||||
|
errorStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
|
// use iam owner => we don't want to test permissions of the create endpoint.
|
||||||
|
createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, fullUserJson)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -223,7 +241,12 @@ func TestReplaceUser(t *testing.T) {
|
|||||||
ctx = CTX
|
ctx = CTX
|
||||||
}
|
}
|
||||||
|
|
||||||
replacedUser, err := Instance.Client.SCIM.Users.Replace(ctx, Instance.DefaultOrg.Id, createdUser.ID, tt.body)
|
replaceUserOrgID := tt.replaceUserOrgID
|
||||||
|
if replaceUserOrgID == "" {
|
||||||
|
replaceUserOrgID = Instance.DefaultOrg.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
replacedUser, err := Instance.Client.SCIM.Users.Replace(ctx, replaceUserOrgID, createdUser.ID, tt.body)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("ReplaceUser() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("ReplaceUser() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/brianvoe/gofakeit/v6"
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -28,7 +27,7 @@ var (
|
|||||||
//go:embed testdata/users_update_test_full.json
|
//go:embed testdata/users_update_test_full.json
|
||||||
fullUserUpdateJson []byte
|
fullUserUpdateJson []byte
|
||||||
|
|
||||||
minimalUserUpdateJson = simpleReplacePatchBody("nickname", "foo")
|
minimalUserUpdateJson = simpleReplacePatchBody("nickname", "\"foo\"")
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -44,9 +43,6 @@ func TestUpdateUser(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
iamOwnerCtx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
|
||||||
secondaryOrg := Instance.CreateOrganization(iamOwnerCtx, gofakeit.Name(), gofakeit.Email())
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
body []byte
|
body []byte
|
||||||
@@ -74,7 +70,15 @@ func TestUpdateUser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "other org",
|
name: "other org",
|
||||||
orgID: secondaryOrg.OrganizationId,
|
orgID: SecondaryOrganization.OrganizationId,
|
||||||
|
body: minimalUserUpdateJson,
|
||||||
|
wantErr: true,
|
||||||
|
errorStatus: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other org with permissions",
|
||||||
|
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
|
||||||
|
orgID: SecondaryOrganization.OrganizationId,
|
||||||
body: minimalUserUpdateJson,
|
body: minimalUserUpdateJson,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
errorStatus: http.StatusNotFound,
|
errorStatus: http.StatusNotFound,
|
||||||
|
@@ -180,7 +180,8 @@ func (h *UsersHandler) Replace(ctx context.Context, id string, user *ScimUser) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) Update(ctx context.Context, id string, operations patch.OperationCollection) error {
|
func (h *UsersHandler) Update(ctx context.Context, id string, operations patch.OperationCollection) error {
|
||||||
userWM, err := h.command.UserHumanWriteModel(ctx, id, true, true, true, true, false, false, true)
|
orgID := authz.GetCtxData(ctx).OrgID
|
||||||
|
userWM, err := h.command.UserHumanWriteModel(ctx, id, orgID, true, true, true, true, false, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -191,6 +192,9 @@ func (h *UsersHandler) Update(ctx context.Context, id string, operations patch.O
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure the identity of the user is not modified
|
||||||
|
changeHuman.ID = id
|
||||||
|
changeHuman.ResourceOwner = orgID
|
||||||
return h.command.ChangeUserHuman(ctx, changeHuman, h.userCodeAlg)
|
return h.command.ChangeUserHuman(ctx, changeHuman, h.userCodeAlg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +204,12 @@ func (h *UsersHandler) Delete(ctx context.Context, id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = h.command.RemoveUserV2(ctx, id, memberships, grants...)
|
_, err = h.command.RemoveUserV2(ctx, id, authz.GetCtxData(ctx).OrgID, memberships, grants...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UsersHandler) Get(ctx context.Context, id string) (*ScimUser, error) {
|
func (h *UsersHandler) Get(ctx context.Context, id string) (*ScimUser, error) {
|
||||||
user, err := h.query.GetUserByID(ctx, false, id)
|
user, err := h.query.GetUserByIDWithResourceOwner(ctx, false, id, authz.GetCtxData(ctx).OrgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/scim/metadata"
|
"github.com/zitadel/zitadel/internal/api/scim/metadata"
|
||||||
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
@@ -73,8 +74,9 @@ func (h *UsersHandler) mapToAddHuman(ctx context.Context, scimUser *ScimUser) (*
|
|||||||
|
|
||||||
func (h *UsersHandler) mapToChangeHuman(ctx context.Context, scimUser *ScimUser) (*command.ChangeHuman, error) {
|
func (h *UsersHandler) mapToChangeHuman(ctx context.Context, scimUser *ScimUser) (*command.ChangeHuman, error) {
|
||||||
human := &command.ChangeHuman{
|
human := &command.ChangeHuman{
|
||||||
ID: scimUser.ID,
|
ID: scimUser.ID,
|
||||||
Username: &scimUser.UserName,
|
ResourceOwner: authz.GetCtxData(ctx).OrgID,
|
||||||
|
Username: &scimUser.UserName,
|
||||||
Profile: &command.Profile{
|
Profile: &command.Profile{
|
||||||
NickName: &scimUser.NickName,
|
NickName: &scimUser.NickName,
|
||||||
DisplayName: &scimUser.DisplayName,
|
DisplayName: &scimUser.DisplayName,
|
||||||
|
@@ -159,12 +159,12 @@ func (c *Commands) userStateWriteModel(ctx context.Context, userID string) (writ
|
|||||||
return writeModel, nil
|
return writeModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) RemoveUserV2(ctx context.Context, userID string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) {
|
func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) {
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing")
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing")
|
||||||
}
|
}
|
||||||
|
|
||||||
existingUser, err := c.userRemoveWriteModel(ctx, userID)
|
existingUser, err := c.userRemoveWriteModel(ctx, userID, resourceOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -210,11 +210,11 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID string, cascadingUse
|
|||||||
return writeModelToObjectDetails(&existingUser.WriteModel), nil
|
return writeModelToObjectDetails(&existingUser.WriteModel), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) userRemoveWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) {
|
func (c *Commands) userRemoveWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *UserV2WriteModel, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
writeModel = NewUserRemoveWriteModel(userID, "")
|
writeModel = NewUserRemoveWriteModel(userID, resourceOwner)
|
||||||
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@@ -14,12 +14,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ChangeHuman struct {
|
type ChangeHuman struct {
|
||||||
ID string
|
ID string
|
||||||
State *domain.UserState
|
ResourceOwner string
|
||||||
Username *string
|
State *domain.UserState
|
||||||
Profile *Profile
|
Username *string
|
||||||
Email *Email
|
Profile *Profile
|
||||||
Phone *Phone
|
Email *Email
|
||||||
|
Phone *Phone
|
||||||
|
|
||||||
Metadata []*domain.Metadata
|
Metadata []*domain.Metadata
|
||||||
MetadataKeysToRemove []string
|
MetadataKeysToRemove []string
|
||||||
@@ -267,6 +268,7 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg
|
|||||||
existingHuman, err := c.UserHumanWriteModel(
|
existingHuman, err := c.UserHumanWriteModel(
|
||||||
ctx,
|
ctx,
|
||||||
human.ID,
|
human.ID,
|
||||||
|
human.ResourceOwner,
|
||||||
human.Profile != nil,
|
human.Profile != nil,
|
||||||
human.Email != nil,
|
human.Email != nil,
|
||||||
human.Phone != nil,
|
human.Phone != nil,
|
||||||
@@ -525,11 +527,11 @@ func (c *Commands) userExistsWriteModel(ctx context.Context, userID string) (wri
|
|||||||
return writeModel, nil
|
return writeModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) UserHumanWriteModel(ctx context.Context, userID string, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM, metadataWM bool) (writeModel *UserV2WriteModel, err error) {
|
func (c *Commands) UserHumanWriteModel(ctx context.Context, userID, resourceOwner string, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM, metadataWM bool) (writeModel *UserV2WriteModel, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
writeModel = NewUserHumanWriteModel(userID, "", profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM, metadataWM)
|
writeModel = NewUserHumanWriteModel(userID, resourceOwner, profileWM, emailWM, phoneWM, passwordWM, avatarWM, idpLinksWM, metadataWM)
|
||||||
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@@ -567,7 +567,7 @@ func TestCommandSide_userHumanWriteModel_profile(t *testing.T) {
|
|||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
}
|
}
|
||||||
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, true, false, false, false, false, false, false)
|
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", true, false, false, false, false, false, false)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -912,7 +912,7 @@ func TestCommandSide_userHumanWriteModel_email(t *testing.T) {
|
|||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
}
|
}
|
||||||
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, true, false, false, false, false, false)
|
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, true, false, false, false, false, false)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -1344,7 +1344,7 @@ func TestCommandSide_userHumanWriteModel_phone(t *testing.T) {
|
|||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
}
|
}
|
||||||
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, false, true, false, false, false, false)
|
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, false, true, false, false, false, false)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -1605,7 +1605,7 @@ func TestCommandSide_userHumanWriteModel_password(t *testing.T) {
|
|||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
}
|
}
|
||||||
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, false, false, true, false, false, false)
|
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, false, false, true, false, false, false)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -2132,7 +2132,7 @@ func TestCommandSide_userHumanWriteModel_avatar(t *testing.T) {
|
|||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
}
|
}
|
||||||
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, false, false, false, true, false, false)
|
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, false, false, false, true, false, false)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -2441,7 +2441,7 @@ func TestCommandSide_userHumanWriteModel_idpLinks(t *testing.T) {
|
|||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
}
|
}
|
||||||
wm, err := r.userRemoveWriteModel(tt.args.ctx, tt.args.userID)
|
wm, err := r.userRemoveWriteModel(tt.args.ctx, tt.args.userID, "")
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@@ -2744,7 +2744,7 @@ func TestCommandSide_userHumanWriteModel_metadata(t *testing.T) {
|
|||||||
r := &Commands{
|
r := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
}
|
}
|
||||||
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, false, false, false, false, false, false, true)
|
wm, err := r.UserHumanWriteModel(tt.args.ctx, tt.args.userID, "", false, false, false, false, false, false, true)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
|
@@ -1358,7 +1358,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
|
|||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
checkPermission: tt.fields.checkPermission,
|
checkPermission: tt.fields.checkPermission,
|
||||||
}
|
}
|
||||||
got, err := r.RemoveUserV2(tt.args.ctx, tt.args.userID, tt.args.cascadingMemberships, tt.args.grantIDs...)
|
got, err := r.RemoveUserV2(tt.args.ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@@ -368,6 +368,10 @@ func (q *Queries) GetUserByIDWithPermission(ctx context.Context, shouldTriggerBu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userID string) (user *User, err error) {
|
func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userID string) (user *User, err error) {
|
||||||
|
return q.GetUserByIDWithResourceOwner(ctx, shouldTriggerBulk, userID, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByIDWithResourceOwner(ctx context.Context, shouldTriggerBulk bool, userID, resourceOwner string) (user *User, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
@@ -382,6 +386,7 @@ func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userI
|
|||||||
},
|
},
|
||||||
userByIDQuery,
|
userByIDQuery,
|
||||||
userID,
|
userID,
|
||||||
|
resourceOwner,
|
||||||
authz.GetInstance(ctx).InstanceID(),
|
authz.GetInstance(ctx).InstanceID(),
|
||||||
)
|
)
|
||||||
return user, err
|
return user, err
|
||||||
|
@@ -20,8 +20,8 @@ WITH login_names AS (SELECT
|
|||||||
WHERE
|
WHERE
|
||||||
u.instance_id = p.instance_id
|
u.instance_id = p.instance_id
|
||||||
AND (
|
AND (
|
||||||
(p.is_default IS TRUE AND p.instance_id = $2)
|
(p.is_default IS TRUE AND p.instance_id = $3)
|
||||||
OR (p.instance_id = $2 AND p.resource_owner = u.resource_owner)
|
OR (p.instance_id = $3 AND p.resource_owner = u.resource_owner)
|
||||||
)
|
)
|
||||||
ORDER BY is_default
|
ORDER BY is_default
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -32,8 +32,9 @@ WITH login_names AS (SELECT
|
|||||||
u.instance_id = d.instance_id
|
u.instance_id = d.instance_id
|
||||||
AND u.resource_owner = d.resource_owner
|
AND u.resource_owner = d.resource_owner
|
||||||
WHERE
|
WHERE
|
||||||
u.instance_id = $2
|
u.id = $1
|
||||||
AND u.id = $1
|
AND (u.resource_owner = $2 OR $2 = '')
|
||||||
|
AND u.instance_id = $3
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
u.id
|
u.id
|
||||||
@@ -80,6 +81,7 @@ LEFT JOIN
|
|||||||
AND u.instance_id = m.instance_id
|
AND u.instance_id = m.instance_id
|
||||||
WHERE
|
WHERE
|
||||||
u.id = $1
|
u.id = $1
|
||||||
AND u.instance_id = $2
|
AND (u.resource_owner = $2 OR $2 = '')
|
||||||
|
AND u.instance_id = $3
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
;
|
;
|
Reference in New Issue
Block a user