feat: user profile requests in resource APIs (#10151)

# Which Problems Are Solved

The commands for the resource based v2beta AuthorizationService API are
added.
Authorizations, previously knows as user grants, give a user in a
specific organization and project context roles.
The project can be owned or granted.
The given roles can be used to restrict access within the projects
applications.

The commands for the resource based v2beta InteralPermissionService API
are added.
Administrators, previously knows as memberships, give a user in a
specific organization and project context roles.
The project can be owned or granted.
The give roles give the user permissions to manage different resources
in Zitadel.

API definitions from https://github.com/zitadel/zitadel/issues/9165 are
implemented.

Contains endpoints for user metadata.

# How the Problems Are Solved

### New Methods

- CreateAuthorization
- UpdateAuthorization
- DeleteAuthorization
- ActivateAuthorization
- DeactivateAuthorization
- ListAuthorizations
- CreateAdministrator
- UpdateAdministrator
- DeleteAdministrator
- ListAdministrators
- SetUserMetadata to set metadata on a user
- DeleteUserMetadata to delete metadata on a user
- ListUserMetadata to query for metadata of a user

## Deprecated Methods

### v1.ManagementService
- GetUserGrantByID
- ListUserGrants
- AddUserGrant
- UpdateUserGrant
- DeactivateUserGrant
- ReactivateUserGrant
- RemoveUserGrant
- BulkRemoveUserGrant

### v1.AuthService
- ListMyUserGrants
- ListMyProjectPermissions

# Additional Changes

- Permission checks for metadata functionality on query and command side
- correct existence checks for resources, for example you can only be an
administrator on an existing project
- combined all member tables to singular query for the administrators
- add permission checks for command an query side functionality
- combined functions on command side where necessary for easier
maintainability

# Additional Context

Closes #9165

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2025-07-04 18:12:59 +02:00
committed by GitHub
parent 9ebf2316c6
commit 5403be7c4b
142 changed files with 13223 additions and 2497 deletions

View File

@@ -690,7 +690,7 @@ func setupLoginClient(commands *Commands, validations *[]preparation.Validation,
func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) {
*validations = append(*validations,
commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner),
commands.AddOrgMemberCommand(&AddOrgMember{orgAgg.ID, userID, []string{domain.RoleOrgOwner}}),
commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner),
)
}

View File

@@ -2,7 +2,7 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
@@ -69,9 +69,19 @@ func IsInstanceMember(ctx context.Context, filter preparation.FilterToQueryReduc
return isMember, nil
}
func (c *Commands) AddInstanceMember(ctx context.Context, userID string, roles ...string) (*domain.Member, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddInstanceMemberCommand(instanceAgg, userID, roles...))
type AddInstanceMember struct {
InstanceID string
UserID string
Roles []string
}
func (c *Commands) AddInstanceMember(ctx context.Context, member *AddInstanceMember) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(member.InstanceID)
if err := c.checkPermissionUpdateInstanceMember(ctx, member.InstanceID); err != nil {
return nil, err
}
//nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddInstanceMemberCommand(instanceAgg, member.UserID, member.Roles...))
if err != nil {
return nil, err
}
@@ -79,33 +89,56 @@ func (c *Commands) AddInstanceMember(ctx context.Context, userID string, roles .
if err != nil {
return nil, err
}
addedMember := NewInstanceMemberWriteModel(ctx, userID)
addedMember := NewInstanceMemberWriteModel(member.InstanceID, member.UserID)
err = AppendAndReduce(addedMember, events...)
if err != nil {
return nil, err
}
return memberWriteModelToMember(&addedMember.MemberWriteModel), nil
return writeModelToObjectDetails(&addedMember.WriteModel), nil
}
type ChangeInstanceMember struct {
InstanceID string
UserID string
Roles []string
}
func (i *ChangeInstanceMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.InstanceID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IAM.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(i.Roles, domain.IAMRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "INSTANCE-3m9fs", "Errors.IAM.MemberInvalid")
}
return nil
}
// ChangeInstanceMember updates an existing member
func (c *Commands) ChangeInstanceMember(ctx context.Context, member *domain.Member) (*domain.Member, error) {
if !member.IsIAMValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IAM.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.IAMRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-3m9fs", "Errors.IAM.MemberInvalid")
}
existingMember, err := c.instanceMemberWriteModelByID(ctx, member.UserID)
if err != nil {
func (c *Commands) ChangeInstanceMember(ctx context.Context, member *ChangeInstanceMember) (*domain.ObjectDetails, error) {
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, zerrors.ThrowPreconditionFailed(nil, "INSTANCE-LiaZi", "Errors.IAM.Member.RolesNotChanged")
existingMember, err := c.instanceMemberWriteModelByID(ctx, member.InstanceID, member.UserID)
if err != nil {
return nil, err
}
instanceAgg := InstanceAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewMemberChangedEvent(ctx, instanceAgg, member.UserID, member.Roles...))
if !existingMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "INSTANCE-D8JxR", "Errors.NotFound")
}
if err := c.checkPermissionUpdateInstanceMember(ctx, existingMember.AggregateID); err != nil {
return nil, err
}
if slices.Compare(existingMember.Roles, member.Roles) == 0 {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx,
instance.NewMemberChangedEvent(ctx,
InstanceAggregateFromWriteModel(&existingMember.WriteModel),
member.UserID,
member.Roles...,
),
)
if err != nil {
return nil, err
}
@@ -114,34 +147,40 @@ func (c *Commands) ChangeInstanceMember(ctx context.Context, member *domain.Memb
return nil, err
}
return memberWriteModelToMember(&existingMember.MemberWriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) RemoveInstanceMember(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
func (c *Commands) RemoveInstanceMember(ctx context.Context, instanceID, userID string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IDMissing")
}
memberWriteModel, err := c.instanceMemberWriteModelByID(ctx, userID)
if err != nil && !zerrors.IsNotFound(err) {
return nil, err
}
if zerrors.IsNotFound(err) {
// empty response because we have no data that match the request
return &domain.ObjectDetails{}, nil
}
instanceAgg := InstanceAggregateFromWriteModel(&memberWriteModel.MemberWriteModel.WriteModel)
removeEvent := c.removeInstanceMember(ctx, instanceAgg, userID, false)
pushedEvents, err := c.eventstore.Push(ctx, removeEvent)
existingMember, err := c.instanceMemberWriteModelByID(ctx, instanceID, userID)
if err != nil {
return nil, err
}
err = AppendAndReduce(memberWriteModel, pushedEvents...)
if err := c.checkPermissionDeleteInstanceMember(ctx, instanceID); err != nil {
return nil, err
}
if !existingMember.State.Exists() {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx,
c.removeInstanceMember(ctx,
InstanceAggregateFromWriteModel(&existingMember.WriteModel),
userID,
false,
),
)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingMember, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&memberWriteModel.MemberWriteModel.WriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) removeInstanceMember(ctx context.Context, instanceAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command {
@@ -155,19 +194,15 @@ func (c *Commands) removeInstanceMember(ctx context.Context, instanceAgg *events
}
}
func (c *Commands) instanceMemberWriteModelByID(ctx context.Context, userID string) (member *InstanceMemberWriteModel, err error) {
func (c *Commands) instanceMemberWriteModelByID(ctx context.Context, instanceID, userID string) (member *InstanceMemberWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel := NewInstanceMemberWriteModel(ctx, userID)
writeModel := NewInstanceMemberWriteModel(instanceID, userID)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "INSTANCE-D8JxR", "Errors.NotFound")
}
return writeModel, nil
}

View File

@@ -1,9 +1,6 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
)
@@ -12,12 +9,12 @@ type InstanceMemberWriteModel struct {
MemberWriteModel
}
func NewInstanceMemberWriteModel(ctx context.Context, userID string) *InstanceMemberWriteModel {
func NewInstanceMemberWriteModel(instanceID, userID string) *InstanceMemberWriteModel {
return &InstanceMemberWriteModel{
MemberWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: authz.GetInstance(ctx).InstanceID(),
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
AggregateID: instanceID,
ResourceOwner: instanceID,
},
UserID: userID,
},

View File

@@ -10,24 +10,22 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommandSide_AddIAMMember(t *testing.T) {
func TestCommandSide_AddInstanceMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
userID string
roles []string
member *AddInstanceMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -39,28 +37,28 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &AddInstanceMember{},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
err: zerrors.IsInternal,
},
},
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -69,10 +67,10 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -80,9 +78,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
@@ -91,8 +91,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "member already exists, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -118,6 +117,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -125,9 +125,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -136,8 +138,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "member add uniqueconstraint err, already exists",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -163,6 +164,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -170,9 +172,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
},
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -181,8 +185,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
{
name: "member add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithInstanceID(
"INSTANCE",
@@ -209,6 +212,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -216,30 +220,49 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
},
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
userID: "user1",
roles: []string{"IAM_OWNER"},
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
InstanceID: "INSTANCE",
ResourceOwner: "INSTANCE",
AggregateID: "INSTANCE",
},
UserID: "user1",
Roles: []string{"IAM_OWNER"},
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "member add, no permission",
fields: fields{
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
},
},
},
args: args{
member: &AddInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.AddInstanceMember(tt.args.ctx, tt.args.userID, tt.args.roles...)
got, err := r.AddInstanceMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -247,24 +270,23 @@ func TestCommandSide_AddIAMMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_ChangeIAMMember(t *testing.T) {
func TestCommandSide_ChangeInstanceMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
instanceID string
member *domain.Member
member *ChangeInstanceMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -276,13 +298,11 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{},
member: &ChangeInstanceMember{},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -291,15 +311,14 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
UserID: "user1",
Roles: []string{"IAM_OWNER"},
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
@@ -309,10 +328,10 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
{
name: "member not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -320,10 +339,10 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
UserID: "user1",
Roles: []string{"IAM_OWNER"},
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
@@ -333,8 +352,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
{
name: "member not changed, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
@@ -345,6 +363,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleIAMOwner,
@@ -352,21 +371,22 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
UserID: "user1",
Roles: []string{"IAM_OWNER"},
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
@@ -384,6 +404,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
@@ -394,32 +415,62 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
UserID: "user1",
Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"},
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"},
},
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "INSTANCE",
AggregateID: "INSTANCE",
InstanceID: "INSTANCE",
},
UserID: "user1",
Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"},
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"user1",
[]string{"IAM_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "IAM_OWNER",
},
{
Role: "IAM_OWNER_VIEWER",
},
},
},
args: args{
member: &ChangeInstanceMember{
InstanceID: "INSTANCE",
UserID: "user1",
Roles: []string{"IAM_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.ChangeInstanceMember(tt.args.ctx, tt.args.member)
got, err := r.ChangeInstanceMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -427,18 +478,18 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveIAMMember(t *testing.T) {
func TestCommandSide_RemoveInstanceMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
instanceID string
userID string
}
@@ -455,13 +506,12 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
userID: "",
instanceID: "INSTANCE",
userID: "",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -470,24 +520,25 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
{
name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
userID: "user1",
instanceID: "INSTANCE",
userID: "user1",
},
res: res{
want: &domain.ObjectDetails{},
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
@@ -504,10 +555,11 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
userID: "user1",
instanceID: "INSTANCE",
userID: "user1",
},
res: res{
want: &domain.ObjectDetails{
@@ -515,13 +567,38 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
},
},
},
{
name: "member remove, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
instance.NewMemberAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
"user1",
[]string{"IAM_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
instanceID: "INSTANCE",
userID: "user1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveInstanceMember(tt.args.ctx, tt.args.userID)
got, err := r.RemoveInstanceMember(context.Background(), tt.args.instanceID, tt.args.userID)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@@ -123,7 +123,7 @@ func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSet
func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail bool) error {
if admin.ID != "" {
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, admin.ID, orgAdminRoles(admin.Roles)...))
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(&AddOrgMember{OrgID: c.aggregate.ID, UserID: admin.ID, Roles: orgAdminRoles(admin.Roles)}))
return nil
}
@@ -147,7 +147,7 @@ func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail
return err
}
}
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, userID, orgAdminRoles(admin.Roles)...))
c.validations = append(c.validations, c.commands.AddOrgMemberCommand(&AddOrgMember{OrgID: c.aggregate.ID, UserID: userID, Roles: orgAdminRoles(admin.Roles)}))
return nil
}
@@ -359,16 +359,15 @@ func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, reso
if err != nil {
return nil, err
}
err = c.checkUserExists(ctx, userID, resourceOwner)
_, err = c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
addedMember := NewOrgMemberWriteModel(addedOrg.AggregateID, userID)
orgMemberEvent, err := c.addOrgMember(ctx, orgAgg, addedMember, domain.NewMember(orgAgg.ID, userID, domain.RoleOrgOwner))
if err != nil {
addMember := &AddOrgMember{OrgID: orgAgg.ID, UserID: userID, Roles: []string{domain.RoleOrgOwner}}
if err := addMember.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
events = append(events, orgMemberEvent)
events = append(events, org.NewMemberAddedEvent(ctx, orgAgg, addMember.UserID, addMember.Roles...))
if setOrgInactive {
deactivateOrgEvent := org.NewOrgDeactivatedEvent(ctx, orgAgg)
events = append(events, deactivateOrgEvent)

View File

@@ -2,8 +2,9 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -12,29 +13,22 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddOrgMemberCommand(a *org.Aggregate, userID string, roles ...string) preparation.Validation {
func (c *Commands) AddOrgMemberCommand(member *AddOrgMember) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument")
}
if len(roles) == 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument")
}
if len(domain.CheckForInvalidRoles(roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists {
if exists, err := ExistsUser(ctx, filter, member.UserID, "", false); err != nil || !exists {
return nil, zerrors.ThrowPreconditionFailed(err, "ORG-GoXOn", "Errors.User.NotFound")
}
if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember {
if isMember, err := IsOrgMember(ctx, filter, member.OrgID, member.UserID); err != nil || isMember {
return nil, zerrors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists")
}
return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil
return []eventstore.Command{org.NewMemberAddedEvent(ctx, &org.NewAggregate(member.OrgID).Aggregate, member.UserID, member.Roles...)}, nil
},
nil
}
@@ -76,12 +70,33 @@ func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, o
return isMember, nil
}
func (c *Commands) AddOrgMember(ctx context.Context, orgID, userID string, roles ...string) (_ *domain.Member, err error) {
type AddOrgMember struct {
OrgID string
UserID string
Roles []string
}
func (m *AddOrgMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if m.UserID == "" || m.OrgID == "" || len(m.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument")
}
if len(domain.CheckForInvalidRoles(m.Roles, domain.OrgRolePrefix, zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(m.Roles, domain.RoleSelfManagementGlobal, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
}
return nil
}
func (c *Commands) AddOrgMember(ctx context.Context, member *AddOrgMember) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
orgAgg := org.NewAggregate(orgID)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddOrgMemberCommand(orgAgg, userID, roles...))
if err := c.checkOrgExists(ctx, member.OrgID); err != nil {
return nil, err
}
if err := c.checkPermissionUpdateOrgMember(ctx, member.OrgID, member.OrgID); err != nil {
return nil, err
}
//nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddOrgMemberCommand(member))
if err != nil {
return nil, err
}
@@ -89,51 +104,59 @@ func (c *Commands) AddOrgMember(ctx context.Context, orgID, userID string, roles
if err != nil {
return nil, err
}
addedMember := NewOrgMemberWriteModel(orgID, userID)
addedMember := NewOrgMemberWriteModel(member.OrgID, member.UserID)
err = AppendAndReduce(addedMember, events...)
if err != nil {
return nil, err
}
return memberWriteModelToMember(&addedMember.MemberWriteModel), nil
return writeModelToObjectDetails(&addedMember.WriteModel), nil
}
func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, addedMember *OrgMemberWriteModel, member *domain.Member) (eventstore.Command, error) {
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid")
type ChangeOrgMember struct {
OrgID string
UserID string
Roles []string
}
func (c *ChangeOrgMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if c.OrgID == "" || c.UserID == "" || len(c.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(member.Roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
}
err := c.eventstore.FilterToQueryReducer(ctx, addedMember)
if err != nil {
return nil, err
}
if addedMember.State == domain.MemberStateActive {
return nil, zerrors.ThrowAlreadyExists(nil, "Org-PtXi1", "Errors.Org.Member.AlreadyExists")
if len(domain.CheckForInvalidRoles(c.Roles, domain.OrgRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid")
}
return org.NewMemberAddedEvent(ctx, orgAgg, member.UserID, member.Roles...), nil
return nil
}
// ChangeOrgMember updates an existing member
func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) {
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid")
}
existingMember, err := c.orgMemberWriteModelByID(ctx, member.AggregateID, member.UserID)
if err != nil {
func (c *Commands) ChangeOrgMember(ctx context.Context, member *ChangeOrgMember) (*domain.ObjectDetails, error) {
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, zerrors.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.Member.RolesNotChanged")
existingMember, err := c.orgMemberWriteModelByID(ctx, member.OrgID, member.UserID)
if err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewMemberChangedEvent(ctx, orgAgg, member.UserID, member.Roles...))
if !existingMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "Org-D8JxR", "Errors.NotFound")
}
if err := c.checkPermissionUpdateOrgMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil {
return nil, err
}
if slices.Compare(existingMember.Roles, member.Roles) == 0 {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx,
org.NewMemberChangedEvent(ctx,
OrgAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel),
member.UserID,
member.Roles...,
),
)
if err != nil {
return nil, err
}
@@ -142,30 +165,39 @@ func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (
return nil, err
}
return memberWriteModelToMember(&existingMember.MemberWriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) RemoveOrgMember(ctx context.Context, orgID, userID string) (*domain.ObjectDetails, error) {
m, err := c.orgMemberWriteModelByID(ctx, orgID, userID)
if err != nil && !zerrors.IsNotFound(err) {
if orgID == "" || userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid")
}
existingMember, err := c.orgMemberWriteModelByID(ctx, orgID, userID)
if err != nil {
return nil, err
}
if zerrors.IsNotFound(err) {
// empty response because we have no data that match the request
return &domain.ObjectDetails{}, nil
if !existingMember.State.Exists() {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
if err := c.checkPermissionDeleteOrgMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&m.MemberWriteModel.WriteModel)
removeEvent := c.removeOrgMember(ctx, orgAgg, userID, false)
pushedEvents, err := c.eventstore.Push(ctx, removeEvent)
pushedEvents, err := c.eventstore.Push(ctx,
c.removeOrgMember(ctx,
OrgAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel),
userID,
false,
),
)
if err != nil {
return nil, err
}
err = AppendAndReduce(m, pushedEvents...)
err = AppendAndReduce(existingMember, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&m.WriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) removeOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command {
@@ -189,9 +221,5 @@ func (c *Commands) orgMemberWriteModelByID(ctx context.Context, orgID, userID st
return nil, err
}
if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "Org-D8JxR", "Errors.NotFound")
}
return writeModel, nil
}

View File

@@ -11,24 +11,19 @@ import (
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestAddMember(t *testing.T) {
type args struct {
a *org.Aggregate
userID string
roles []string
member *AddOrgMember
zitadelRoles []authz.RoleMapping
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := org.NewAggregate("test")
tests := []struct {
name string
@@ -38,8 +33,10 @@ func TestAddMember(t *testing.T) {
{
name: "no user id",
args: args{
a: agg,
userID: "",
member: &AddOrgMember{
OrgID: "test",
UserID: "",
},
},
want: Want{
ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
@@ -48,19 +45,23 @@ func TestAddMember(t *testing.T) {
{
name: "no roles",
args: args{
a: agg,
userID: "12342",
member: &AddOrgMember{
OrgID: "test",
UserID: "12342",
},
},
want: Want{
ValidationErr: zerrors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument"),
ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
},
},
{
name: "TODO: invalid roles",
args: args{
a: agg,
userID: "123",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "test",
UserID: "12342",
Roles: []string{"ORG_OWNER"},
},
},
want: Want{
ValidationErr: zerrors.ThrowInvalidArgument(nil, "Org-4N8es", ""),
@@ -69,9 +70,11 @@ func TestAddMember(t *testing.T) {
{
name: "user not exists",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "test",
UserID: "userID",
Roles: []string{"ORG_OWNER"},
},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
@@ -89,9 +92,11 @@ func TestAddMember(t *testing.T) {
{
name: "already member",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "test",
UserID: "userID",
Roles: []string{"ORG_OWNER"},
},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
@@ -129,9 +134,11 @@ func TestAddMember(t *testing.T) {
{
name: "correct",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "test",
UserID: "userID",
Roles: []string{"ORG_OWNER"},
},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
@@ -158,14 +165,14 @@ func TestAddMember(t *testing.T) {
},
want: Want{
Commands: []eventstore.Command{
org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID", "ORG_OWNER"),
org.NewMemberAddedEvent(ctx, &org.NewAggregate("test").Aggregate, "userID", "ORG_OWNER"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, context.Background(), (&Commands{zitadelRoles: tt.args.zitadelRoles}).AddOrgMemberCommand(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want)
AssertValidation(t, context.Background(), (&Commands{zitadelRoles: tt.args.zitadelRoles}).AddOrgMemberCommand(tt.args.member), tt.args.filter, tt.want)
})
}
}
@@ -287,17 +294,15 @@ func TestIsMember(t *testing.T) {
func TestCommandSide_AddOrgMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
}
type args struct {
ctx context.Context
userID string
orgID string
roles []string
member *AddOrgMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -309,13 +314,22 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
member: &AddOrgMember{
OrgID: "org1",
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -324,15 +338,24 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -341,10 +364,18 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -352,10 +383,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{domain.RoleOrgOwner},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
@@ -364,8 +396,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "member already exists, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -391,6 +430,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -398,10 +438,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -410,8 +451,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "member add uniqueconstraint err, already exists",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -437,6 +485,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -444,10 +493,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -456,8 +506,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
{
name: "member add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -483,6 +540,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -490,30 +548,58 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
roles: []string{"ORG_OWNER"},
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "org1",
},
UserID: "user1",
Roles: []string{domain.RoleOrgOwner},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member add, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"org",
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
},
},
},
args: args{
member: &AddOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.AddOrgMember(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.roles...)
got, err := r.AddOrgMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -521,7 +607,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
@@ -529,15 +615,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
func TestCommandSide_ChangeOrgMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
}
type args struct {
ctx context.Context
member *domain.Member
member *ChangeOrgMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -549,16 +635,12 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
},
},
res: res{
@@ -568,16 +650,12 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
@@ -589,10 +667,10 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
{
name: "member not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -600,11 +678,8 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
@@ -614,10 +689,9 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
{
name: "member not changed, precondition error",
name: "member not changed, no change",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
@@ -628,6 +702,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleOrgOwner,
@@ -635,24 +710,22 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
@@ -670,6 +743,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
@@ -680,160 +754,210 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"},
},
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "org1",
},
UserID: "user1",
Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
}
got, err := r.ChangeOrgMember(tt.args.ctx, tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveOrgMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
projectID string
userID string
resourceOwner string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "invalid member projectid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
projectID: "",
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "",
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
expectPush(
project.NewProjectMemberRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
),
),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"user1",
[]string{"ORG_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
},
{
Role: "ORG_OWNER_VIEWER",
},
},
},
args: args{
member: &ChangeOrgMember{
OrgID: "org1",
UserID: "user1",
Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner)
got, err := r.ChangeOrgMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assertObjectDetails(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveOrgMember(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
orgID string
userID string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "invalid member orgID missing, error",
fields: fields{
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "",
userID: "user1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "member not existing, empty object details result",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
expectPush(
org.NewMemberRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"user1",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member remove, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewMemberAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveOrgMember(tt.args.ctx, tt.args.orgID, tt.args.userID)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@@ -1,6 +1,8 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
@@ -63,3 +65,7 @@ func (wm *OrgWriteModel) Query() *eventstore.SearchQueryBuilder {
func OrgAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return eventstore.AggregateFromWriteModel(wm, org.AggregateType, org.AggregateVersion)
}
func OrgAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate {
return org.AggregateFromWriteModel(ctx, wm)
}

View File

@@ -73,7 +73,7 @@ func TestAddOrg(t *testing.T) {
func TestCommandSide_AddOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
zitadelRoles []authz.RoleMapping
}
@@ -97,9 +97,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "invalid org, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -113,9 +111,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "invalid org (spaces), error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -130,8 +126,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "user removed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -174,8 +169,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "push failed unique constraint, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -193,7 +187,6 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
),
expectFilterOrgMemberNotFound(),
expectPushFailed(zerrors.ThrowAlreadyExists(nil, "id", "internal"),
org.NewOrgAddedEvent(
context.Background(),
@@ -242,8 +235,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -261,7 +253,6 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
),
expectFilterOrgMemberNotFound(),
expectPushFailed(zerrors.ThrowInternal(nil, "id", "internal"),
org.NewOrgAddedEvent(
context.Background(),
@@ -310,8 +301,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "add org, no error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -329,7 +319,6 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
),
expectFilterOrgMemberNotFound(),
expectPush(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org2").Aggregate,
@@ -381,8 +370,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
{
name: "add org (remove spaces), no error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilterOrgDomainNotFound(),
expectFilter(
eventFromEventPusher(
@@ -400,7 +388,6 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
),
expectFilterOrgMemberNotFound(),
expectPush(
org.NewOrgAddedEvent(context.Background(),
&org.NewAggregate("org2").Aggregate,
@@ -453,7 +440,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
zitadelRoles: tt.fields.zitadelRoles,
}
@@ -473,7 +460,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
func TestCommandSide_ChangeOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -492,9 +479,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "empty name, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -507,9 +492,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "empty name (spaces), invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -523,8 +506,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "org not found, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -540,8 +522,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "no change (spaces), error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -563,8 +544,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -593,8 +573,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "change org name verified, not primary",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -645,8 +624,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "change org name verified, with primary",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -705,8 +683,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
{
name: "change org name case verified, with primary",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -754,7 +731,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
_, err := r.ChangeOrg(tt.args.ctx, tt.args.orgID, tt.args.name)
if tt.res.err == nil {
@@ -769,7 +746,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
func TestCommandSide_DeactivateOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
iamDomain string
}
@@ -790,8 +767,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
{
name: "org not found, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -806,8 +782,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
{
name: "org already inactive, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -832,8 +807,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -860,8 +834,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
{
name: "deactivate org",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -886,7 +859,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
_, err := r.DeactivateOrg(tt.args.ctx, tt.args.orgID)
@@ -902,7 +875,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
func TestCommandSide_ReactivateOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
iamDomain string
}
@@ -923,8 +896,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
{
name: "org not found, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -939,8 +911,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
{
name: "org already active, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -961,8 +932,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -994,8 +964,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
{
name: "reactivate org",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
org.NewOrgAddedEvent(context.Background(),
@@ -1024,7 +993,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
_, err := r.ReactivateOrg(tt.args.ctx, tt.args.orgID)
@@ -1040,7 +1009,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
func TestCommandSide_RemoveOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
@@ -1059,9 +1028,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "default org, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: authz.WithInstance(context.Background(), &mockInstance{}),
@@ -1074,8 +1041,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "zitadel org, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
@@ -1100,8 +1066,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "org not found, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(), // zitadel project check
expectFilter(),
),
@@ -1117,8 +1082,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "push failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(), // zitadel project check
expectFilter(
eventFromEventPusher(
@@ -1160,8 +1124,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "remove org",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(), // zitadel project check
expectFilter(
eventFromEventPusher(
@@ -1200,8 +1163,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
{
name: "remove org with usernames and domains",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(), // zitadel project check
expectFilter(
eventFromEventPusher(
@@ -1291,7 +1253,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
_, err := r.RemoveOrg(tt.args.ctx, tt.args.orgID)

View File

@@ -6,6 +6,8 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/v2/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -13,6 +15,8 @@ import (
type PermissionCheck func(resourceOwner, aggregateID string) error
type UserGrantPermissionCheck func(projectID, projectGrantID string) PermissionCheck
func (c *Commands) newPermissionCheck(ctx context.Context, permission string, aggregateType eventstore.AggregateType) PermissionCheck {
return func(resourceOwner, aggregateID string) error {
if aggregateID == "" {
@@ -93,3 +97,62 @@ func (c *Commands) checkPermissionUpdateApplication(ctx context.Context, resourc
func (c *Commands) checkPermissionDeleteApp(ctx context.Context, resourceOwner, appID string) error {
return c.newPermissionCheck(ctx, domain.PermissionProjectAppDelete, project.AggregateType)(resourceOwner, appID)
}
func (c *Commands) checkPermissionUpdateInstanceMember(ctx context.Context, instanceID string) error {
return c.newPermissionCheck(ctx, domain.PermissionInstanceMemberWrite, instance.AggregateType)(instanceID, instanceID)
}
func (c *Commands) checkPermissionDeleteInstanceMember(ctx context.Context, instanceID string) error {
return c.newPermissionCheck(ctx, domain.PermissionInstanceMemberDelete, instance.AggregateType)(instanceID, instanceID)
}
func (c *Commands) checkPermissionUpdateOrgMember(ctx context.Context, instanceID, orgID string) error {
return c.newPermissionCheck(ctx, domain.PermissionOrgMemberWrite, org.AggregateType)(instanceID, orgID)
}
func (c *Commands) checkPermissionDeleteOrgMember(ctx context.Context, instanceID, orgID string) error {
return c.newPermissionCheck(ctx, domain.PermissionOrgMemberDelete, org.AggregateType)(instanceID, orgID)
}
func (c *Commands) checkPermissionUpdateProjectMember(ctx context.Context, resourceOwner, projectID string) error {
return c.newPermissionCheck(ctx, domain.PermissionProjectMemberWrite, project.AggregateType)(resourceOwner, projectID)
}
func (c *Commands) checkPermissionDeleteProjectMember(ctx context.Context, resourceOwner, projectID string) error {
return c.newPermissionCheck(ctx, domain.PermissionProjectMemberDelete, project.AggregateType)(resourceOwner, projectID)
}
func (c *Commands) checkPermissionUpdateProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) {
// TODO: add permission check for project grant owners
//if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil {
return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(grantedOrgID, projectGrantID)
//}
//return nil
}
func (c *Commands) checkPermissionDeleteProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) {
// TODO: add permission check for project grant owners
//if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil {
return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(grantedOrgID, projectGrantID)
//}
//return nil
}
func (c *Commands) newUserGrantPermissionCheck(ctx context.Context, permission string) UserGrantPermissionCheck {
check := c.newPermissionCheck(ctx, permission, project.AggregateType)
return func(projectID, projectGrantID string) PermissionCheck {
return func(resourceOwner, _ string) error {
if projectGrantID != "" {
return check(resourceOwner, projectGrantID)
}
return check(resourceOwner, projectID)
}
}
}
func (c *Commands) NewPermissionCheckUserGrantWrite(ctx context.Context) UserGrantPermissionCheck {
return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantWrite)
}
func (c *Commands) NewPermissionCheckUserGrantDelete(ctx context.Context) UserGrantPermissionCheck {
return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantDelete)
}

View File

@@ -346,7 +346,7 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s
}
for _, grantID := range cascadingUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, grantID, "", true)
event, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil)
if err != nil {
logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue
@@ -398,7 +398,7 @@ func (c *Commands) DeleteProject(ctx context.Context, id, resourceOwner string,
),
}
for _, grantID := range cascadingUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, grantID, "", true)
event, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil)
if err != nil {
logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue

View File

@@ -234,6 +234,17 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
}
func (c *Commands) checkProjectGrantExists(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (string, string, error) {
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner)
if err != nil {
return "", "", err
}
if !existingGrant.State.Exists() {
return "", "", zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound")
}
return existingGrant.GrantedOrgID, existingGrant.ResourceOwner, nil
}
func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) {
if (grantID == "" && grantedOrgID == "") || projectID == "" {
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing")
@@ -302,16 +313,17 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r
ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel),
existingGrant.GrantID,
existingGrant.GrantedOrgID,
),
)
))
for _, userGrantID := range cascadeUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true)
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true, true, nil)
if err != nil {
logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue
}
events = append(events, event)
if event != nil {
events = append(events, event)
}
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
@@ -348,12 +360,14 @@ func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, g
)
for _, userGrantID := range cascadeUserGrantIDs {
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true)
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true, true, nil)
if err != nil {
logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
continue
}
events = append(events, event)
if event != nil {
events = append(events, event)
}
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {

View File

@@ -2,8 +2,9 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
@@ -11,25 +12,66 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (_ *domain.ProjectGrantMember, err error) {
type AddProjectGrantMember struct {
ResourceOwner string
UserID string
GrantID string
ProjectID string
Roles []string
}
func (i *AddProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid")
}
return nil
}
func (c *Commands) AddProjectGrantMember(ctx context.Context, member *AddProjectGrantMember) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid")
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid")
}
err = c.checkUserExists(ctx, member.UserID, "")
_, err = c.checkUserExists(ctx, member.UserID, "")
if err != nil {
return nil, err
}
addedMember := NewProjectGrantMemberWriteModel(member.AggregateID, member.UserID, member.GrantID)
projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel)
grantedOrgID, projectGrantResourceOwner, err := c.checkProjectGrantExists(ctx, member.GrantID, "", member.ProjectID, "")
if err != nil {
return nil, err
}
if member.ResourceOwner == "" {
member.ResourceOwner = projectGrantResourceOwner
}
addedMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, member.ResourceOwner)
if err != nil {
return nil, err
}
// TODO: change e2e tests to use correct resourceowner, wrong resource owner is corrected through aggregate
// error if provided resourceowner is not equal to the resourceowner of the project grant
//if projectGrantResourceOwner != addedMember.ResourceOwner {
// return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid")
//}
if addedMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.AlreadyExists")
}
if err := c.checkPermissionUpdateProjectGrantMember(ctx, grantedOrgID, addedMember.GrantID); err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(
ctx,
project.NewProjectGrantMemberAddedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...))
project.NewProjectGrantMemberAddedEvent(ctx,
ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel),
member.UserID,
member.GrantID,
member.Roles...,
))
if err != nil {
return nil, err
}
@@ -38,30 +80,58 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.Pro
return nil, err
}
return memberWriteModelToProjectGrantMember(addedMember), nil
return writeModelToObjectDetails(&addedMember.WriteModel), nil
}
type ChangeProjectGrantMember struct {
UserID string
GrantID string
ProjectID string
Roles []string
}
func (i *ChangeProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Grant.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Grant.Member.Invalid")
}
return nil
}
// ChangeProjectGrantMember updates an existing member
func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (*domain.ProjectGrantMember, error) {
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Member.Invalid")
func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *ChangeProjectGrantMember) (*domain.ObjectDetails, error) {
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Member.Invalid")
}
existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.AggregateID, member.UserID, member.GrantID)
existingGrant, err := c.projectGrantWriteModelByID(ctx, member.GrantID, "", member.ProjectID, "")
if err != nil {
return nil, err
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-2n8vx", "Errors.Project.Member.RolesNotChanged")
existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, existingGrant.ResourceOwner)
if err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&existingMember.WriteModel)
if !existingMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.NotFound")
}
if err := c.checkPermissionUpdateProjectGrantMember(ctx, existingGrant.GrantedOrgID, existingMember.GrantID); err != nil {
return nil, err
}
if slices.Compare(existingMember.Roles, member.Roles) == 0 {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(
ctx,
project.NewProjectGrantMemberChangedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...))
project.NewProjectGrantMemberChangedEvent(ctx,
ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel),
member.UserID,
member.GrantID,
member.Roles...,
))
if err != nil {
return nil, err
}
@@ -70,29 +140,43 @@ func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain.
return nil, err
}
return memberWriteModelToProjectGrantMember(existingMember), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, userID, grantID string) (*domain.ObjectDetails, error) {
if projectID == "" || userID == "" || grantID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid")
}
m, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID)
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, "")
if err != nil {
return nil, err
}
existingMember, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID, existingGrant.ResourceOwner)
if err != nil {
return nil, err
}
if !existingMember.State.Exists() {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
if err := c.checkPermissionDeleteProjectGrantMember(ctx, existingGrant.GrantedOrgID, existingMember.GrantID); err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&m.WriteModel)
removeEvent := c.removeProjectGrantMember(ctx, projectAgg, userID, grantID, false)
removeEvent := c.removeProjectGrantMember(ctx,
ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel),
userID,
grantID,
false,
)
pushedEvents, err := c.eventstore.Push(ctx, removeEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(m, pushedEvents...)
err = AppendAndReduce(existingMember, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&m.WriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) removeProjectGrantMember(ctx context.Context, projectAgg *eventstore.Aggregate, userID, grantID string, cascade bool) eventstore.Command {
@@ -107,19 +191,15 @@ func (c *Commands) removeProjectGrantMember(ctx context.Context, projectAgg *eve
}
}
func (c *Commands) projectGrantMemberWriteModelByID(ctx context.Context, projectID, userID, grantID string) (member *ProjectGrantMemberWriteModel, err error) {
func (c *Commands) projectGrantMemberWriteModelByID(ctx context.Context, projectID, userID, grantID, resourceOwner string) (member *ProjectGrantMemberWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel := NewProjectGrantMemberWriteModel(projectID, userID, grantID)
writeModel := NewProjectGrantMemberWriteModel(projectID, userID, grantID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.NotFound")
}
return writeModel, nil
}

View File

@@ -16,10 +16,11 @@ type ProjectGrantMemberWriteModel struct {
State domain.MemberState
}
func NewProjectGrantMemberWriteModel(projectID, userID, grantID string) *ProjectGrantMemberWriteModel {
func NewProjectGrantMemberWriteModel(projectID, userID, grantID, resourceOwner string) *ProjectGrantMemberWriteModel {
return &ProjectGrantMemberWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: projectID,
AggregateID: projectID,
ResourceOwner: resourceOwner,
},
UserID: userID,
GrantID: grantID,
@@ -66,6 +67,7 @@ func (wm *ProjectGrantMemberWriteModel) Reduce() error {
case *project.GrantMemberAddedEvent:
wm.Roles = e.Roles
wm.State = domain.MemberStateActive
wm.ResourceOwner = e.Aggregate().ResourceOwner
case *project.GrantMemberChangedEvent:
wm.Roles = e.Roles
case *project.GrantMemberRemovedEvent:
@@ -80,7 +82,8 @@ func (wm *ProjectGrantMemberWriteModel) Reduce() error {
}
func (wm *ProjectGrantMemberWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(wm.AggregateID).
@@ -92,4 +95,5 @@ func (wm *ProjectGrantMemberWriteModel) Query() *eventstore.SearchQueryBuilder {
project.GrantRemovedType,
project.ProjectRemovedType).
Builder()
return query
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -18,15 +17,15 @@ import (
func TestCommandSide_AddProjectGrantMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
member *domain.ProjectGrantMember
member *AddProjectGrantMember
}
type res struct {
want *domain.ProjectGrantMember
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -38,16 +37,12 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
member: &AddProjectGrantMember{
ProjectID: "project1",
},
},
res: res{
@@ -57,19 +52,15 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
@@ -79,10 +70,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -90,14 +81,11 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
@@ -107,8 +95,7 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "member add uniqueconstraint err, already exists",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -125,15 +112,28 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
eventFromEventPusher(project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"grantedorg1",
[]string{"key1"},
),
),
),
),
expectFilter(),
expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internal"),
project.NewProjectGrantMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "").Aggregate,
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
"projectgrant1",
[]string{"PROJECT_GRANT_OWNER"}...,
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -141,14 +141,11 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
@@ -158,8 +155,7 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
{
name: "member add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -176,15 +172,28 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
eventFromEventPusher(project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"grantedorg1",
[]string{"key1"},
),
),
),
),
expectFilter(),
expectPush(
project.NewProjectGrantMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "").Aggregate,
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
"projectgrant1",
[]string{"PROJECT_GRANT_OWNER"}...,
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -192,35 +201,81 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
GrantID: "projectgrant1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
want: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
ID: "project1",
},
},
},
{
name: "member add, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username1",
"firstname1",
"lastname1",
"nickname1",
"displayname1",
language.German,
domain.GenderMale,
"email1",
true,
),
),
),
expectFilter(
eventFromEventPusher(
eventFromEventPusher(project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"grantedorg1",
[]string{"key1"},
),
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
},
},
},
args: args{
member: &AddProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.AddProjectGrantMember(tt.args.ctx, tt.args.member)
got, err := r.AddProjectGrantMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -236,15 +291,15 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) {
func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
member *domain.ProjectGrantMember
member *ChangeProjectGrantMember
}
type res struct {
want *domain.ProjectGrantMember
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -256,16 +311,12 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
},
},
res: res{
@@ -275,19 +326,15 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
},
res: res{
@@ -297,10 +344,20 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
{
name: "member not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -308,14 +365,11 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
@@ -325,8 +379,17 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
{
name: "member not changed, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
@@ -338,6 +401,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -345,25 +409,33 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER"},
},
},
res: res{
err: zerrors.IsPreconditionFailed,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
@@ -383,6 +455,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
@@ -393,36 +466,75 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"},
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"},
},
},
res: res{
want: &domain.ProjectGrantMember{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "project1",
},
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
"projectgrant1",
[]string{"PROJECT_GRANT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: "PROJECT_GRANT_OWNER",
},
{
Role: "PROJECT_GRANT_VIEWER",
},
},
},
args: args{
member: &ChangeProjectGrantMember{
ProjectID: "project1",
GrantID: "projectgrant1",
UserID: "user1",
Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.ChangeProjectGrantMember(tt.args.ctx, tt.args.member)
got, err := r.ChangeProjectGrantMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -430,7 +542,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
@@ -438,7 +550,8 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) {
func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
@@ -459,9 +572,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
{
name: "invalid member projectid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -476,9 +588,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -493,9 +604,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
{
name: "invalid member grantid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -508,12 +618,22 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
},
},
{
name: "member not existing, not found err",
name: "member not existing, not found ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -522,14 +642,25 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
grantID: "projectgrant1",
},
res: res{
err: zerrors.IsNotFound,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
@@ -548,6 +679,7 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -561,11 +693,49 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) {
},
},
},
{
name: "member remove, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewGrantAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectgrant1",
"org2",
[]string{"rol1", "role2"},
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectGrantMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
"projectgrant1",
[]string{"PROJECT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
grantID: "projectgrant1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveProjectGrantMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.grantID)
if tt.res.err == nil {

View File

@@ -2,8 +2,9 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
@@ -11,18 +12,64 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member, resourceOwner string) (_ *domain.Member, err error) {
type AddProjectMember struct {
ResourceOwner string
ProjectID string
UserID string
Roles []string
}
func (i *AddProjectMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.ProjectID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid")
}
return nil
}
func (c *Commands) AddProjectMember(ctx context.Context, member *AddProjectMember) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
addedMember := NewProjectMemberWriteModel(member.AggregateID, member.UserID, resourceOwner)
projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel)
event, err := c.addProjectMember(ctx, projectAgg, addedMember, member)
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
_, err = c.checkUserExists(ctx, member.UserID, "")
if err != nil {
return nil, err
}
projectResourceOwner, err := c.checkProjectExists(ctx, member.ProjectID, member.ResourceOwner)
if err != nil {
return nil, err
}
// resourceowner of the member if not provided is the resourceowner of the project
if member.ResourceOwner == "" {
member.ResourceOwner = projectResourceOwner
}
addedMember, err := c.projectMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ResourceOwner)
if err != nil {
return nil, err
}
// error if provided resourceowner is not equal to the resourceowner of the project
if projectResourceOwner != addedMember.ResourceOwner {
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Member.Invalid")
}
if err := c.checkPermissionUpdateProjectMember(ctx, addedMember.ResourceOwner, addedMember.AggregateID); err != nil {
return nil, err
}
if addedMember.State.Exists() {
return nil, zerrors.ThrowAlreadyExists(nil, "PROJECT-PtXi1", "Errors.Project.Member.AlreadyExists")
}
pushedEvents, err := c.eventstore.Push(ctx, event)
pushedEvents, err := c.eventstore.Push(ctx,
project.NewProjectMemberAddedEvent(ctx,
ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel),
member.UserID,
member.Roles...,
),
)
if err != nil {
return nil, err
}
@@ -31,53 +78,46 @@ func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member,
return nil, err
}
return memberWriteModelToMember(&addedMember.MemberWriteModel), nil
return writeModelToObjectDetails(&addedMember.WriteModel), nil
}
func (c *Commands) addProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, addedMember *ProjectMemberWriteModel, member *domain.Member) (_ eventstore.Command, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
type ChangeProjectMember struct {
ResourceOwner string
ProjectID string
UserID string
Roles []string
}
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid")
func (i *ChangeProjectMember) IsValid(zitadelRoles []authz.RoleMapping) error {
if i.ProjectID == "" || i.UserID == "" || len(i.Roles) == 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid")
if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectRolePrefix, zitadelRoles)) > 0 {
return zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid")
}
err = c.checkUserExists(ctx, addedMember.UserID, "")
if err != nil {
return nil, err
}
err = c.eventstore.FilterToQueryReducer(ctx, addedMember)
if err != nil {
return nil, err
}
if addedMember.State == domain.MemberStateActive {
return nil, zerrors.ThrowAlreadyExists(nil, "PROJECT-PtXi1", "Errors.Project.Member.AlreadyExists")
}
return project.NewProjectMemberAddedEvent(ctx, projectAgg, member.UserID, member.Roles...), nil
return nil
}
// ChangeProjectMember updates an existing member
func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Member, resourceOwner string) (*domain.Member, error) {
if !member.IsValid() {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid")
}
existingMember, err := c.projectMemberWriteModelByID(ctx, member.AggregateID, member.UserID, resourceOwner)
if err != nil {
func (c *Commands) ChangeProjectMember(ctx context.Context, member *ChangeProjectMember) (*domain.ObjectDetails, error) {
if err := member.IsValid(c.zitadelRoles); err != nil {
return nil, err
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-LiaZi", "Errors.Project.Member.RolesNotChanged")
existingMember, err := c.projectMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ResourceOwner)
if err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel)
if !existingMember.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.NotFound")
}
if err := c.checkPermissionUpdateProjectMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil {
return nil, err
}
if slices.Compare(existingMember.Roles, member.Roles) == 0 {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectMemberChangedEvent(ctx, projectAgg, member.UserID, member.Roles...))
if err != nil {
return nil, err
@@ -88,33 +128,35 @@ func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Membe
return nil, err
}
return memberWriteModelToMember(&existingMember.MemberWriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) RemoveProjectMember(ctx context.Context, projectID, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if projectID == "" || userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid")
}
m, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner)
if err != nil && !zerrors.IsNotFound(err) {
existingMember, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner)
if err != nil {
return nil, err
}
if zerrors.IsNotFound(err) {
// empty response because we have no data that match the request
return &domain.ObjectDetails{}, nil
if !existingMember.State.Exists() {
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
if err := c.checkPermissionDeleteProjectMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil {
return nil, err
}
projectAgg := ProjectAggregateFromWriteModel(&m.MemberWriteModel.WriteModel)
projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel)
removeEvent := c.removeProjectMember(ctx, projectAgg, userID, false)
pushedEvents, err := c.eventstore.Push(ctx, removeEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(m, pushedEvents...)
err = AppendAndReduce(existingMember, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&m.WriteModel), nil
return writeModelToObjectDetails(&existingMember.WriteModel), nil
}
func (c *Commands) removeProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command {
@@ -138,9 +180,5 @@ func (c *Commands) projectMemberWriteModelByID(ctx context.Context, projectID, u
return nil, err
}
if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.NotFound")
}
return writeModel, nil
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -18,16 +17,15 @@ import (
func TestCommandSide_AddProjectMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
member *domain.Member
resourceOwner string
member *AddProjectMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -39,18 +37,14 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -59,20 +53,16 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -81,10 +71,10 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "user not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -92,15 +82,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsPreconditionFailed,
@@ -109,8 +96,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "member already exists, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -127,6 +113,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -136,6 +135,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -143,15 +143,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -160,8 +157,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "member add uniqueconstraint err, already exists",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -178,6 +174,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
),
),
expectFilter(),
expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internal"),
project.NewProjectMemberAddedEvent(context.Background(),
@@ -187,6 +196,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -194,15 +204,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorAlreadyExists,
@@ -211,8 +218,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
{
name: "member add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -229,6 +235,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
),
),
expectFilter(),
expectPush(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -238,6 +257,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -245,35 +265,81 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{domain.RoleProjectOwner},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
}, {
name: "member add, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username1",
"firstname1",
"lastname1",
"nickname1",
"displayname1",
language.German,
domain.GenderMale,
"email1",
true,
),
),
),
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
},
},
},
args: args{
member: &AddProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.AddProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner)
got, err := r.AddProjectMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -281,7 +347,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
@@ -289,16 +355,15 @@ func TestCommandSide_AddProjectMember(t *testing.T) {
func TestCommandSide_ChangeProjectMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
eventstore func(t *testing.T) *eventstore.Eventstore
zitadelRoles []authz.RoleMapping
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
member *domain.Member
resourceOwner string
member *ChangeProjectMember
}
type res struct {
want *domain.Member
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
@@ -310,18 +375,14 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
{
name: "invalid member, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -330,20 +391,16 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
{
name: "invalid roles, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -352,10 +409,10 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
{
name: "member not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -363,15 +420,12 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsNotFound,
@@ -380,8 +434,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
{
name: "member not changed, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -392,6 +445,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -399,25 +453,23 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER"},
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsPreconditionFailed,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -435,6 +487,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
@@ -445,35 +498,64 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
},
},
args: args{
ctx: context.Background(),
member: &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"},
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"},
},
resourceOwner: "org1",
},
res: res{
want: &domain.Member{
ObjectRoot: models.ObjectRoot{
ResourceOwner: "org1",
AggregateID: "project1",
},
UserID: "user1",
Roles: []string{domain.RoleProjectOwner, "PROJECT_VIEWER"},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member change, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
zitadelRoles: []authz.RoleMapping{
{
Role: domain.RoleProjectOwner,
},
{
Role: "PROJECT_VIEWER",
},
},
},
args: args{
member: &ChangeProjectMember{
ResourceOwner: "org1",
ProjectID: "project1",
UserID: "user1",
Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
zitadelRoles: tt.fields.zitadelRoles,
eventstore: tt.fields.eventstore(t),
zitadelRoles: tt.fields.zitadelRoles,
checkPermission: tt.fields.checkPermission,
}
got, err := r.ChangeProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner)
got, err := r.ChangeProjectMember(context.Background(), tt.args.member)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -481,7 +563,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assertObjectDetails(t, tt.res.want, got)
}
})
}
@@ -489,10 +571,10 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) {
func TestCommandSide_RemoveProjectMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
projectID string
userID string
resourceOwner string
@@ -510,12 +592,10 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
{
name: "invalid member projectid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "",
userID: "user1",
resourceOwner: "org1",
@@ -527,12 +607,10 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
{
name: "invalid member userid missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "",
resourceOwner: "org1",
@@ -544,26 +622,26 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
{
name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "member remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
@@ -580,9 +658,9 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
@@ -593,13 +671,39 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
},
},
},
{
name: "member remove, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectMemberAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"user1",
[]string{"PROJECT_OWNER"}...,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
projectID: "project1",
userID: "user1",
resourceOwner: "org1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner)
got, err := r.RemoveProjectMember(context.Background(), tt.args.projectID, tt.args.userID, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@@ -194,7 +194,7 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string,
events = append(events, user.NewUserRemovedEvent(ctx, userAgg, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain))
for _, grantID := range cascadingGrantIDs {
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true)
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil)
if err != nil {
logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant")
continue
@@ -327,18 +327,18 @@ func (c *Commands) UserDomainClaimedSent(ctx context.Context, orgID, userID stri
return err
}
func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner string) (err error) {
func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner string) (_ string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
return "", err
}
if !isUserStateExists(existingUser.UserState) {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-uXHNj", "Errors.User.NotFound")
return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-uXHNj", "Errors.User.NotFound")
}
return nil
return existingUser.ResourceOwner, nil
}
func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *UserWriteModel, err error) {

View File

@@ -2,7 +2,7 @@ package command
import (
"context"
"reflect"
"slices"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
@@ -15,11 +15,14 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (_ *domain.UserGrant, err error) {
// AddUserGrant authorizes a user for a project with the given role keys.
// The project must be owned by or granted to the resourceOwner.
// If the resourceOwner is nil, the project must be owned by the project that belongs to usergrant.ProjectID.
func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (_ *domain.UserGrant, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
event, addedUserGrant, err := c.addUserGrant(ctx, usergrant, resourceOwner)
event, addedUserGrant, err := c.addUserGrant(ctx, usergrant, check)
if err != nil {
return nil, err
}
@@ -35,11 +38,11 @@ func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant
return userGrantWriteModelToUserGrant(addedUserGrant), nil
}
func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (command eventstore.Command, _ *UserGrantWriteModel, err error) {
func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, check UserGrantPermissionCheck) (command eventstore.Command, _ *UserGrantWriteModel, err error) {
if !userGrant.IsValid() {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-kVfMa", "Errors.UserGrant.Invalid")
}
err = c.checkUserGrantPreCondition(ctx, userGrant, resourceOwner)
err = c.checkUserGrantPreCondition(ctx, userGrant, check)
if err != nil {
return nil, nil, err
}
@@ -48,7 +51,7 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant
return nil, nil, err
}
addedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, resourceOwner)
addedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, userGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&addedUserGrant.WriteModel)
command = usergrant.NewUserGrantAddedEvent(
ctx,
@@ -61,54 +64,51 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant
return command, addedUserGrant, nil
}
func (c *Commands) ChangeUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (_ *domain.UserGrant, err error) {
event, changedUserGrant, err := c.changeUserGrant(ctx, userGrant, resourceOwner, false)
func (c *Commands) ChangeUserGrant(ctx context.Context, userGrant *domain.UserGrant, cascade, ignoreUnchanged bool, check UserGrantPermissionCheck) (_ *domain.UserGrant, err error) {
if userGrant.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid")
}
existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, "")
if err != nil {
return nil, err
}
if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound")
}
grantUnchanged := slices.Equal(existingUserGrant.RoleKeys, userGrant.RoleKeys)
if grantUnchanged {
if ignoreUnchanged {
return userGrantWriteModelToUserGrant(existingUserGrant), nil
}
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rs8fy", "Errors.UserGrant.NotChanged")
}
userGrant.UserID = existingUserGrant.UserID
userGrant.ProjectID = existingUserGrant.ProjectID
userGrant.ProjectGrantID = existingUserGrant.ProjectGrantID
userGrant.ResourceOwner = existingUserGrant.ResourceOwner
err = c.checkUserGrantPreCondition(ctx, userGrant, check)
if err != nil {
return nil, err
}
changedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, userGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel)
var event eventstore.Command = usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, userGrant.RoleKeys)
if cascade {
event = usergrant.NewUserGrantCascadeChangedEvent(ctx, userGrantAgg, userGrant.RoleKeys)
}
pushedEvents, err := c.eventstore.Push(ctx, event)
if err != nil {
return nil, err
}
err = AppendAndReduce(changedUserGrant, pushedEvents...)
err = AppendAndReduce(existingUserGrant, pushedEvents...)
if err != nil {
return nil, err
}
return userGrantWriteModelToUserGrant(changedUserGrant), nil
}
func (c *Commands) changeUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string, cascade bool) (_ eventstore.Command, _ *UserGrantWriteModel, err error) {
if userGrant.AggregateID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid")
}
existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, userGrant.ResourceOwner)
if err != nil {
return nil, nil, err
}
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
if err != nil {
return nil, nil, err
}
if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved {
return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound")
}
if reflect.DeepEqual(existingUserGrant.RoleKeys, userGrant.RoleKeys) {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rs8fy", "Errors.UserGrant.NotChanged")
}
userGrant.ProjectID = existingUserGrant.ProjectID
userGrant.ProjectGrantID = existingUserGrant.ProjectGrantID
err = c.checkUserGrantPreCondition(ctx, userGrant, resourceOwner)
if err != nil {
return nil, nil, err
}
changedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, resourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel)
if cascade {
return usergrant.NewUserGrantCascadeChangedEvent(ctx, userGrantAgg, userGrant.RoleKeys), existingUserGrant, nil
}
return usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, userGrant.RoleKeys), existingUserGrant, nil
return userGrantWriteModelToUserGrant(existingUserGrant), nil
}
func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID string, roleKeys []string, cascade bool) (_ eventstore.Command, err error) {
@@ -144,8 +144,8 @@ func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID stri
return usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, existingUserGrant.RoleKeys), nil
}
func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) {
if grantID == "" || resourceOwner == "" {
func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID string, resourceOwner string, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) {
if grantID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-N2OhG", "Errors.UserGrant.IDMissing")
}
@@ -157,14 +157,17 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound")
}
if existingUserGrant.State != domain.UserGrantStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1S9gx", "Errors.UserGrant.NotActive")
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
if check != nil {
err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, "")
} else {
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
}
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
if err != nil {
return nil, err
}
deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner)
deactivateUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, usergrant.NewUserGrantDeactivatedEvent(ctx, userGrantAgg))
if err != nil {
@@ -177,8 +180,8 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) {
if grantID == "" || resourceOwner == "" {
func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID string, resourceOwner string, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) {
if grantID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qxy8v", "Errors.UserGrant.IDMissing")
}
@@ -190,13 +193,17 @@ func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwn
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Lp0gs", "Errors.UserGrant.NotFound")
}
if existingUserGrant.State != domain.UserGrantStateInactive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1ML0v", "Errors.UserGrant.NotInactive")
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
if check != nil {
err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, "")
} else {
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
}
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
if err != nil {
return nil, err
}
deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner)
deactivateUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, usergrant.NewUserGrantReactivatedEvent(ctx, userGrantAgg))
if err != nil {
@@ -209,12 +216,14 @@ func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwn
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
func (c *Commands) RemoveUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) {
event, existingUserGrant, err := c.removeUserGrant(ctx, grantID, resourceOwner, false)
func (c *Commands) RemoveUserGrant(ctx context.Context, grantID string, resourceOwner string, ignoreNotFound bool, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) {
event, existingUserGrant, err := c.removeUserGrant(ctx, grantID, resourceOwner, false, ignoreNotFound, check)
if err != nil {
return nil, err
}
if event == nil {
return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx, event)
if err != nil {
return nil, err
@@ -232,7 +241,7 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r
}
events := make([]eventstore.Command, len(grantIDs))
for i, grantID := range grantIDs {
event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false)
event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false, false, nil)
if err != nil {
return err
}
@@ -242,7 +251,7 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r
return err
}
func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner string, cascade bool) (_ eventstore.Command, writeModel *UserGrantWriteModel, err error) {
func (c *Commands) removeUserGrant(ctx context.Context, grantID string, resourceOwner string, cascade, ignoreNotFound bool, check UserGrantPermissionCheck) (_ eventstore.Command, writeModel *UserGrantWriteModel, err error) {
if grantID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-J9sc5", "Errors.UserGrant.IDMissing")
}
@@ -252,15 +261,22 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s
return nil, nil, err
}
if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved {
if ignoreNotFound {
return nil, existingUserGrant, nil
}
return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-1My0t", "Errors.UserGrant.NotFound")
}
if !cascade {
if !cascade && check == nil {
err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID)
if err != nil {
return nil, nil, err
}
}
if check != nil {
if err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, ""); err != nil {
return nil, nil, err
}
}
removeUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&removeUserGrant.WriteModel)
if cascade {
@@ -279,7 +295,7 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s
existingUserGrant.ProjectGrantID), existingUserGrant, nil
}
func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, resourceOwner string) (writeModel *UserGrantWriteModel, err error) {
func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID string, resourceOwner string) (writeModel *UserGrantWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -291,31 +307,46 @@ func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, res
return writeModel, nil
}
func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) {
func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (err error) {
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeUserGrant) {
return c.checkUserGrantPreConditionOld(ctx, usergrant, resourceOwner)
return c.checkUserGrantPreConditionOld(ctx, usergrant, check)
}
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil {
if _, err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil {
return err
}
existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant, resourceOwner)
if usergrant.ProjectGrantID != "" || usergrant.ResourceOwner == "" {
projectOwner, grantID, err := c.searchProjectOwnerAndGrantID(ctx, usergrant.ProjectID, "")
if err != nil {
return err
}
if usergrant.ResourceOwner == "" {
usergrant.ResourceOwner = projectOwner
}
if usergrant.ProjectGrantID == "" {
usergrant.ProjectGrantID = grantID
}
}
existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant)
if err != nil {
return err
}
if usergrant.HasInvalidRoles(existingRoleKeys) {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound")
}
return nil
if check != nil {
return check(usergrant.ProjectID, usergrant.ProjectGrantID)(usergrant.ResourceOwner, "")
}
return checkExplicitProjectPermission(ctx, usergrant.ProjectGrantID, usergrant.ProjectID)
}
// this code needs to be rewritten anyways as soon as we improved the fields handling
//
//nolint:gocognit
func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (existingRoleKeys []string, err error) {
func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant) (existingRoleKeys []string, err error) {
criteria := []map[eventstore.FieldType]any{
// project state query
{
@@ -327,7 +358,7 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra
// granted org query
{
eventstore.FieldTypeAggregateType: org.AggregateType,
eventstore.FieldTypeAggregateID: resourceOwner,
eventstore.FieldTypeAggregateID: userGrant.ResourceOwner,
eventstore.FieldTypeFieldName: org.OrgStateSearchField,
eventstore.FieldTypeObjectType: org.OrgSearchType,
},
@@ -386,7 +417,7 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra
case project.ProjectGrantGrantedOrgIDSearchField:
var orgID string
err := result.Value.Unmarshal(&orgID)
if err != nil || orgID != resourceOwner {
if err != nil || orgID != userGrant.ResourceOwner {
return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound")
}
case project.ProjectGrantStateSearchField:
@@ -425,26 +456,63 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra
return existingRoleKeys, nil
}
func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) {
func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID, resourceOwner)
preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID, usergrant.ResourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, preConditions)
if err != nil {
return err
}
if usergrant.ResourceOwner == "" {
usergrant.ResourceOwner = preConditions.ProjectResourceOwner
}
if usergrant.ProjectGrantID == "" {
usergrant.ProjectGrantID = preConditions.ProjectGrantID
}
if !preConditions.UserExists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-4f8sg", "Errors.User.NotFound")
}
if usergrant.ProjectGrantID == "" && !preConditions.ProjectExists {
projectIsOwned := usergrant.ResourceOwner == "" || usergrant.ResourceOwner == preConditions.ProjectResourceOwner
if projectIsOwned && !preConditions.ProjectExists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-3n77S", "Errors.Project.NotFound")
}
if usergrant.ProjectGrantID != "" && !preConditions.ProjectGrantExists {
if !projectIsOwned && !preConditions.ProjectGrantExists {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-4m9ff", "Errors.Project.Grant.NotFound")
}
if usergrant.HasInvalidRoles(preConditions.ExistingRoleKeys) {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound")
}
return nil
if check != nil {
return check(usergrant.ProjectID, usergrant.ProjectGrantID)(usergrant.ResourceOwner, "")
}
return checkExplicitProjectPermission(ctx, usergrant.ProjectGrantID, usergrant.ProjectID)
}
func (c *Commands) searchProjectOwnerAndGrantID(ctx context.Context, projectID string, grantedOrgID string) (projectOwner string, grantID string, err error) {
grantIDQuery := map[eventstore.FieldType]any{
eventstore.FieldTypeAggregateType: project.AggregateType,
eventstore.FieldTypeAggregateID: projectID,
eventstore.FieldTypeFieldName: project.ProjectGrantGrantedOrgIDSearchField,
}
if grantedOrgID != "" {
grantIDQuery[eventstore.FieldTypeValue] = grantedOrgID
grantIDQuery[eventstore.FieldTypeObjectType] = project.ProjectGrantSearchType
}
results, err := c.eventstore.Search(ctx, grantIDQuery)
if err != nil {
return "", "", err
}
for _, result := range results {
projectOwner = result.Aggregate.ResourceOwner
if grantedOrgID != "" && grantedOrgID == projectOwner {
return projectOwner, "", nil
}
if result.Object.Type == project.ProjectGrantSearchType {
return projectOwner, result.Object.ID, nil
}
}
return projectOwner, grantID, err
}

View File

@@ -36,6 +36,7 @@ func (wm *UserGrantWriteModel) Reduce() error {
wm.ProjectGrantID = e.ProjectGrantID
wm.RoleKeys = e.RoleKeys
wm.State = domain.UserGrantStateActive
wm.ResourceOwner = e.Aggregate().ResourceOwner
case *usergrant.UserGrantChangedEvent:
wm.RoleKeys = e.RoleKeys
case *usergrant.UserGrantCascadeChangedEvent:
@@ -86,17 +87,18 @@ func UserGrantAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Agg
type UserGrantPreConditionReadModel struct {
eventstore.WriteModel
UserID string
ProjectID string
ProjectGrantID string
ResourceOwner string
UserExists bool
ProjectExists bool
ProjectGrantExists bool
ExistingRoleKeys []string
UserID string
ProjectID string
ProjectResourceOwner string
ProjectGrantID string
ResourceOwner string
UserExists bool
ProjectExists bool
ProjectGrantExists bool
ExistingRoleKeys []string
}
func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID, resourceOwner string) *UserGrantPreConditionReadModel {
func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID string, resourceOwner string) *UserGrantPreConditionReadModel {
return &UserGrantPreConditionReadModel{
UserID: userID,
ProjectID: projectID,
@@ -117,15 +119,19 @@ func (wm *UserGrantPreConditionReadModel) Reduce() error {
case *user.UserRemovedEvent:
wm.UserExists = false
case *project.ProjectAddedEvent:
if wm.ProjectGrantID == "" && wm.ResourceOwner == e.Aggregate().ResourceOwner {
if wm.ResourceOwner == "" || wm.ResourceOwner == e.Aggregate().ResourceOwner {
wm.ProjectExists = true
}
wm.ProjectResourceOwner = e.Aggregate().ResourceOwner
case *project.ProjectRemovedEvent:
wm.ProjectExists = false
case *project.GrantAddedEvent:
if wm.ProjectGrantID == e.GrantID && wm.ResourceOwner == e.GrantedOrgID {
if (wm.ProjectGrantID == e.GrantID || wm.ProjectGrantID == "") && wm.ResourceOwner != "" && wm.ResourceOwner == e.GrantedOrgID {
wm.ProjectGrantExists = true
wm.ExistingRoleKeys = e.RoleKeys
if wm.ProjectGrantID == "" {
wm.ProjectGrantID = e.GrantID
}
}
case *project.GrantChangedEvent:
if wm.ProjectGrantID == e.GrantID {

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, res
if err != nil {
return err
}
if err = c.checkUserExists(ctx, userID, resourceOwner); err != nil {
if _, err = c.checkUserExists(ctx, userID, resourceOwner); err != nil {
return err
}

View File

@@ -56,7 +56,7 @@ func (c *Commands) BulkAddedUserIDPLinks(ctx context.Context, userID, resourceOw
return zerrors.ThrowInvalidArgument(nil, "COMMAND-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded")
}
if err := c.checkUserExists(ctx, userID, resourceOwner); err != nil {
if _, err := c.checkUserExists(ctx, userID, resourceOwner); err != nil {
return err
}

View File

@@ -1,6 +1,7 @@
package command
import (
"bytes"
"context"
"github.com/zitadel/zitadel/internal/domain"
@@ -14,12 +15,25 @@ func (c *Commands) SetUserMetadata(ctx context.Context, metadata *domain.Metadat
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
err = c.checkUserExists(ctx, userID, resourceOwner)
userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil {
return nil, err
}
setMetadata, err := c.getUserMetadataModelByID(ctx, userID, userResourceOwner, metadata.Key)
if err != nil {
return nil, err
}
setMetadata := NewUserMetadataWriteModel(userID, resourceOwner, metadata.Key)
userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel)
// return if no change in the metadata
if bytes.Equal(setMetadata.Value, metadata.Value) {
return writeModelToUserMetadata(setMetadata), nil
}
event, err := c.setUserMetadata(ctx, userAgg, metadata)
if err != nil {
return nil, err
@@ -40,20 +54,35 @@ func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwne
if len(metadatas) == 0 {
return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
events := make([]eventstore.Command, len(metadatas))
setMetadata := NewUserMetadataListWriteModel(userID, resourceOwner)
if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil {
return nil, err
}
events := make([]eventstore.Command, 0)
setMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner)
if err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel)
for i, data := range metadatas {
for _, data := range metadatas {
// if no change to metadata no event has to be pushed
if existingValue, ok := setMetadata.metadataList[data.Key]; ok && bytes.Equal(existingValue, data.Value) {
continue
}
event, err := c.setUserMetadata(ctx, userAgg, data)
if err != nil {
return nil, err
}
events[i] = event
events = append(events, event)
}
// no changes for the metadata
if len(events) == 0 {
return writeModelToObjectDetails(&setMetadata.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
@@ -84,11 +113,16 @@ func (c *Commands) RemoveUserMetadata(ctx context.Context, metadataKey, userID,
if metadataKey == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "META-2n0fs", "Errors.Metadata.Invalid")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, resourceOwner, metadataKey)
if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil {
return nil, err
}
removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, userResourceOwner, metadataKey)
if err != nil {
return nil, err
}
@@ -116,13 +150,17 @@ func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceO
if len(metadataKeys) == 0 {
return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData")
}
err = c.checkUserExists(ctx, userID, resourceOwner)
userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil {
return nil, err
}
events := make([]eventstore.Command, len(metadataKeys))
removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, resourceOwner)
removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner)
if err != nil {
return nil, err
}
@@ -153,24 +191,6 @@ func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceO
return writeModelToObjectDetails(&removeMetadata.WriteModel), nil
}
func (c *Commands) removeUserMetadataFromOrg(ctx context.Context, resourceOwner string) ([]eventstore.Command, error) {
existingUserMetadata, err := c.getUserMetadataByOrgListModelByID(ctx, resourceOwner)
if err != nil {
return nil, err
}
if len(existingUserMetadata.UserMetadata) == 0 {
return nil, nil
}
events := make([]eventstore.Command, 0)
for key, value := range existingUserMetadata.UserMetadata {
if len(value) == 0 {
continue
}
events = append(events, user.NewMetadataRemovedAllEvent(ctx, &user.NewAggregate(key, resourceOwner).Aggregate))
}
return events, nil
}
func (c *Commands) removeUserMetadata(ctx context.Context, userAgg *eventstore.Aggregate, metadataKey string) (command eventstore.Command, err error) {
command = user.NewMetadataRemovedEvent(
ctx,
@@ -197,12 +217,3 @@ func (c *Commands) getUserMetadataListModelByID(ctx context.Context, userID, res
}
return userMetadataWriteModel, nil
}
func (c *Commands) getUserMetadataByOrgListModelByID(ctx context.Context, resourceOwner string) (*UserMetadataByOrgListWriteModel, error) {
userMetadataWriteModel := NewUserMetadataByOrgListWriteModel(resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, userMetadataWriteModel)
if err != nil {
return nil, err
}
return userMetadataWriteModel, nil
}

View File

@@ -16,7 +16,8 @@ import (
func TestCommandSide_SetUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@@ -39,10 +40,10 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -60,8 +61,7 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -78,7 +78,17 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -93,10 +103,9 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
},
},
{
name: "add metadata, ok",
name: "add metadata, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -113,6 +122,43 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
Value: []byte("value"),
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "add metadata, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(),
expectPush(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
@@ -121,6 +167,7 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -143,11 +190,116 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
},
},
},
{
name: "add metadata, reset, invalid",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "add metadata, reset, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(
eventFromEventPusher(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
),
),
),
expectPush(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value2"),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadata: &domain.Metadata{
Key: "key",
Value: []byte("value2"),
},
},
res: res{
want: &domain.Metadata{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
Key: "key",
Value: []byte("value2"),
State: domain.MetadataStateActive,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.SetUserMetadata(tt.args.ctx, tt.args.metadata, tt.args.userID, tt.args.orgID)
if tt.res.err == nil {
@@ -165,7 +317,8 @@ func TestCommandSide_SetUserMetadata(t *testing.T) {
func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@@ -188,9 +341,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
{
name: "empty meta data list, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -204,8 +355,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -225,8 +375,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -243,7 +392,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
),
),
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -259,10 +410,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
},
},
{
name: "add metadata, ok",
name: "add metadata, no permission",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -279,6 +429,43 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []*domain.Metadata{
{Key: "key", Value: []byte("value")},
{Key: "key1", Value: []byte("value1")},
},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "add metadata, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
expectFilter(),
expectPush(
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
@@ -292,6 +479,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -312,7 +500,8 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.BulkSetUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...)
if tt.res.err == nil {
@@ -330,7 +519,8 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
func TestCommandSide_UserRemoveMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@@ -353,8 +543,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -371,9 +560,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -388,8 +575,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
{
name: "meta data not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -408,6 +594,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
),
expectFilter(),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -419,11 +606,43 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
err: zerrors.IsNotFound,
},
},
{
name: "remove metadata, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataKey: "key",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "remove metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -456,6 +675,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -473,7 +693,8 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.RemoveUserMetadata(tt.args.ctx, tt.args.metadataKey, tt.args.userID, tt.args.orgID)
if tt.res.err == nil {
@@ -491,7 +712,8 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@@ -514,9 +736,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
{
name: "empty meta data list, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -530,8 +750,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
{
name: "user not existing, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -548,8 +767,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
{
name: "remove metadata keys not existing, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -576,6 +794,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -590,8 +809,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
{
name: "invalid metadata, pre condition error",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -625,6 +843,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -636,11 +855,43 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "remove metadata, no permission",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
metadataList: []string{"key", "key1"},
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "remove metadata, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -684,6 +935,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -701,7 +953,8 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.BulkRemoveUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...)
if tt.res.err == nil {

View File

@@ -2,7 +2,6 @@ package command
import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/domain"
@@ -150,7 +149,7 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin
events = append(events, user.NewUserRemovedEvent(ctx, &existingUser.Aggregate().Aggregate, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain))
for _, grantID := range cascadingGrantIDs {
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true)
removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, true, nil)
if err != nil {
logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant")
continue